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 namespace Ryujinx.Common.Configuration.Hid
{ {
public class KeyboardHotkeys public class KeyboardHotkeys
@ -11,5 +13,6 @@ namespace Ryujinx.Common.Configuration.Hid
public Key ResScaleDown { get; set; } public Key ResScaleDown { get; set; }
public Key VolumeUp { get; set; } public Key VolumeUp { get; set; }
public Key VolumeDown { 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.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.Input; using Ryujinx.Input;
using Ryujinx.Input.HLE; using Ryujinx.Input.HLE;
@ -46,6 +47,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -1200,6 +1202,24 @@ namespace Ryujinx.Ava
_viewModel.Volume = Device.GetVolume(); _viewModel.Volume = Device.GetVolume();
break; 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: case KeyboardHotkeyState.None:
(_keyboardInterface as AvaloniaKeyboard).Clear(); (_keyboardInterface as AvaloniaKeyboard).Clear();
break; break;
@ -1274,6 +1294,15 @@ namespace Ryujinx.Ava
state = KeyboardHotkeyState.VolumeDown; 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; return state;
} }
} }

View file

@ -738,6 +738,7 @@
"RyujinxUpdaterMessage": "Do you want to update Ryujinx to the latest version?", "RyujinxUpdaterMessage": "Do you want to update Ryujinx to the latest version?",
"SettingsTabHotkeysVolumeUpHotkey": "Increase Volume:", "SettingsTabHotkeysVolumeUpHotkey": "Increase Volume:",
"SettingsTabHotkeysVolumeDownHotkey": "Decrease Volume:", "SettingsTabHotkeysVolumeDownHotkey": "Decrease Volume:",
"SettingsTabHotkeysCycleControllers": "Cycle Controllers",
"SettingsEnableMacroHLE": "Enable Macro HLE", "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.", "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", "SettingsEnableColorSpacePassthrough": "Color Space Passthrough",

View file

@ -12,5 +12,13 @@ namespace Ryujinx.Ava.Common
ResScaleDown, ResScaleDown,
VolumeUp, VolumeUp,
VolumeDown, 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.Ava.UI.ViewModels;
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;
namespace Ryujinx.Ava.UI.Models.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) 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) if (config != null)
{ {
ToggleVsync = config.ToggleVsync; ToggleVsync = config.ToggleVsync;
@ -117,7 +128,9 @@ namespace Ryujinx.Ava.UI.Models.Input
ResScaleDown = config.ResScaleDown; ResScaleDown = config.ResScaleDown;
VolumeUp = config.VolumeUp; VolumeUp = config.VolumeUp;
VolumeDown = config.VolumeDown; 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() public KeyboardHotkeys GetConfig()
@ -133,6 +146,7 @@ namespace Ryujinx.Ava.UI.Models.Input
ResScaleDown = ResScaleDown, ResScaleDown = ResScaleDown,
VolumeUp = VolumeUp, VolumeUp = VolumeUp,
VolumeDown = VolumeDown, VolumeDown = VolumeDown,
CycleControllers = CycleControllers.Select(x => x.Hotkey).ToList()
}; };
return config; 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() public InputViewModel()
{ {
_mainWindow =
(MainWindow)((IClassicDesktopStyleApplicationLifetime)Application.Current
.ApplicationLifetime).MainWindow;
AvaloniaKeyboardDriver = new AvaloniaKeyboardDriver(_mainWindow);
PlayerIndexes = new ObservableCollection<PlayerModel>(); PlayerIndexes = new ObservableCollection<PlayerModel>();
Controllers = new ObservableCollection<ControllerModel>(); Controllers = new ObservableCollection<ControllerModel>();
Devices = new ObservableCollection<(DeviceType Type, string Id, string Name)>(); Devices = new ObservableCollection<(DeviceType Type, string Id, string Name)>();
@ -740,12 +744,14 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
return; return;
} }
else
{
bool validFileName = ProfileName.IndexOfAny(Path.GetInvalidFileNameChars()) == -1; bool validFileName = ProfileName.IndexOfAny(Path.GetInvalidFileNameChars()) == -1;
if (validFileName) if (!validFileName)
{ {
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileInvalidProfileNameErrorMessage]);
return;
}
string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json"); string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json");
InputConfig config = null; InputConfig config = null;
@ -767,12 +773,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
LoadProfiles(); LoadProfiles();
} }
else
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileInvalidProfileNameErrorMessage]);
}
}
}
public async void RemoveProfile() public async void RemoveProfile()
{ {

View file

@ -18,15 +18,15 @@
<helpers:KeyValueConverter x:Key="Key" /> <helpers:KeyValueConverter x:Key="Key" />
</UserControl.Resources> </UserControl.Resources>
<UserControl.Styles> <UserControl.Styles>
<Style Selector="StackPanel > StackPanel"> <Style Selector="StackPanel StackPanel">
<Setter Property="Margin" Value="10, 0, 0, 0" /> <Setter Property="Margin" Value="10, 0, 0, 0" />
<Setter Property="Orientation" Value="Horizontal" /> <Setter Property="Orientation" Value="Horizontal" />
</Style> </Style>
<Style Selector="StackPanel > StackPanel > TextBlock"> <Style Selector="StackPanel StackPanel > TextBlock">
<Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Width" Value="230" /> <Setter Property="Width" Value="230" />
</Style> </Style>
<Style Selector="ToggleButton"> <Style Selector="ToggleButton, Button">
<Setter Property="Width" Value="90" /> <Setter Property="Width" Value="90" />
<Setter Property="Height" Value="27" /> <Setter Property="Height" Value="27" />
</Style> </Style>
@ -42,13 +42,18 @@
VerticalScrollBarVisibility="Auto"> VerticalScrollBarVisibility="Auto">
<Border Classes="settings"> <Border Classes="settings">
<StackPanel <StackPanel
Name="SettingButtons"
Margin="10" Margin="10"
HorizontalAlignment="Stretch"
Orientation="Vertical" Orientation="Vertical"
Spacing="10"> Spacing="10"
Name="SettingButtons">
<TextBlock <TextBlock
Classes="h1" Classes="h1"
Text="{locale:Locale SettingsTabHotkeysHotkeys}" /> Text="{locale:Locale SettingsTabHotkeysHotkeys}" />
<StackPanel
Margin="10,0,0,0"
Spacing="10"
Orientation="Vertical">
<StackPanel> <StackPanel>
<TextBlock Text="{locale:Locale SettingsTabHotkeysToggleVsyncHotkey}" /> <TextBlock Text="{locale:Locale SettingsTabHotkeysToggleVsyncHotkey}" />
<ToggleButton Name="ToggleVsync"> <ToggleButton Name="ToggleVsync">
@ -104,6 +109,44 @@
</ToggleButton> </ToggleButton>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<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> </Border>
</ScrollViewer> </ScrollViewer>
</UserControl> </UserControl>

View file

@ -3,11 +3,17 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.VisualTree;
using DynamicData.Kernel;
using FluentAvalonia.Core;
using Ryujinx.Ava.Input; using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Input; using Ryujinx.Input;
using Ryujinx.Input.Assigner; using Ryujinx.Input.Assigner;
using System;
using System.Collections.Specialized;
using System.Linq;
using Key = Ryujinx.Common.Configuration.Hid.Key; using Key = Ryujinx.Common.Configuration.Hid.Key;
namespace Ryujinx.Ava.UI.Views.Settings namespace Ryujinx.Ava.UI.Views.Settings
@ -20,16 +26,21 @@ namespace Ryujinx.Ava.UI.Views.Settings
public SettingsHotkeysView() public SettingsHotkeysView()
{ {
InitializeComponent(); InitializeComponent();
RegisterEvents();
_avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this);
CycleControllers.LayoutUpdated += (_, _1) => RegisterEvents();
}
private void RegisterEvents()
{
foreach (ILogical visual in SettingButtons.GetLogicalDescendants()) foreach (ILogical visual in SettingButtons.GetLogicalDescendants())
{ {
if (visual is ToggleButton button and not CheckBox) if (visual is ToggleButton button and not CheckBox)
{ {
button.IsCheckedChanged -= Button_IsCheckedChanged;
button.IsCheckedChanged += Button_IsCheckedChanged; button.IsCheckedChanged += Button_IsCheckedChanged;
} }
} }
_avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this);
} }
protected override void OnPointerReleased(PointerReleasedEventArgs e) protected override void OnPointerReleased(PointerReleasedEventArgs e)
@ -50,20 +61,33 @@ namespace Ryujinx.Ava.UI.Views.Settings
PointerPressed -= MouseClick; PointerPressed -= MouseClick;
} }
private void Button_IsCheckedChanged(object sender, RoutedEventArgs e) private void Button_IsCheckedChanged(object sender, RoutedEventArgs e)
{ {
if (sender is ToggleButton button) if (sender is not ToggleButton button)
{ {
if ((bool)button.IsChecked) return;
}
if (!button.IsChecked.ValueOr(default))
{ {
_currentAssigner?.Cancel();
_currentAssigner = null;
return;
}
if (_currentAssigner != null && button == _currentAssigner.ToggledButton) if (_currentAssigner != null && button == _currentAssigner.ToggledButton)
{ {
return; return;
} }
if (_currentAssigner == null) if (_currentAssigner != null)
{ {
_currentAssigner.Cancel();
_currentAssigner = null;
button.IsChecked = false;
return;
}
_currentAssigner = new ButtonKeyAssigner(button); _currentAssigner = new ButtonKeyAssigner(button);
this.Focus(NavigationMethod.Pointer); this.Focus(NavigationMethod.Pointer);
@ -75,8 +99,11 @@ namespace Ryujinx.Ava.UI.Views.Settings
_currentAssigner.ButtonAssigned += (sender, e) => _currentAssigner.ButtonAssigned += (sender, e) =>
{ {
if (e.ButtonValue.HasValue) if (!e.ButtonValue.HasValue)
{ {
return;
}
var viewModel = (DataContext) as SettingsViewModel; var viewModel = (DataContext) as SettingsViewModel;
var buttonValue = e.ButtonValue.Value; var buttonValue = e.ButtonValue.Value;
@ -109,29 +136,15 @@ namespace Ryujinx.Ava.UI.Views.Settings
case "VolumeDown": case "VolumeDown":
viewModel.KeyboardHotkey.VolumeDown = buttonValue.AsHidType<Key>(); viewModel.KeyboardHotkey.VolumeDown = buttonValue.AsHidType<Key>();
break; 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); _currentAssigner.GetInputAndAssign(assigner, keyboard);
} }
else
{
if (_currentAssigner != null)
{
_currentAssigner.Cancel();
_currentAssigner = null;
button.IsChecked = false;
}
}
}
else
{
_currentAssigner?.Cancel();
_currentAssigner = null;
}
}
}
public void Dispose() public void Dispose()
{ {