diff --git a/Amethyst.Plugins.Contract/Actions.cs b/Amethyst.Plugins.Contract/Actions.cs index a02f22aa..e3a02d42 100644 --- a/Amethyst.Plugins.Contract/Actions.cs +++ b/Amethyst.Plugins.Contract/Actions.cs @@ -1,26 +1,25 @@ using System; using System.Diagnostics.CodeAnalysis; -using static System.Runtime.InteropServices.JavaScript.JSType; namespace Amethyst.Plugins.Contract; // Input action declaration for key events -public abstract class KeyInputAction : IComparable, IEquatable +public interface IKeyInputAction : IComparable, IEquatable { /// /// Identifies the action /// - public Guid Guid { get; init; } = Guid.Empty; + public string Guid { get; init; } /// /// Friendly name of the action /// - public string Name { get; set; } = string.Empty; + public string Name { get; set; } /// /// Action description for binding UI /// - public string Description { get; set; } = string.Empty; + public string Description { get; set; } /// /// Action image for binding UI to be shown in info @@ -37,23 +36,57 @@ public abstract class KeyInputAction : IComparable, IEquatable public Action Invoke => _ => { }; + /// + /// Checks whether the action is used for anything + /// + public bool IsUsed => false; + /// /// Action data type (shortcut) /// public Type DataType => typeof(object); +} + +// Input action declaration for key events +public class KeyInputAction : IKeyInputAction +{ + /// + /// Identifies the action + /// + public string Guid { get; init; } = System.Guid.NewGuid().ToString(); + + /// + /// Friendly name of the action + /// + public string Name { get; set; } = "INVALID"; + + /// + /// Action description for binding UI + /// + public string Description { get; set; } = string.Empty; + + /// + /// Action image for binding UI to be shown in info + /// MUST BE OF TYPE Microsoft.UI.Xaml.Controls.Image + /// + /// + /// Make this a getter and return an Image constructed + /// during OnLoad, expect COM-related crashes otherwise + /// + public object? Image { get; set; } /// /// Implement comparator with other actions (by Guid) /// - public int CompareTo(KeyInputAction? other) + public int CompareTo(IKeyInputAction? other) { - return Guid.CompareTo(other?.Guid); + return string.Compare(Guid, other?.Guid, StringComparison.Ordinal); } /// /// Implement comparator with other actions (by Guid) /// - public bool Equals(KeyInputAction? other) + public bool Equals(IKeyInputAction? other) { return Guid.Equals(other?.Guid); } @@ -63,7 +96,7 @@ public bool Equals(KeyInputAction? other) /// public override bool Equals(object? obj) { - return Equals(obj as KeyInputAction); + return Equals(obj as IKeyInputAction); } /// @@ -73,30 +106,25 @@ public override int GetHashCode() { return Guid.GetHashCode(); } -} - -// Input action declaration for key events -public class KeyInputAction : KeyInputAction -{ - [SetsRequiredMembers] - public KeyInputAction() - { - Guid = Guid.NewGuid(); - Name = "INVALID"; - } /// - /// Host import for Invoke() calls + /// Host import for Invoke() calls and stuff + /// Func so you can use it in static context /// - public IAmethystHost? Host { get; set; } + public Func GetHost { get; set; } = () => null; /// /// Invoke the action (shortcut) /// - public new Action Invoke => data => Host?.ReceiveKeyInput(this, data); + public Action Invoke => data => GetHost()?.ReceiveKeyInput(this, data); + + /// + /// Checks whether the action is used for anything + /// + public bool IsUsed => GetHost()?.CheckInputActionIsUsed(this) ?? false; /// /// Action data type (shortcut) /// - public new Type DataType => typeof(T); + public Type DataType => typeof(T); } \ No newline at end of file diff --git a/Amethyst.Plugins.Contract/Amethyst.Plugins.Contract.csproj b/Amethyst.Plugins.Contract/Amethyst.Plugins.Contract.csproj index aa402fd8..7d6d4651 100644 --- a/Amethyst.Plugins.Contract/Amethyst.Plugins.Contract.csproj +++ b/Amethyst.Plugins.Contract/Amethyst.Plugins.Contract.csproj @@ -7,7 +7,7 @@ True Amethyst Device Plugin API (Contract) - 0.3.27-alpha + 0.3.32-alpha x64 diff --git a/Amethyst.Plugins.Contract/Classes.cs b/Amethyst.Plugins.Contract/Classes.cs index dc6cb642..77ff41b8 100644 --- a/Amethyst.Plugins.Contract/Classes.cs +++ b/Amethyst.Plugins.Contract/Classes.cs @@ -111,7 +111,7 @@ public Quaternion Orientation /// [JsonIgnore] [IgnoreDataMember] - public SortedSet SupportedInputActions { get; init; } = new(); + public SortedSet SupportedInputActions { get; init; } = new(); } [DataContract] diff --git a/Amethyst.Plugins.Contract/Contract.cs b/Amethyst.Plugins.Contract/Contract.cs index 1632ae72..9fe19cd7 100644 --- a/Amethyst.Plugins.Contract/Contract.cs +++ b/Amethyst.Plugins.Contract/Contract.cs @@ -282,7 +282,7 @@ public interface IServiceEndpoint /// You will not be able to receive actions from unsupported TrackerType either /// [DefaultValue(null)] - public Dictionary> SupportedInputActions { get; } + public Dictionary> SupportedInputActions { get; } /// /// Get the absolute pose of the HMD, calibrated against the play space @@ -352,7 +352,7 @@ public interface IServiceEndpoint /// /// Process a key input event sent by a device, that was assigned and found /// - public Task ProcessKeyInput(KeyInputAction action, T? data, + public Task ProcessKeyInput(IKeyInputAction action, object? data, TrackerType? receiver, CancellationToken? token = null); } @@ -425,6 +425,11 @@ public interface IAmethystHost /// bool IsTrackedJointValid(TrackedJointType jointType); + /// + /// Check if a tracker with the specified role is enabled and active + /// + bool IsTrackerEnabled(TrackerType trackerType); + /// /// Lock the main update loop while in scope with [lock (UpdateThreadLock) { }] /// This will block AME from updating while locked, and also wait for when the/ @@ -490,7 +495,16 @@ void Log(object message, LogSeverity severity = LogSeverity.Info, [CallerLineNum /// /// Data to be sent, involved with the action /// - void ReceiveKeyInput(KeyInputAction action, T? data); + void ReceiveKeyInput(IKeyInputAction action, object? data); + + /// + /// Check whether a KeyInputAction is used for anything + /// Devices may use this to skip updating unused actions + /// + /// + /// Definition of the input action to validate + /// + bool CheckInputActionIsUsed(IKeyInputAction action); } /// @@ -498,7 +512,7 @@ void Log(object message, LogSeverity severity = LogSeverity.Info, [CallerLineNum /// under "DependencyInstaller" for dependency installation functionality /// Note: you should only use basic/local functionality, as it is unknown /// whether the load context will contain any external libraries you may -/// be relying on - like framework or device proprietary SDKs and such, etc +/// be relying on - like framework or device proprietary SDKs and such, etc. /// public interface IDependencyInstaller { diff --git a/Amethyst/Classes/AppTracker.cs b/Amethyst/Classes/AppTracker.cs index 1cf3877c..477d42cb 100644 --- a/Amethyst/Classes/AppTracker.cs +++ b/Amethyst/Classes/AppTracker.cs @@ -61,7 +61,7 @@ public class AppTracker : INotifyPropertyChanged // Input actions: < SERVICE : < SERVICE ACTION DATA : DEVICE ACTION DATA > > // For "disabled" actions InputActionSource is null, for "hidden" - nothing // Use InputActionsMap for faster and easier data access, along with bindings - public SortedDictionary> InputActions = []; + public SortedDictionary> InputActions = []; public Vector3 OrientationOffset = new(0, 0, 0); // Internal data offset @@ -220,17 +220,39 @@ public bool IsActiveEnabled Role is TrackerType.TrackerWaist or TrackerType.TrackerLeftFoot or TrackerType.TrackerRightFoot || (AppPlugins.CurrentServiceEndpoint?.AdditionalSupportedTrackerTypes.Contains(Role) ?? false); + // Returns all input actions available from the currently selected service, + // paired with their selected sources that will trigger the action if updated + // Note: use InputActionSource.IsValid to check whether the source exists [JsonIgnore] public Dictionary InputActionsMap => InputActions.TryGetValue(AppData.Settings.ServiceEndpointGuid, out var map) ? map : []; + // Returns all input actions available from the currently selected service, + // paired with their current selection state: true for used, false for hidden + // Note: "used" can also mean that the action is shown & disabled by the user [JsonIgnore] public Dictionary AvailableInputActions => AppPlugins.CurrentServiceEndpoint?.SupportedInputActions?.TryGetValue(Role, out var actions) ?? false - ? actions.ToDictionary(x => new InputActionEndpoint { Tracker = Role, Action = x.Guid }, - x => InputActionsMap.Keys.Any(y => y.Tracker == Role && y.Action == x.Guid && y.IsValid)) + ? actions.ToDictionary(x => new InputActionEndpoint { Tracker = Role, Guid = x.Guid }, + x => InputActionsMap.Keys.Any(y => y.Tracker == Role && y.Guid == x.Guid && y.IsValid)) : []; + [JsonIgnore] + public IEnumerable InputActionEntries => + AvailableInputActions.Select(x => new InputActionEntry + { + Action = x.Key, + IsEnabled = x.Value + }).OrderBy(x => x.Name); + + [JsonIgnore] + public IEnumerable InputActionBindingEntries => + InputActionsMap.Select(x => new InputActionBindingEntry + { + Action = x.Key, + Source = x.Value + }).OrderBy(x => x.ActionName); + [JsonIgnore] public bool OverridePhysics { get; set; } [JsonIgnore] public string TrackerName => Interfacing.LocalizedJsonString($"/SharedStrings/Joints/Enum/{(int)Role}"); @@ -757,4 +779,9 @@ public bool IsManagedBy(string guid) { return guid == ManagingDeviceGuid; } +} + +[JsonArray] +public class JsonDictionary : Dictionary +{ } \ No newline at end of file diff --git a/Amethyst/Controls/JointSettingsExpander.xaml b/Amethyst/Controls/JointSettingsExpander.xaml index a2056678..d81cfe34 100644 --- a/Amethyst/Controls/JointSettingsExpander.xaml +++ b/Amethyst/Controls/JointSettingsExpander.xaml @@ -2,116 +2,239 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + x:Class="Amethyst.Controls.JointSettingsExpander" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:util="using:Amethyst.Utils" + xmlns:mvvm="using:Amethyst.MVVM" + xmlns:classes="using:Amethyst.Classes" + xmlns:icons="using:WinUI.Fluent.Icons" + xmlns:controls="using:Amethyst.Controls" + xmlns:pages="using:Amethyst.Pages" + xmlns:generic="using:System.Collections.Generic" + Visibility="{x:Bind Show, Mode=OneWay}" + mc:Ignorable="d"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Amethyst/Controls/JointSettingsExpander.xaml.cs b/Amethyst/Controls/JointSettingsExpander.xaml.cs index 54baa911..a99e261c 100644 --- a/Amethyst/Controls/JointSettingsExpander.xaml.cs +++ b/Amethyst/Controls/JointSettingsExpander.xaml.cs @@ -3,9 +3,12 @@ using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Amethyst.Classes; +using Amethyst.MVVM; +using Amethyst.Pages; using Amethyst.Plugins.Contract; using Amethyst.Utils; using Microsoft.UI.Xaml; @@ -58,6 +61,8 @@ public List Trackers private bool IsSupported => Trackers.All(x => x.IsSupported); + private bool ShowBindingsSection => Trackers.Any(x => x.AvailableInputActions.Count > 0); + private bool IsActiveEnabled { get => Trackers.All(x => x.IsActiveEnabled); @@ -265,4 +270,172 @@ private void OrientationOptionComboBox_SelectionChanged(object sender, Selection if ((sender as ComboBox)!.SelectedIndex < 0) (sender as ComboBox)!.SelectedItem = e.RemovedItems[0]; } + + private void ActionConfigButton_OnClick(object sender, RoutedEventArgs e) + { + // ignored + } + + private void InputActionToggle_OnClick(object sender, RoutedEventArgs e) + { + if (sender is not ToggleMenuFlyoutItem { DataContext: InputActionEntry entry, Parent: ItemsRepeater parent } item || + parent.GetValue(AttachedObject.AttachedObjectProperty) is not AppTracker tracker) return; + + if (!tracker.InputActions.ContainsKey(AppData.Settings.ServiceEndpointGuid)) + tracker.InputActions[AppData.Settings.ServiceEndpointGuid] = []; + + // Set up the binding placeholder now + if (item.IsChecked) + tracker.InputActions[AppData.Settings.ServiceEndpointGuid][entry.Action] = null; + else + tracker.InputActions[AppData.Settings.ServiceEndpointGuid].Remove( + tracker.InputActions[AppData.Settings.ServiceEndpointGuid] + .FirstOrDefault(x => x.Key.Guid == entry.Action.Guid).Key ?? entry.Action); + + // Refresh everything + _blockPropertyToggleSignals = true; + OnPropertyChanged(); // Refresh UI + Trackers.ForEach(x => + { + x.OnPropertyChanged("InputActionEntries"); + x.OnPropertyChanged("InputActionBindingEntries"); + }); + _blockPropertyToggleSignals = false; + + // Hide and save + if (((parent.Parent as StackPanel)?.Parent as FlyoutPresenter)? + .Parent is Microsoft.UI.Xaml.Controls.Primitives.Popup popup) + popup.IsOpen = false; + + AppData.Settings.SaveSettings(); + } + + private void BindingFlyoutItem_OnClick(object sender, RoutedEventArgs e) + { + if (sender is not MenuFlyoutItem { DataContext: InputActionBindingEntry entry } item || + item.GetValue(AttachedString.AttachedStringProperty) is not string command) return; + + var tracker = Trackers.FirstOrDefault(x => x.Role == entry.Action.Tracker); + if (tracker is null) return; // Not related to the updated selection + + switch (command) + { + case "HIDE": + // Remove the action from our bindings + tracker.InputActions[AppData.Settings.ServiceEndpointGuid].Remove( + tracker.InputActions[AppData.Settings.ServiceEndpointGuid] + .FirstOrDefault(x => x.Key.Guid == entry.Action.Guid).Key ?? entry.Action); + break; + case "DISABLE": + // Set up the command as "null" so it's shown but disabled + tracker.InputActions[AppData.Settings.ServiceEndpointGuid][entry.Action] = null; + break; + default: + return; + } + + // Refresh everything + _blockPropertyToggleSignals = true; + OnPropertyChanged(); // Refresh UI + Trackers.ForEach(x => + { + x.OnPropertyChanged("InputActionEntries"); + x.OnPropertyChanged("InputActionBindingEntries"); + }); + _blockPropertyToggleSignals = false; + + // Hide and save + AppData.Settings.SaveSettings(); + } + + private void BindingsFlyout_OnOpening(object sender, object e) + { + if (sender is not MenuFlyout flyout) return; + if (flyout.Items.LastOrDefault()?.DataContext is not InputActionBindingEntry entry) return; + + var tracker = Trackers.FirstOrDefault(x => x.Role == entry.Action.Tracker); + if (tracker is null) return; // Not related to the updated selection + + if (flyout.Items.Count > 4) // Remove the sources + flyout.Items.Take(flyout.Items.Count - 4) + .ToList().ForEach(x => flyout.Items.Remove(x)); + + // Loop over all devices that have accessible actions and add them to the flyout + // Group by Device>Joint>Action(Selectable) + foreach (var device in AppPlugins.TrackingDevicesList.Values + .Where(x => x.TrackedJoints.Any(y => y.SupportedInputActions.Any())) + .OrderByDescending(x => x.Name)) + { + var deviceItem = new MenuFlyoutSubItem { Text = device.Name }; + foreach (var joint in device.TrackedJoints + .Where(x => x.SupportedInputActions.Any())) + { + var jointItem = new MenuFlyoutSubItem { Text = joint.Name }; + foreach (var action in joint.SupportedInputActions.OrderBy(action => action.Name)) + { + var actionItem = new ToggleMenuFlyoutItem + { + Text = action.Name, + IsChecked = entry.Source?.LinkedAction?.Guid == action.Guid, + IsEnabled = entry.Action?.LinkedAction?.DataType == action.DataType + }; + + ToolTipService.SetToolTip(actionItem, + new ToolTip { Content = action.Description }); + + actionItem.Click += (_, _) => + { + // Set up the command binding + tracker.InputActions[AppData.Settings.ServiceEndpointGuid] + [entry.Action] = new InputActionSource + { + Device = device.Guid, + Guid = action.Guid, + Name = action.Name, + Tracker = joint.Role + }; + + // Refresh everything + _blockPropertyToggleSignals = true; + OnPropertyChanged(); // Refresh UI + Trackers.ForEach(x => + { + x.OnPropertyChanged("InputActionEntries"); + x.OnPropertyChanged("InputActionBindingEntries"); + }); + _blockPropertyToggleSignals = false; + + AppData.Settings.SaveSettings(); // Save and signal + device.SignalJoint(device.TrackedJoints.IndexOf(joint)); + }; + jointItem.Items.Add(actionItem); + } + + deviceItem.Items.Add(jointItem); + } + + flyout.Items.Insert(0, deviceItem); + } + } +} + +public class AttachedObject : DependencyObject +{ + public static readonly DependencyProperty AttachedObjectProperty = + DependencyProperty.RegisterAttached( + "AttachedObject", + typeof(object), + typeof(AttachedObject), + new PropertyMetadata(false) + ); + + public static void SetAttachedObject(UIElement element, object value) + { + element.SetValue(AttachedObjectProperty, value); + } + + public static object GetAttachedObject(UIElement element) + { + return element.GetValue(AttachedObjectProperty); + } } \ No newline at end of file diff --git a/Amethyst/MVVM/PluginHost.cs b/Amethyst/MVVM/PluginHost.cs index abb3250e..4ad22523 100644 --- a/Amethyst/MVVM/PluginHost.cs +++ b/Amethyst/MVVM/PluginHost.cs @@ -62,6 +62,13 @@ public bool IsTrackedJointValid(TrackedJointType jointType) return AppPlugins.BaseTrackingDevice.TrackedJoints.Exists(x => x.Role == jointType); } + // Check if a tracker with the specified role is enabled and active + public bool IsTrackerEnabled(TrackerType trackerType) + { + return AppData.Settings.TrackersVector + .Any(x => x.Role == trackerType && x.IsActive); + } + // Lock the main update loop while in scope with [lock (UpdateThreadLock) { }] public object UpdateThreadLock => UpdateLock; @@ -279,14 +286,15 @@ public void RequestExit(string message, bool fatal = false) // Process a key input action called from a single joint // The handler will check whether the action is used anywhere, // and trigger the linked output action if applicable - public void ReceiveKeyInput(KeyInputAction action, T data) + public void ReceiveKeyInput(IKeyInputAction action, object data) { try { // Invoke all linked input actions AppData.Settings.TrackersVector - .SelectMany(x => x.InputActionsMap.Where(y => y.Value.Action == action.Guid)) - .Select(x => x.Key.LinkedAction).ToList().ForEach(x => x?.Invoke(data)); + .SelectMany(x => x.InputActionsMap.Where(y => y.Value?.Guid == action.Guid) + .Select(y => (Tracker: x.Role, Action: y.Key.LinkedAction))).ToList() + .ForEach(x => AppPlugins.CurrentServiceEndpoint.ProcessKeyInput(x.Action, data, x.Tracker)); } catch (Exception e) { @@ -294,6 +302,14 @@ public void ReceiveKeyInput(KeyInputAction action, T data) } } + // Check whether a KeyInputAction is used for anything + // Devices may use this to skip updating unused actions + public bool CheckInputActionIsUsed(IKeyInputAction action) + { + return AppData.Settings.TrackersVector + .Any(x => x.InputActionsMap.Any(y => y.Value?.Guid == action.Guid)); + } + // INTERNAL: Available only via reflection, not defined in the Host interface // Return all devices added from plugins, INCLUDING forwarded ones public Dictionary TrackingDevices => @@ -1101,6 +1117,29 @@ public class AppTrackerEntry public bool IsEnabled => AppData.Settings.TrackersVector.Any(x => x.Role == TrackerRole); } +public class InputActionEntry +{ + public InputActionEndpoint Action { get; set; } + public bool IsEnabled { get; set; } + public string Name => Action?.LinkedAction?.Name ?? "INVALID"; +} + +public class InputActionBindingEntry +{ + public InputActionEndpoint Action { get; set; } + public InputActionSource Source { get; set; } + + public string ActionName => Action?.LinkedAction?.Name ?? "INVALID"; + public string SourceName => Source?.LinkedAction?.Name ?? Source?.Name ?? "Disabled"; // TODO translator, also INVALID->Unavailable + public string ActionNameFormatted => $"{ActionName}:"; + public string ActionDescription => Action?.LinkedAction?.Description; + public string SourceDescription => Source?.LinkedAction?.Description; + + public bool IsEnabled => Source is not null; + public bool IsValid => !IsEnabled || Source?.LinkedAction is not null; + public bool IsInvalid => !IsValid; +} + public static class CollectionExtensions { public static bool AddPlugin(this ICollection collection, DirectoryInfo item) where T : ComposablePartCatalog @@ -1315,18 +1354,19 @@ public static bool AddPlugin(this ICollection collection, DirectoryInfo it } } +[Serializable] public class InputActionEndpoint { // Action's container tracker - public TrackerType Tracker { get; set; } + [JsonProperty] public TrackerType Tracker { get; set; } // Action that should be called - public Guid Action { get; set; } + [JsonProperty] public string Guid { get; set; } // MVVM Stuff [JsonIgnore] [IgnoreDataMember] - public KeyInputAction LinkedAction + public IKeyInputAction LinkedAction { get { @@ -1334,7 +1374,7 @@ public KeyInputAction LinkedAction { return AppPlugins.CurrentServiceEndpoint?.SupportedInputActions? .TryGetValue(Tracker, out var actions) ?? false - ? actions?.First(x => x.Guid == Action) // Find the action + ? actions?.First(x => x.Guid == Guid) // Find the action : null; // If there's no corresponding tracker - give up now } catch (Exception) @@ -1347,24 +1387,25 @@ public KeyInputAction LinkedAction [JsonIgnore] [IgnoreDataMember] public bool IsValid => LinkedAction is not null; } +[Serializable] public class InputActionSource { // The provider device's Guid - public string Device { get; set; } + [JsonProperty] public string Device { get; set; } // The action's friendly name (cached) - public string Name { get; set; } + [JsonProperty] public string Name { get; set; } // Action's container tracker - public TrackedJointType Tracker { get; set; } + [JsonProperty] public TrackedJointType Tracker { get; set; } // Action that should be called - public Guid Action { get; set; } + [JsonProperty] public string Guid { get; set; } // MVVM Stuff [JsonIgnore] [IgnoreDataMember] - public KeyInputAction LinkedAction + public IKeyInputAction LinkedAction { get { @@ -1372,7 +1413,7 @@ public KeyInputAction LinkedAction { return AppPlugins.TrackingDevicesList.TryGetValue(Device, out var device) ? device?.TrackedJoints?.Where(x => x.Role == Tracker) - .Select(x => x.SupportedInputActions.FirstOrDefault(y => y.Guid == Action, null)) + .Select(x => x.SupportedInputActions.FirstOrDefault(y => y.Guid == Guid, null)) .FirstOrDefault(x => x is not null, null) // Return the first valid action : null; } diff --git a/Amethyst/MVVM/ServiceEndpoint.cs b/Amethyst/MVVM/ServiceEndpoint.cs index 854203c3..503590f2 100644 --- a/Amethyst/MVVM/ServiceEndpoint.cs +++ b/Amethyst/MVVM/ServiceEndpoint.cs @@ -67,7 +67,7 @@ public class ServiceEndpoint(string name, string guid, string path, Version vers // Keeps all supported input actions that may be received from ProcessKeyInput // You will not be able to receive actions from unsupported TrackerType either - public Dictionary> SupportedInputActions => Service.SupportedInputActions; + public Dictionary> SupportedInputActions => Service.SupportedInputActions; // Mark as true to tell the user that they need to restart/ // /in case they want to add more trackers after spawning @@ -165,6 +165,13 @@ public TrackerBase GetTrackerPose(string contains, bool canBeFromAmethyst = true return Service.UpdateTrackerPoses(trackerBases, wantReply, token); } + // Process a key input event sent by a device, that was assigned and found + public Task ProcessKeyInput(IKeyInputAction action, T data, + TrackerType? receiver, CancellationToken? token = null) + { + return Service.ProcessKeyInput(action, data, receiver, token); + } + // This is called after the app loads the plugin public void OnLoad() { diff --git a/Amethyst/Pages/General.xaml.cs b/Amethyst/Pages/General.xaml.cs index aaa1e51b..0b363ef2 100644 --- a/Amethyst/Pages/General.xaml.cs +++ b/Amethyst/Pages/General.xaml.cs @@ -732,11 +732,11 @@ private async void CalibrationButton_Click(object sender, RoutedEventArgs e) // If auto-calibration is not supported, proceed straight to manual // Test: supports if the device provides a head joint / otherwise not - if (trackingDevice.TrackedJoints.Any(x => x.Role == TrackedJointType.JointHead)) + if (trackingDevice.TrackedJoints.Any(x => x.Role == TrackedJointType.JointHead) && + AppPlugins.CurrentServiceEndpoint.HeadsetPose != null) { // If manual calibration is not supported, proceed straight to automatic - if (AppPlugins.CurrentServiceEndpoint.HeadsetPose != null && - AppPlugins.CurrentServiceEndpoint.ControllerInputActions is null) ExecuteAutomaticCalibration(); + if (AppPlugins.CurrentServiceEndpoint.ControllerInputActions is null) ExecuteAutomaticCalibration(); return; // Else open the selection pane } diff --git a/Amethyst/Pages/Info.xaml b/Amethyst/Pages/Info.xaml index 53f47a79..b66134e4 100644 --- a/Amethyst/Pages/Info.xaml +++ b/Amethyst/Pages/Info.xaml @@ -229,6 +229,7 @@ _Legend Pendrokar Gehaktman + Jonas Enzo Bergström