Ability to assign hotkeys to cycle controllers for players (WIP)

This commit is contained in:
Barış Hamil 2024-08-17 23:04:01 +03:00
parent 0137c9e635
commit 6a555ee9f7
9 changed files with 313 additions and 166 deletions

View file

@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace Ryujinx.Common.Configuration.Hid
{
public class KeyboardHotkeys
@ -11,5 +13,6 @@ namespace Ryujinx.Common.Configuration.Hid
public Key ResScaleDown { get; set; }
public Key VolumeUp { get; set; }
public Key VolumeDown { get; set; }
public List<Key> CycleControllers { get; set; }
}
}

View file

@ -32,6 +32,7 @@ using Ryujinx.HLE;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.Input;
using Ryujinx.Input.HLE;
@ -46,6 +47,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@ -1200,6 +1202,24 @@ namespace Ryujinx.Ava
_viewModel.Volume = Device.GetVolume();
break;
case KeyboardHotkeyState.CycleControllersPlayer1:
case KeyboardHotkeyState.CycleControllersPlayer2:
case KeyboardHotkeyState.CycleControllersPlayer3:
case KeyboardHotkeyState.CycleControllersPlayer4:
case KeyboardHotkeyState.CycleControllersPlayer5:
case KeyboardHotkeyState.CycleControllersPlayer6:
case KeyboardHotkeyState.CycleControllersPlayer7:
case KeyboardHotkeyState.CycleControllersPlayer8:
Dispatcher.UIThread.Invoke(() => {
var player = currentHotkeyState - KeyboardHotkeyState.CycleControllersPlayer1;
var ivm = new UI.ViewModels.Input.InputViewModel();
ivm.LoadDevices();
ivm.PlayerId = (Ryujinx.Common.Configuration.Hid.PlayerIndex) player;
ivm.Device = (ivm.Device + 1) % ivm.Devices.Count;
ivm.Save();
Console.WriteLine($"Cycling controller for player {ivm.PlayerId} to {ivm.Devices[ivm.Device].Name}");
});
break;
case KeyboardHotkeyState.None:
(_keyboardInterface as AvaloniaKeyboard).Clear();
break;
@ -1274,6 +1294,15 @@ namespace Ryujinx.Ava
state = KeyboardHotkeyState.VolumeDown;
}
foreach (var cycle in ConfigurationState.Instance.Hid.Hotkeys.Value.CycleControllers.Select((value, index) => (value, index)))
{
if (_keyboardInterface.IsPressed((Key)cycle.value))
{
state = KeyboardHotkeyState.CycleControllersPlayer1 + cycle.index;
break;
}
}
return state;
}
}

View file

@ -738,6 +738,7 @@
"RyujinxUpdaterMessage": "Do you want to update Ryujinx to the latest version?",
"SettingsTabHotkeysVolumeUpHotkey": "Increase Volume:",
"SettingsTabHotkeysVolumeDownHotkey": "Decrease Volume:",
"SettingsTabHotkeysCycleControllers": "Cycle Controllers",
"SettingsEnableMacroHLE": "Enable Macro HLE",
"SettingsEnableMacroHLETooltip": "High-level emulation of GPU Macro code.\n\nImproves performance, but may cause graphical glitches in some games.\n\nLeave ON if unsure.",
"SettingsEnableColorSpacePassthrough": "Color Space Passthrough",

View file

@ -12,5 +12,13 @@ namespace Ryujinx.Ava.Common
ResScaleDown,
VolumeUp,
VolumeDown,
CycleControllersPlayer1,
CycleControllersPlayer2,
CycleControllersPlayer3,
CycleControllersPlayer4,
CycleControllersPlayer5,
CycleControllersPlayer6,
CycleControllersPlayer7,
CycleControllersPlayer8
}
}

View file

@ -1,5 +1,10 @@
using DynamicData;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Configuration.Hid;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;
namespace Ryujinx.Ava.UI.Models.Input
{
@ -104,8 +109,14 @@ namespace Ryujinx.Ava.UI.Models.Input
}
}
public ObservableCollection<CycleController> CycleControllers { get; set; } = new ObservableCollection<CycleController>();
public ICommand AddCycleController { get; set; }
public ICommand RemoveCycleController { get; set; }
public bool CanRemoveCycleController => CycleControllers.Count > 0 && CycleControllers.Count < 8;
public HotkeyConfig(KeyboardHotkeys config)
{
AddCycleController = MiniCommand.Create(() => CycleControllers.Add(new CycleController(CycleControllers.Count + 1, Key.Unbound)));
RemoveCycleController = MiniCommand.Create(() => CycleControllers.Remove(CycleControllers.Last()));
if (config != null)
{
ToggleVsync = config.ToggleVsync;
@ -117,7 +128,9 @@ namespace Ryujinx.Ava.UI.Models.Input
ResScaleDown = config.ResScaleDown;
VolumeUp = config.VolumeUp;
VolumeDown = config.VolumeDown;
CycleControllers.AddRange((config.CycleControllers ?? []).Select((x, i) => new CycleController(i + 1, x)));
}
CycleControllers.CollectionChanged += (sender, e) => OnPropertyChanged(nameof(CanRemoveCycleController));
}
public KeyboardHotkeys GetConfig()
@ -133,6 +146,7 @@ namespace Ryujinx.Ava.UI.Models.Input
ResScaleDown = ResScaleDown,
VolumeUp = VolumeUp,
VolumeDown = VolumeDown,
CycleControllers = CycleControllers.Select(x => x.Hotkey).ToList()
};
return config;

View file

@ -0,0 +1,36 @@
using Ryujinx.Common.Configuration.Hid;
namespace Ryujinx.Ava.UI.ViewModels
{
public class CycleController : BaseModel
{
private string _player;
private Key _hotkey;
public string Player
{
get => _player;
set
{
_player = value;
OnPropertyChanged(nameof(Player));
}
}
public Key Hotkey
{
get => _hotkey;
set
{
_hotkey = value;
OnPropertyChanged(nameof(Hotkey));
}
}
public CycleController(int v, Key x)
{
Player = $"Player {v}";
Hotkey = x;
}
}
}

View file

@ -256,6 +256,10 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public InputViewModel()
{
_mainWindow =
(MainWindow)((IClassicDesktopStyleApplicationLifetime)Application.Current
.ApplicationLifetime).MainWindow;
AvaloniaKeyboardDriver = new AvaloniaKeyboardDriver(_mainWindow);
PlayerIndexes = new ObservableCollection<PlayerModel>();
Controllers = new ObservableCollection<ControllerModel>();
Devices = new ObservableCollection<(DeviceType Type, string Id, string Name)>();
@ -740,38 +744,34 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
return;
}
else
bool validFileName = ProfileName.IndexOfAny(Path.GetInvalidFileNameChars()) == -1;
if (!validFileName)
{
bool validFileName = ProfileName.IndexOfAny(Path.GetInvalidFileNameChars()) == -1;
if (validFileName)
{
string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json");
InputConfig config = null;
if (IsKeyboard)
{
config = (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig();
}
else if (IsController)
{
config = (ConfigViewModel as ControllerInputViewModel).Config.GetConfig();
}
config.ControllerType = Controllers[_controller].Type;
string jsonString = JsonHelper.Serialize(config, _serializerContext.InputConfig);
await File.WriteAllTextAsync(path, jsonString);
LoadProfiles();
}
else
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileInvalidProfileNameErrorMessage]);
}
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileInvalidProfileNameErrorMessage]);
return;
}
string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json");
InputConfig config = null;
if (IsKeyboard)
{
config = (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig();
}
else if (IsController)
{
config = (ConfigViewModel as ControllerInputViewModel).Config.GetConfig();
}
config.ControllerType = Controllers[_controller].Type;
string jsonString = JsonHelper.Serialize(config, _serializerContext.InputConfig);
await File.WriteAllTextAsync(path, jsonString);
LoadProfiles();
}
public async void RemoveProfile()

View file

@ -18,15 +18,15 @@
<helpers:KeyValueConverter x:Key="Key" />
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="StackPanel > StackPanel">
<Style Selector="StackPanel StackPanel">
<Setter Property="Margin" Value="10, 0, 0, 0" />
<Setter Property="Orientation" Value="Horizontal" />
</Style>
<Style Selector="StackPanel > StackPanel > TextBlock">
<Style Selector="StackPanel StackPanel > TextBlock">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Width" Value="230" />
</Style>
<Style Selector="ToggleButton">
<Style Selector="ToggleButton, Button">
<Setter Property="Width" Value="90" />
<Setter Property="Height" Value="27" />
</Style>
@ -42,67 +42,110 @@
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel
Name="SettingButtons"
Margin="10"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
Spacing="10"
Name="SettingButtons">
<TextBlock
Classes="h1"
Text="{locale:Locale SettingsTabHotkeysHotkeys}" />
<StackPanel>
<TextBlock Text="{locale:Locale SettingsTabHotkeysToggleVsyncHotkey}" />
<ToggleButton Name="ToggleVsync">
<TextBlock Text="{Binding KeyboardHotkey.ToggleVsync, Converter={StaticResource Key}}" />
</ToggleButton>
<StackPanel
Margin="10,0,0,0"
Spacing="10"
Orientation="Vertical">
<StackPanel>
<TextBlock Text="{locale:Locale SettingsTabHotkeysToggleVsyncHotkey}" />
<ToggleButton Name="ToggleVsync">
<TextBlock Text="{Binding KeyboardHotkey.ToggleVsync, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{locale:Locale SettingsTabHotkeysScreenshotHotkey}" />
<ToggleButton Name="Screenshot">
<TextBlock Text="{Binding KeyboardHotkey.Screenshot, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{locale:Locale SettingsTabHotkeysShowUiHotkey}" />
<ToggleButton Name="ShowUI">
<TextBlock Text="{Binding KeyboardHotkey.ShowUI, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{locale:Locale SettingsTabHotkeysPauseHotkey}" />
<ToggleButton Name="Pause">
<TextBlock Text="{Binding KeyboardHotkey.Pause, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{locale:Locale SettingsTabHotkeysToggleMuteHotkey}" />
<ToggleButton Name="ToggleMute">
<TextBlock Text="{Binding KeyboardHotkey.ToggleMute, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{locale:Locale SettingsTabHotkeysResScaleUpHotkey}" />
<ToggleButton Name="ResScaleUp">
<TextBlock Text="{Binding KeyboardHotkey.ResScaleUp, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{locale:Locale SettingsTabHotkeysResScaleDownHotkey}" />
<ToggleButton Name="ResScaleDown">
<TextBlock Text="{Binding KeyboardHotkey.ResScaleDown, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{locale:Locale SettingsTabHotkeysVolumeUpHotkey}" />
<ToggleButton Name="VolumeUp">
<TextBlock Text="{Binding KeyboardHotkey.VolumeUp, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{locale:Locale SettingsTabHotkeysVolumeDownHotkey}" />
<ToggleButton Name="VolumeDown">
<TextBlock Text="{Binding KeyboardHotkey.VolumeDown, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
</StackPanel>
<StackPanel>
<TextBlock Text="{locale:Locale SettingsTabHotkeysScreenshotHotkey}" />
<ToggleButton Name="Screenshot">
<TextBlock Text="{Binding KeyboardHotkey.Screenshot, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{locale:Locale SettingsTabHotkeysShowUiHotkey}" />
<ToggleButton Name="ShowUI">
<TextBlock Text="{Binding KeyboardHotkey.ShowUI, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{locale:Locale SettingsTabHotkeysPauseHotkey}" />
<ToggleButton Name="Pause">
<TextBlock Text="{Binding KeyboardHotkey.Pause, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{locale:Locale SettingsTabHotkeysToggleMuteHotkey}" />
<ToggleButton Name="ToggleMute">
<TextBlock Text="{Binding KeyboardHotkey.ToggleMute, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{locale:Locale SettingsTabHotkeysResScaleUpHotkey}" />
<ToggleButton Name="ResScaleUp">
<TextBlock Text="{Binding KeyboardHotkey.ResScaleUp, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{locale:Locale SettingsTabHotkeysResScaleDownHotkey}" />
<ToggleButton Name="ResScaleDown">
<TextBlock Text="{Binding KeyboardHotkey.ResScaleDown, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{locale:Locale SettingsTabHotkeysVolumeUpHotkey}" />
<ToggleButton Name="VolumeUp">
<TextBlock Text="{Binding KeyboardHotkey.VolumeUp, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{locale:Locale SettingsTabHotkeysVolumeDownHotkey}" />
<ToggleButton Name="VolumeDown">
<TextBlock Text="{Binding KeyboardHotkey.VolumeDown, Converter={StaticResource Key}}" />
</ToggleButton>
<Separator Height="1" />
<StackPanel Margin="0">
<TextBlock
Classes="h1"
Text="{locale:Locale SettingsTabHotkeysCycleControllers}" />
<Button
Margin="10,0,0,0"
Content="+"
Command="{Binding KeyboardHotkey.AddCycleController}" />
<Button
Margin="10,0,0,0"
Content="-"
IsEnabled="{Binding KeyboardHotkey.CanRemoveCycleController}"
Command="{Binding KeyboardHotkey.RemoveCycleController}" />
</StackPanel>
<ItemsControl ItemsSource="{Binding KeyboardHotkey.CycleControllers}"
Name="CycleControllers">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel
Margin="0"
Orientation="Vertical"
Spacing="10" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Player}" />
<ToggleButton>
<TextBlock
Text="{Binding Hotkey, Converter={StaticResource Key}}" />
</ToggleButton>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</ScrollViewer>

View file

@ -3,11 +3,17 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.VisualTree;
using DynamicData.Kernel;
using FluentAvalonia.Core;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Input;
using Ryujinx.Input.Assigner;
using System;
using System.Collections.Specialized;
using System.Linq;
using Key = Ryujinx.Common.Configuration.Hid.Key;
namespace Ryujinx.Ava.UI.Views.Settings
@ -20,16 +26,21 @@ namespace Ryujinx.Ava.UI.Views.Settings
public SettingsHotkeysView()
{
InitializeComponent();
RegisterEvents();
_avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this);
CycleControllers.LayoutUpdated += (_, _1) => RegisterEvents();
}
private void RegisterEvents()
{
foreach (ILogical visual in SettingButtons.GetLogicalDescendants())
{
if (visual is ToggleButton button and not CheckBox)
{
button.IsCheckedChanged -= Button_IsCheckedChanged;
button.IsCheckedChanged += Button_IsCheckedChanged;
}
}
_avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
@ -50,87 +61,89 @@ namespace Ryujinx.Ava.UI.Views.Settings
PointerPressed -= MouseClick;
}
private void Button_IsCheckedChanged(object sender, RoutedEventArgs e)
{
if (sender is ToggleButton button)
if (sender is not ToggleButton button)
{
if ((bool)button.IsChecked)
{
if (_currentAssigner != null && button == _currentAssigner.ToggledButton)
{
return;
}
if (_currentAssigner == null)
{
_currentAssigner = new ButtonKeyAssigner(button);
this.Focus(NavigationMethod.Pointer);
PointerPressed += MouseClick;
var keyboard = (IKeyboard)_avaloniaKeyboardDriver.GetGamepad("0");
IButtonAssigner assigner = new KeyboardKeyAssigner(keyboard);
_currentAssigner.ButtonAssigned += (sender, e) =>
{
if (e.ButtonValue.HasValue)
{
var viewModel = (DataContext) as SettingsViewModel;
var buttonValue = e.ButtonValue.Value;
switch (button.Name)
{
case "ToggleVsync":
viewModel.KeyboardHotkey.ToggleVsync = buttonValue.AsHidType<Key>();
break;
case "Screenshot":
viewModel.KeyboardHotkey.Screenshot = buttonValue.AsHidType<Key>();
break;
case "ShowUI":
viewModel.KeyboardHotkey.ShowUI = buttonValue.AsHidType<Key>();
break;
case "Pause":
viewModel.KeyboardHotkey.Pause = buttonValue.AsHidType<Key>();
break;
case "ToggleMute":
viewModel.KeyboardHotkey.ToggleMute = buttonValue.AsHidType<Key>();
break;
case "ResScaleUp":
viewModel.KeyboardHotkey.ResScaleUp = buttonValue.AsHidType<Key>();
break;
case "ResScaleDown":
viewModel.KeyboardHotkey.ResScaleDown = buttonValue.AsHidType<Key>();
break;
case "VolumeUp":
viewModel.KeyboardHotkey.VolumeUp = buttonValue.AsHidType<Key>();
break;
case "VolumeDown":
viewModel.KeyboardHotkey.VolumeDown = buttonValue.AsHidType<Key>();
break;
}
}
};
_currentAssigner.GetInputAndAssign(assigner, keyboard);
}
else
{
if (_currentAssigner != null)
{
_currentAssigner.Cancel();
_currentAssigner = null;
button.IsChecked = false;
}
}
}
else
{
_currentAssigner?.Cancel();
_currentAssigner = null;
}
return;
}
if (!button.IsChecked.ValueOr(default))
{
_currentAssigner?.Cancel();
_currentAssigner = null;
return;
}
if (_currentAssigner != null && button == _currentAssigner.ToggledButton)
{
return;
}
if (_currentAssigner != null)
{
_currentAssigner.Cancel();
_currentAssigner = null;
button.IsChecked = false;
return;
}
_currentAssigner = new ButtonKeyAssigner(button);
this.Focus(NavigationMethod.Pointer);
PointerPressed += MouseClick;
var keyboard = (IKeyboard)_avaloniaKeyboardDriver.GetGamepad("0");
IButtonAssigner assigner = new KeyboardKeyAssigner(keyboard);
_currentAssigner.ButtonAssigned += (sender, e) =>
{
if (!e.ButtonValue.HasValue)
{
return;
}
var viewModel = (DataContext) as SettingsViewModel;
var buttonValue = e.ButtonValue.Value;
switch (button.Name)
{
case "ToggleVsync":
viewModel.KeyboardHotkey.ToggleVsync = buttonValue.AsHidType<Key>();
break;
case "Screenshot":
viewModel.KeyboardHotkey.Screenshot = buttonValue.AsHidType<Key>();
break;
case "ShowUI":
viewModel.KeyboardHotkey.ShowUI = buttonValue.AsHidType<Key>();
break;
case "Pause":
viewModel.KeyboardHotkey.Pause = buttonValue.AsHidType<Key>();
break;
case "ToggleMute":
viewModel.KeyboardHotkey.ToggleMute = buttonValue.AsHidType<Key>();
break;
case "ResScaleUp":
viewModel.KeyboardHotkey.ResScaleUp = buttonValue.AsHidType<Key>();
break;
case "ResScaleDown":
viewModel.KeyboardHotkey.ResScaleDown = buttonValue.AsHidType<Key>();
break;
case "VolumeUp":
viewModel.KeyboardHotkey.VolumeUp = buttonValue.AsHidType<Key>();
break;
case "VolumeDown":
viewModel.KeyboardHotkey.VolumeDown = buttonValue.AsHidType<Key>();
break;
default:
var index = button.FindAncestorOfType<ItemsControl>().GetLogicalDescendants().OfType<ToggleButton>().IndexOf(button);
viewModel.KeyboardHotkey.CycleControllers[index].Hotkey = buttonValue.AsHidType<Key>();
break;
}
};
_currentAssigner.GetInputAndAssign(assigner, keyboard);
}
public void Dispose()