Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor joystick handling #1811

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 81 additions & 50 deletions MobiFlight/Joysticks/Joystick.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,54 +96,22 @@ protected virtual void EnumerateDevices()
{
this.DIJoystick.GetObjectInfoById(device.ObjectId);

int offset = device.Offset;
int usage = device.Usage;
ObjectAspect aspect = device.Aspect;
String name = device.Name;

bool IsAxis = (device.ObjectId.Flags & DeviceObjectTypeFlags.AbsoluteAxis) > 0;
bool IsButton = (device.ObjectId.Flags & DeviceObjectTypeFlags.Button) > 0;
bool IsPOV = (device.ObjectId.Flags & DeviceObjectTypeFlags.PointOfViewController) > 0;


if (IsAxis && Axes.Count < DIJoystick.Capabilities.AxeCount)
{
String axisName;
string axisLabel = "Unknown";
try
{
var OffsetAxisName = GetAxisNameForUsage(usage);
axisName = $"{AxisPrefix} {OffsetAxisName}";
axisLabel = MapDeviceNameToLabel($"{AxisPrefix} {OffsetAxisName}");

} catch (ArgumentOutOfRangeException)
{
Log.Instance.log($"Axis can't be mapped: {DIJoystick.Information.InstanceName} Aspect: {aspect} Offset: {offset} Usage: {usage} Axis: {name} Label: {axisLabel}.", LogSeverity.Error);
continue;
}
Axes.Add(new JoystickDevice() { Name = axisName, Label = axisLabel, Type = DeviceType.AnalogInput, JoystickDeviceType = JoystickDeviceType.Axis});
Log.Instance.log($"Added {DIJoystick.Information.InstanceName} Aspect {aspect} + Offset: {offset} Usage: {usage} Axis: {name} Label: {axisLabel}.", LogSeverity.Debug);

RegisterAxis(device);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method has three sub-methods abstracted for more specific overrides in derived classes and to keep function length down - derived classes do not need to copy the full size of the method if all they want to change is a line or two.

}
else if (IsButton)
{
// Use the device.Usage value so this is consistent with how Axes are referenced and avoid ID collisions
// when looking up names in the the .joystick.json file.
var buttonName = $"{ButtonPrefix} {device.Usage}";
var buttonLabel = MapDeviceNameToLabel(buttonName);
Buttons.Add(new JoystickDevice() { Name = buttonName, Label = buttonLabel, Type = DeviceType.Button, JoystickDeviceType = JoystickDeviceType.Button });
Log.Instance.log($"Added {DIJoystick.Information.InstanceName} Aspect: {aspect} Offset: {offset} Usage: {usage} Button: {name} Label: {buttonLabel}.", LogSeverity.Debug);
RegisterButton(device);
}
else if (IsPOV)
{
POV.Add(new JoystickDevice() { Name = $"{PovPrefix} {name}U", Label = $"{name} (↑)", Type = DeviceType.Button, JoystickDeviceType = JoystickDeviceType.POV });
POV.Add(new JoystickDevice() { Name = $"{PovPrefix} {name}UR", Label = $"{name} (↗)", Type = DeviceType.Button, JoystickDeviceType = JoystickDeviceType.POV });
POV.Add(new JoystickDevice() { Name = $"{PovPrefix} {name}R", Label = $"{name} (→)", Type = DeviceType.Button, JoystickDeviceType = JoystickDeviceType.POV });
POV.Add(new JoystickDevice() { Name = $"{PovPrefix} {name}DR", Label = $"{name} (↘)", Type = DeviceType.Button, JoystickDeviceType = JoystickDeviceType.POV });
POV.Add(new JoystickDevice() { Name = $"{PovPrefix} {name}D", Label = $"{name} (↓)", Type = DeviceType.Button, JoystickDeviceType = JoystickDeviceType.POV });
POV.Add(new JoystickDevice() { Name = $"{PovPrefix} {name}DL", Label = $"{name} (↙)", Type = DeviceType.Button, JoystickDeviceType = JoystickDeviceType.POV });
POV.Add(new JoystickDevice() { Name = $"{PovPrefix} {name}L", Label = $"{name} (←)", Type = DeviceType.Button, JoystickDeviceType = JoystickDeviceType.POV });
POV.Add(new JoystickDevice() { Name = $"{PovPrefix} {name}UL", Label = $"{name} (↖)", Type = DeviceType.Button, JoystickDeviceType = JoystickDeviceType.POV });
RegisterPOV(device);
}
else
{
Expand All @@ -152,6 +120,69 @@ protected virtual void EnumerateDevices()
}
}

protected virtual void RegisterAxis(DeviceObjectInstance device)
{
String axisName;
string axisLabel = "Unknown";
try
{
var OffsetAxisName = GetAxisNameForUsage(device.Usage);
axisName = $"{AxisPrefix} {OffsetAxisName}";
axisLabel = MapDeviceNameToLabel($"{AxisPrefix} {OffsetAxisName}");

}
catch (ArgumentOutOfRangeException)
{
LogAddInputError(device, axisLabel, "Axis can't be mapped");
return;
}
Axes.Add(new JoystickDevice() { Name = axisName, Label = axisLabel, Type = DeviceType.AnalogInput, JoystickDeviceType = JoystickDeviceType.Axis });
LogInputAdded(device, axisLabel);
}

protected virtual void RegisterButton(DeviceObjectInstance device)
{
// Use the device.Usage value so this is consistent with how Axes are referenced and avoid ID collisions
// when looking up names in the the .joystick.json file.
var buttonName = $"{ButtonPrefix} {device.Usage}";
var buttonLabel = MapDeviceNameToLabel(buttonName);
Buttons.Add(new JoystickDevice() { Name = buttonName, Label = buttonLabel, Type = DeviceType.Button, JoystickDeviceType = JoystickDeviceType.Button });
LogInputAdded(device, buttonLabel);
}

protected virtual void RegisterPOV(DeviceObjectInstance device)
{
String name = device.Name;

POV.Add(new JoystickDevice() { Name = $"{PovPrefix} {name}U", Label = $"{name} (↑)", Type = DeviceType.Button, JoystickDeviceType = JoystickDeviceType.POV });
POV.Add(new JoystickDevice() { Name = $"{PovPrefix} {name}UR", Label = $"{name} (↗)", Type = DeviceType.Button, JoystickDeviceType = JoystickDeviceType.POV });
POV.Add(new JoystickDevice() { Name = $"{PovPrefix} {name}R", Label = $"{name} (→)", Type = DeviceType.Button, JoystickDeviceType = JoystickDeviceType.POV });
POV.Add(new JoystickDevice() { Name = $"{PovPrefix} {name}DR", Label = $"{name} (↘)", Type = DeviceType.Button, JoystickDeviceType = JoystickDeviceType.POV });
POV.Add(new JoystickDevice() { Name = $"{PovPrefix} {name}D", Label = $"{name} (↓)", Type = DeviceType.Button, JoystickDeviceType = JoystickDeviceType.POV });
POV.Add(new JoystickDevice() { Name = $"{PovPrefix} {name}DL", Label = $"{name} (↙)", Type = DeviceType.Button, JoystickDeviceType = JoystickDeviceType.POV });
POV.Add(new JoystickDevice() { Name = $"{PovPrefix} {name}L", Label = $"{name} (←)", Type = DeviceType.Button, JoystickDeviceType = JoystickDeviceType.POV });
POV.Add(new JoystickDevice() { Name = $"{PovPrefix} {name}UL", Label = $"{name} (↖)", Type = DeviceType.Button, JoystickDeviceType = JoystickDeviceType.POV });
LogInputAdded(device, name);
}

protected void LogInputAdded(DeviceObjectInstance device, string label)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trading debug information (calling function name) for DRYness here. If having the log point to dedicated logging methods is unwanted, I have a different compromise in mind as an alternative option (generate the string in a method and call Log.Instance.log with that).

{
int offset = device.Offset;
int usage = device.Usage;
ObjectAspect aspect = device.Aspect;
String name = device.Name;
Log.Instance.log($"Added {DIJoystick.Information.InstanceName} Aspect: {aspect} Offset: {offset} Usage: {usage} Button: {name} Label: {label}.", LogSeverity.Debug);
}

protected void LogAddInputError(DeviceObjectInstance device, string label, string errmsg)
{
int offset = device.Offset;
int usage = device.Usage;
ObjectAspect aspect = device.Aspect;
String name = device.Name;
Log.Instance.log($"{errmsg}: {DIJoystick.Information.InstanceName} Aspect: {aspect} Offset: {offset} Usage: {usage} Axis: {name} Label: {label}.", LogSeverity.Error);
}

public string MapDeviceNameToLabel(string deviceName)
{
// First try and look for a custom label.
Expand All @@ -160,7 +191,6 @@ public string MapDeviceNameToLabel(string deviceName)
{
return input.Label;
}

string result = string.Empty;

if (deviceName.StartsWith(ButtonPrefix))
Expand Down Expand Up @@ -216,32 +246,33 @@ public List<ListItem<IBaseDevice>> GetAvailableDevicesAsListItems()

private List<JoystickDevice> GetButtonsSorted()
{
var buttons = Buttons.ToArray().ToList();
buttons.Sort(SortByPositionInDefintion);
return Buttons;
return Buttons.OrderBy(button => GetIndexForJoystickDevice(button)).ToList();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This addresses two nuisances (cf. issue #1808):

  • The mix of member "Buttons" and local "buttons" led to the sorted list being completely ignored
  • List.Sort() uses an unstable sorting algorithm, effectively applying an undefined permutation to buttons without a sorting index.

The Lambda could also be a function pointer, writing it this way mainly makes it more explicit that it is acting on individual buttons.

}

private List<JoystickDevice> GetAxisSorted()
{
var axes = Axes.ToArray().ToList();
Axes.Sort(SortByPositionInDefintion);

return Axes;
return Axes.OrderBy(axis => GetIndexForJoystickDevice(axis)).ToList();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the buttons, with the added fun that the sort function was run on the member axes, not the local axes.

}

public int GetIndexForKey(string key)
public virtual int GetIndexForKey(string key)
{
return Definition?.Inputs?.FindIndex(input => input.Name == key) ?? 0;
// -2 = definition not found / no inputs in definition
// -1 = index not found
// 0 or greater: valid index
return Definition?.Inputs?.FindIndex(input => input.Name == key) ?? -2;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented some outputs and changed the return value in case of null pointers from a valid index to something the caller can distinguish.
Added virtual keyword because this is something that derived classes may want to override: the naming scheme of derived classes is often not based on DirectInput numbers, whereas input.Name is always based on the "Button " scheme.

}

int SortByPositionInDefintion(JoystickDevice b1, JoystickDevice b2)
public int GetIndexForJoystickDevice(JoystickDevice button)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enumerable.OrderBy, unlike List.Sort, allows us to define what property to sort by. Therefore, the approach to integrate the index retrieval into the function is somewhat different.

{
if (GetIndexForKey(b1.Name) == GetIndexForKey(b2.Name)) return 0;
if (GetIndexForKey(b1.Name) > GetIndexForKey(b2.Name)) return 1;
return -1;
var index = GetIndexForKey(button.Name);
// Ensure items in definition appear first
if(index < 0)
{
index = Int32.MaxValue; // force undefined stuff to the end of the list
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inputs most relevant to MobiFlight users are probably spelled out in the definition. Entries not in the definition are likely to be less relevant, so let us move them after the named ones.

}
return index;
}


private void Connect()
{
if (Device == null)
Expand Down
Loading