diff --git a/Ryujinx.HLE/HOS/Services/Hid/HidDevices/MouseDevice.cs b/Ryujinx.HLE/HOS/Services/Hid/HidDevices/MouseDevice.cs index e07c1d20..66d1b0c4 100644 --- a/Ryujinx.HLE/HOS/Services/Hid/HidDevices/MouseDevice.cs +++ b/Ryujinx.HLE/HOS/Services/Hid/HidDevices/MouseDevice.cs @@ -7,7 +7,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid { public MouseDevice(Switch device, bool active) : base(device, active) { } - public void Update(int mouseX, int mouseY, uint buttons = 0, int scrollX = 0, int scrollY = 0) + public void Update(int mouseX, int mouseY, uint buttons = 0, int scrollX = 0, int scrollY = 0, bool connected = false) { ref RingLifo lifo = ref _device.Hid.SharedMemory.Mouse; @@ -27,6 +27,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid newState.DeltaY = mouseY - previousEntry.DeltaY; newState.WheelDeltaX = scrollX; newState.WheelDeltaY = scrollY; + newState.Attributes = connected ? MouseAttribute.IsConnected : MouseAttribute.None; } lifo.Write(ref newState); diff --git a/Ryujinx.Input/HLE/InputManager.cs b/Ryujinx.Input/HLE/InputManager.cs index 699e521d..bc38cf5a 100644 --- a/Ryujinx.Input/HLE/InputManager.cs +++ b/Ryujinx.Input/HLE/InputManager.cs @@ -23,7 +23,7 @@ namespace Ryujinx.Input.HLE public NpadManager CreateNpadManager() { - return new NpadManager(KeyboardDriver, GamepadDriver); + return new NpadManager(KeyboardDriver, GamepadDriver, MouseDriver); } public TouchScreenManager CreateTouchScreenManager() diff --git a/Ryujinx.Input/HLE/NpadManager.cs b/Ryujinx.Input/HLE/NpadManager.cs index abb820b0..c46f80b0 100644 --- a/Ryujinx.Input/HLE/NpadManager.cs +++ b/Ryujinx.Input/HLE/NpadManager.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.HLE.HOS.Services.Hid; @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; - using CemuHookClient = Ryujinx.Input.Motion.CemuHook.Client; using Switch = Ryujinx.HLE.Switch; @@ -26,22 +25,23 @@ namespace Ryujinx.Input.HLE private readonly IGamepadDriver _keyboardDriver; private readonly IGamepadDriver _gamepadDriver; - + private readonly IGamepadDriver _mouseDriver; private bool _isDisposed; private List _inputConfig; private bool _enableKeyboard; + private bool _enableMouse; private Switch _device; - public NpadManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver) + public NpadManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, IGamepadDriver mouseDriver) { _controllers = new NpadController[MaxControllers]; _cemuHookClient = new CemuHookClient(this); _keyboardDriver = keyboardDriver; _gamepadDriver = gamepadDriver; + _mouseDriver = mouseDriver; _inputConfig = new List(); - _enableKeyboard = false; _gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected; _gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected; @@ -58,13 +58,13 @@ namespace Ryujinx.Input.HLE private void HandleOnGamepadDisconnected(string obj) { // Force input reload - ReloadConfiguration(_inputConfig, _enableKeyboard); + ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse); } private void HandleOnGamepadConnected(string id) { // Force input reload - ReloadConfiguration(_inputConfig, _enableKeyboard); + ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -93,7 +93,7 @@ namespace Ryujinx.Input.HLE } } - public void ReloadConfiguration(List inputConfig, bool enableKeyboard) + public void ReloadConfiguration(List inputConfig, bool enableKeyboard, bool enableMouse) { lock (_lock) { @@ -119,8 +119,9 @@ namespace Ryujinx.Input.HLE } } - _inputConfig = inputConfig; + _inputConfig = inputConfig; _enableKeyboard = enableKeyboard; + _enableMouse = enableMouse; _device.Hid.RefreshInputConfig(inputConfig); } @@ -142,15 +143,15 @@ namespace Ryujinx.Input.HLE } } - public void Initialize(Switch device, List inputConfig, bool enableKeyboard) + public void Initialize(Switch device, List inputConfig, bool enableKeyboard, bool enableMouse) { _device = device; _device.Configuration.RefreshInputConfig = RefreshInputConfigForHLE; - ReloadConfiguration(inputConfig, enableKeyboard); + ReloadConfiguration(inputConfig, enableKeyboard, enableMouse); } - public void Update() + public void Update(float aspectRatio = 0) { lock (_lock) { @@ -206,6 +207,48 @@ namespace Ryujinx.Input.HLE _device.Hid.Keyboard.Update(hleKeyboardInput.Value); } + if (_enableMouse) + { + var mouse = _mouseDriver.GetGamepad("0") as IMouse; + + var mouseInput = IMouse.GetMouseStateSnapshot(mouse); + + uint buttons = 0; + + if (mouseInput.IsPressed(MouseButton.Button1)) + { + buttons |= 1 << 0; + } + + if (mouseInput.IsPressed(MouseButton.Button2)) + { + buttons |= 1 << 1; + } + + if (mouseInput.IsPressed(MouseButton.Button3)) + { + buttons |= 1 << 2; + } + + if (mouseInput.IsPressed(MouseButton.Button4)) + { + buttons |= 1 << 3; + } + + if (mouseInput.IsPressed(MouseButton.Button5)) + { + buttons |= 1 << 4; + } + + var position = IMouse.GetScreenPosition(mouseInput.Position, mouse.ClientSize, aspectRatio); + + _device.Hid.Mouse.Update((int)position.X, (int)position.Y, buttons, (int)mouseInput.Scroll.X, (int)mouseInput.Scroll.Y, true); + } + else + { + _device.Hid.Mouse.Update(0, 0); + } + _device.TamperMachine.UpdateInput(hleInputStates); } } diff --git a/Ryujinx.Input/HLE/TouchScreenManager.cs b/Ryujinx.Input/HLE/TouchScreenManager.cs index 579dcd74..e4b0f8fc 100644 --- a/Ryujinx.Input/HLE/TouchScreenManager.cs +++ b/Ryujinx.Input/HLE/TouchScreenManager.cs @@ -29,7 +29,7 @@ namespace Ryujinx.Input.HLE if (_wasClicking && !isClicking) { MouseStateSnapshot snapshot = IMouse.GetMouseStateSnapshot(_mouse); - var touchPosition = IMouse.GetTouchPosition(snapshot.Position, _mouse.ClientSize, aspectRatio); + var touchPosition = IMouse.GetScreenPosition(snapshot.Position, _mouse.ClientSize, aspectRatio); TouchPoint currentPoint = new TouchPoint { @@ -58,7 +58,7 @@ namespace Ryujinx.Input.HLE if (aspectRatio > 0) { MouseStateSnapshot snapshot = IMouse.GetMouseStateSnapshot(_mouse); - var touchPosition = IMouse.GetTouchPosition(snapshot.Position, _mouse.ClientSize, aspectRatio); + var touchPosition = IMouse.GetScreenPosition(snapshot.Position, _mouse.ClientSize, aspectRatio); TouchAttribute attribute = TouchAttribute.None; diff --git a/Ryujinx.Input/IMouse.cs b/Ryujinx.Input/IMouse.cs index 37de0229..fde150fc 100644 --- a/Ryujinx.Input/IMouse.cs +++ b/Ryujinx.Input/IMouse.cs @@ -23,6 +23,11 @@ namespace Ryujinx.Input /// Vector2 GetPosition(); + /// + /// Get the mouse scroll delta. + /// + Vector2 GetScroll(); + /// /// Get the client size. /// @@ -40,22 +45,21 @@ namespace Ryujinx.Input /// A snaphost of the state of the mouse. public static MouseStateSnapshot GetMouseStateSnapshot(IMouse mouse) { - var position = mouse.GetPosition(); bool[] buttons = new bool[(int)MouseButton.Count]; mouse.Buttons.CopyTo(buttons, 0); - return new MouseStateSnapshot(buttons, position); + return new MouseStateSnapshot(buttons, mouse.GetPosition(), mouse.GetScroll()); } /// - /// Get the touch position of a mouse position relative to the app's view + /// Get the position of a mouse on screen relative to the app's view /// /// The position of the mouse in the client /// The size of the client /// The aspect ratio of the view /// A snaphost of the state of the mouse. - public static Vector2 GetTouchPosition(Vector2 mousePosition, Size clientSize, float aspectRatio) + public static Vector2 GetScreenPosition(Vector2 mousePosition, Size clientSize, float aspectRatio) { float mouseX = mousePosition.X; float mouseY = mousePosition.Y; diff --git a/Ryujinx.Input/MouseStateSnapshot.cs b/Ryujinx.Input/MouseStateSnapshot.cs index 4fbfeebd..ddfdebc6 100644 --- a/Ryujinx.Input/MouseStateSnapshot.cs +++ b/Ryujinx.Input/MouseStateSnapshot.cs @@ -10,17 +10,28 @@ namespace Ryujinx.Input { private bool[] _buttonState; + /// + /// The position of the mouse cursor + /// public Vector2 Position { get; } + /// + /// The scroll delta of the mouse + /// + public Vector2 Scroll { get; } + /// /// Create a new . /// - /// The keys state - public MouseStateSnapshot(bool[] buttonState, Vector2 position) + /// The button state + /// The position of the cursor + /// The scroll delta + public MouseStateSnapshot(bool[] buttonState, Vector2 position, Vector2 scroll) { _buttonState = buttonState; Position = position; + Scroll = scroll; } /// diff --git a/Ryujinx/Config.json b/Ryujinx/Config.json index 0ce0813b..033186fe 100644 --- a/Ryujinx/Config.json +++ b/Ryujinx/Config.json @@ -1,5 +1,5 @@ { - "version": 24, + "version": 27, "enable_file_log": true, "res_scale": 1, "res_scale_custom": 1, @@ -55,6 +55,7 @@ "custom_theme_path": "", "start_fullscreen": false, "enable_keyboard": false, + "enable_mouse": false, "hotkeys": { "toggle_vsync": "Tab" }, diff --git a/Ryujinx/Configuration/ConfigurationFileFormat.cs b/Ryujinx/Configuration/ConfigurationFileFormat.cs index d988b849..04a51815 100644 --- a/Ryujinx/Configuration/ConfigurationFileFormat.cs +++ b/Ryujinx/Configuration/ConfigurationFileFormat.cs @@ -14,7 +14,7 @@ namespace Ryujinx.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 26; + public const int CurrentVersion = 27; public int Version { get; set; } @@ -224,6 +224,11 @@ namespace Ryujinx.Configuration /// public bool EnableKeyboard { get; set; } + /// + /// Enable or disable mouse support (Independent from controllers binding) + /// + public bool EnableMouse { get; set; } + /// /// Hotkey Keyboard Bindings /// diff --git a/Ryujinx/Configuration/ConfigurationState.cs b/Ryujinx/Configuration/ConfigurationState.cs index 11ec1373..1769dfa9 100644 --- a/Ryujinx/Configuration/ConfigurationState.cs +++ b/Ryujinx/Configuration/ConfigurationState.cs @@ -268,6 +268,11 @@ namespace Ryujinx.Configuration /// Enable or disable keyboard support (Independent from controllers binding) /// public ReactiveObject EnableKeyboard { get; private set; } + + /// + /// Enable or disable mouse support (Independent from controllers binding) + /// + public ReactiveObject EnableMouse { get; private set; } /// /// Hotkey Keyboard Bindings @@ -284,6 +289,7 @@ namespace Ryujinx.Configuration public HidSection() { EnableKeyboard = new ReactiveObject(); + EnableMouse = new ReactiveObject(); Hotkeys = new ReactiveObject(); InputConfig = new ReactiveObject>(); } @@ -471,6 +477,7 @@ namespace Ryujinx.Configuration CustomThemePath = Ui.CustomThemePath, StartFullscreen = Ui.StartFullscreen, EnableKeyboard = Hid.EnableKeyboard, + EnableMouse = Hid.EnableMouse, Hotkeys = Hid.Hotkeys, KeyboardConfig = new List(), ControllerConfig = new List(), @@ -532,6 +539,7 @@ namespace Ryujinx.Configuration Ui.CustomThemePath.Value = ""; Ui.StartFullscreen.Value = false; Hid.EnableKeyboard.Value = false; + Hid.EnableMouse.Value = false; Hid.Hotkeys.Value = new KeyboardHotkeys { ToggleVsync = Key.Tab @@ -828,6 +836,15 @@ namespace Ryujinx.Configuration configurationFileUpdated = true; } + if (configurationFileFormat.Version < 27) + { + Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 27."); + + configurationFileFormat.EnableMouse = false; + + configurationFileUpdated = true; + } + Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; Graphics.ResScale.Value = configurationFileFormat.ResScale; Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; @@ -878,6 +895,7 @@ namespace Ryujinx.Configuration Ui.CustomThemePath.Value = configurationFileFormat.CustomThemePath; Ui.StartFullscreen.Value = configurationFileFormat.StartFullscreen; Hid.EnableKeyboard.Value = configurationFileFormat.EnableKeyboard; + Hid.EnableMouse.Value = configurationFileFormat.EnableMouse; Hid.Hotkeys.Value = configurationFileFormat.Hotkeys; Hid.InputConfig.Value = configurationFileFormat.InputConfig; diff --git a/Ryujinx/Input/GTK3/GTK3Mouse.cs b/Ryujinx/Input/GTK3/GTK3Mouse.cs index eb0c8c9a..836c2bf4 100644 --- a/Ryujinx/Input/GTK3/GTK3Mouse.cs +++ b/Ryujinx/Input/GTK3/GTK3Mouse.cs @@ -31,6 +31,11 @@ namespace Ryujinx.Input.GTK3 return _driver.CurrentPosition; } + public Vector2 GetScroll() + { + return _driver.Scroll; + } + public GamepadStateSnapshot GetMappedStateSnapshot() { throw new NotImplementedException(); diff --git a/Ryujinx/Input/GTK3/GTK3MouseDriver.cs b/Ryujinx/Input/GTK3/GTK3MouseDriver.cs index 015f5817..df37e4f4 100644 --- a/Ryujinx/Input/GTK3/GTK3MouseDriver.cs +++ b/Ryujinx/Input/GTK3/GTK3MouseDriver.cs @@ -14,18 +14,27 @@ namespace Ryujinx.Input.GTK3 public bool[] PressedButtons { get; } public Vector2 CurrentPosition { get; private set; } + public Vector2 Scroll{ get; private set; } public GTK3MouseDriver(Widget parent) { _widget = parent; - _widget.MotionNotifyEvent += Parent_MotionNotifyEvent; - _widget.ButtonPressEvent += Parent_ButtonPressEvent; + _widget.MotionNotifyEvent += Parent_MotionNotifyEvent; + _widget.ButtonPressEvent += Parent_ButtonPressEvent; _widget.ButtonReleaseEvent += Parent_ButtonReleaseEvent; + _widget.ScrollEvent += Parent_ScrollEvent; PressedButtons = new bool[(int)MouseButton.Count]; } + + [GLib.ConnectBefore] + private void Parent_ScrollEvent(object o, ScrollEventArgs args) + { + Scroll = new Vector2((float)args.Event.X, (float)args.Event.Y); + } + [GLib.ConnectBefore] private void Parent_ButtonReleaseEvent(object o, ButtonReleaseEventArgs args) { diff --git a/Ryujinx/Ui/RendererWidgetBase.cs b/Ryujinx/Ui/RendererWidgetBase.cs index 289b2dbf..699f06c5 100644 --- a/Ryujinx/Ui/RendererWidgetBase.cs +++ b/Ryujinx/Ui/RendererWidgetBase.cs @@ -63,6 +63,7 @@ namespace Ryujinx.Ui private int _windowHeight; private int _windowWidth; + private bool _isMouseInClient; public RendererWidgetBase(InputManager inputManager, GraphicsDebugLevel glLogLevel) { @@ -87,6 +88,9 @@ namespace Ryujinx.Ui AddEvents((int)(EventMask.ButtonPressMask | EventMask.ButtonReleaseMask | EventMask.PointerMotionMask + | EventMask.ScrollMask + | EventMask.EnterNotifyMask + | EventMask.LeaveNotifyMask | EventMask.KeyPressMask | EventMask.KeyReleaseMask)); @@ -125,6 +129,8 @@ namespace Ryujinx.Ui { ConfigurationState.Instance.HideCursorOnIdle.Event -= HideCursorStateChanged; + Window.Cursor = null; + NpadManager.Dispose(); Dispose(); } @@ -136,9 +142,34 @@ namespace Ryujinx.Ui _lastCursorMoveTime = Stopwatch.GetTimestamp(); } + if(ConfigurationState.Instance.Hid.EnableMouse) + { + Window.Cursor = _invisibleCursor; + } + + _isMouseInClient = true; + return false; } + protected override bool OnEnterNotifyEvent(EventCrossing evnt) + { + Window.Cursor = ConfigurationState.Instance.Hid.EnableMouse ? _invisibleCursor : null; + + _isMouseInClient = true; + + return base.OnEnterNotifyEvent(evnt); + } + + protected override bool OnLeaveNotifyEvent(EventCrossing evnt) + { + Window.Cursor = null; + + _isMouseInClient = false; + + return base.OnLeaveNotifyEvent(evnt); + } + protected override void OnGetPreferredHeight(out int minimumHeight, out int naturalHeight) { Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window); @@ -241,11 +272,16 @@ namespace Ryujinx.Ui _toggleDockedMode = toggleDockedMode; - if (_hideCursorOnIdle) + if (_hideCursorOnIdle && !ConfigurationState.Instance.Hid.EnableMouse) { long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime; Window.Cursor = (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency) ? _invisibleCursor : null; } + + if(ConfigurationState.Instance.Hid.EnableMouse && _isMouseInClient) + { + Window.Cursor = _invisibleCursor; + } } public void Initialize(Switch device) @@ -254,7 +290,7 @@ namespace Ryujinx.Ui Renderer = Device.Gpu.Renderer; Renderer?.Window.SetSize(_windowWidth, _windowHeight); - NpadManager.Initialize(device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard); + NpadManager.Initialize(device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse); TouchScreenManager.Initialize(device); } @@ -442,7 +478,7 @@ namespace Ryujinx.Ui }); } - NpadManager.Update(); + NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat()); if ((Toplevel as MainWindow).IsFocused) { @@ -461,7 +497,7 @@ namespace Ryujinx.Ui bool hasTouch = false; // Get screen touch position - if ((Toplevel as MainWindow).IsFocused) + if ((Toplevel as MainWindow).IsFocused && !ConfigurationState.Instance.Hid.EnableMouse) { hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as GTK3MouseDriver).IsButtonPressed(MouseButton.Button1), ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat()); } diff --git a/Ryujinx/Ui/Windows/ControllerWindow.cs b/Ryujinx/Ui/Windows/ControllerWindow.cs index 7a49c7ba..655f1afb 100644 --- a/Ryujinx/Ui/Windows/ControllerWindow.cs +++ b/Ryujinx/Ui/Windows/ControllerWindow.cs @@ -1150,7 +1150,7 @@ namespace Ryujinx.Ui.Windows if (_mainWindow.RendererWidget != null) { - _mainWindow.RendererWidget.NpadManager.ReloadConfiguration(newConfig, ConfigurationState.Instance.Hid.EnableKeyboard); + _mainWindow.RendererWidget.NpadManager.ReloadConfiguration(newConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse); } // Atomically replace and signal input change. diff --git a/Ryujinx/Ui/Windows/SettingsWindow.cs b/Ryujinx/Ui/Windows/SettingsWindow.cs index f4e11fde..e7e89640 100644 --- a/Ryujinx/Ui/Windows/SettingsWindow.cs +++ b/Ryujinx/Ui/Windows/SettingsWindow.cs @@ -56,6 +56,7 @@ namespace Ryujinx.Ui.Windows [GUI] CheckButton _expandRamToggle; [GUI] CheckButton _ignoreToggle; [GUI] CheckButton _directKeyboardAccess; + [GUI] CheckButton _directMouseAccess; [GUI] ComboBoxText _systemLanguageSelect; [GUI] ComboBoxText _systemRegionSelect; [GUI] Entry _systemTimeZoneEntry; @@ -245,6 +246,11 @@ namespace Ryujinx.Ui.Windows _directKeyboardAccess.Click(); } + if (ConfigurationState.Instance.Hid.EnableMouse) + { + _directMouseAccess.Click(); + } + if (ConfigurationState.Instance.Ui.EnableCustomTheme) { _custThemeToggle.Click(); @@ -461,6 +467,7 @@ namespace Ryujinx.Ui.Windows ConfigurationState.Instance.System.ExpandRam.Value = _expandRamToggle.Active; ConfigurationState.Instance.System.IgnoreMissingServices.Value = _ignoreToggle.Active; ConfigurationState.Instance.Hid.EnableKeyboard.Value = _directKeyboardAccess.Active; + ConfigurationState.Instance.Hid.EnableMouse.Value = _directMouseAccess.Active; ConfigurationState.Instance.Ui.EnableCustomTheme.Value = _custThemeToggle.Active; ConfigurationState.Instance.System.Language.Value = Enum.Parse(_systemLanguageSelect.ActiveId); ConfigurationState.Instance.System.Region.Value = Enum.Parse(_systemRegionSelect.ActiveId); diff --git a/Ryujinx/Ui/Windows/SettingsWindow.glade b/Ryujinx/Ui/Windows/SettingsWindow.glade index a50f202e..e96ad64a 100644 --- a/Ryujinx/Ui/Windows/SettingsWindow.glade +++ b/Ryujinx/Ui/Windows/SettingsWindow.glade @@ -1,5 +1,5 @@ - + @@ -520,6 +520,22 @@ 1 + + + Direct Mouse Access + True + True + False + Enable or disable "direct keyboard access (HID) support" (Provides games access to your keyboard as a text entry device) + True + + + False + False + 10 + 2 + + False diff --git a/Ryujinx/_schema.json b/Ryujinx/_schema.json index 242874d5..f819b9d6 100644 --- a/Ryujinx/_schema.json +++ b/Ryujinx/_schema.json @@ -22,6 +22,7 @@ "enable_fs_integrity_checks", "fs_global_access_log_mode", "enable_keyboard", + "enable_mouse", "keyboard_config", "controller_config" ], @@ -1438,6 +1439,17 @@ false ] }, + "enable_mouse": { + "$id": "#/properties/enable_mouse", + "type": "boolean", + "title": "(HID) Mouse Enable", + "description": "Enable or disable direct mouse access (HID) support (Provides games access to your mouse as a pointing device)", + "default": false, + "examples": [ + true, + false + ] + }, "hotkeys": { "$id": "#/properties/hotkeys", "type": "object",