From cee712105850ac3385cd0091a923438167433f9f Mon Sep 17 00:00:00 2001 From: TSR Berry <20988865+TSRBerry@users.noreply.github.com> Date: Sat, 8 Apr 2023 01:22:00 +0200 Subject: Move solution and projects to src --- src/Ryujinx.Ava/UI/Applet/AvaHostUiHandler.cs | 197 ++ .../UI/Applet/AvaloniaDynamicTextInputHandler.cs | 164 ++ src/Ryujinx.Ava/UI/Applet/AvaloniaHostUiTheme.cs | 43 + src/Ryujinx.Ava/UI/Applet/ErrorAppletWindow.axaml | 52 + .../UI/Applet/ErrorAppletWindow.axaml.cs | 80 + src/Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml | 65 + .../UI/Applet/SwkbdAppletDialog.axaml.cs | 148 ++ src/Ryujinx.Ava/UI/Controls/GameGridView.axaml | 177 ++ src/Ryujinx.Ava/UI/Controls/GameGridView.axaml.cs | 57 + src/Ryujinx.Ava/UI/Controls/GameListView.axaml | 233 +++ src/Ryujinx.Ava/UI/Controls/GameListView.axaml.cs | 57 + .../UI/Controls/NavigationDialogHost.axaml | 17 + .../UI/Controls/NavigationDialogHost.axaml.cs | 218 +++ src/Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml | 42 + .../UI/Controls/UpdateWaitWindow.axaml.cs | 31 + .../UI/Helpers/ApplicationOpenedEventArgs.cs | 16 + .../UI/Helpers/BitmapArrayValueConverter.cs | 35 + src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs | 118 ++ src/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs | 407 +++++ src/Ryujinx.Ava/UI/Helpers/Glyph.cs | 9 + src/Ryujinx.Ava/UI/Helpers/GlyphValueConverter.cs | 49 + src/Ryujinx.Ava/UI/Helpers/HotKeyControl.cs | 52 + src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs | 46 + src/Ryujinx.Ava/UI/Helpers/LoggerAdapter.cs | 115 ++ src/Ryujinx.Ava/UI/Helpers/MiniCommand.cs | 71 + src/Ryujinx.Ava/UI/Helpers/NotificationHelper.cs | 65 + src/Ryujinx.Ava/UI/Helpers/OffscreenTextBox.cs | 40 + src/Ryujinx.Ava/UI/Helpers/UserErrorDialog.cs | 91 + src/Ryujinx.Ava/UI/Helpers/UserResult.cs | 12 + src/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs | 123 ++ src/Ryujinx.Ava/UI/Models/CheatModel.cs | 40 + src/Ryujinx.Ava/UI/Models/CheatsList.cs | 51 + src/Ryujinx.Ava/UI/Models/ControllerModel.cs | 6 + src/Ryujinx.Ava/UI/Models/DeviceType.cs | 9 + .../UI/Models/DownloadableContentModel.cs | 35 + .../UI/Models/Generic/LastPlayedSortComparer.cs | 33 + src/Ryujinx.Ava/UI/Models/InputConfiguration.cs | 456 +++++ src/Ryujinx.Ava/UI/Models/PlayerModel.cs | 6 + src/Ryujinx.Ava/UI/Models/ProfileImageModel.cs | 32 + src/Ryujinx.Ava/UI/Models/SaveModel.cs | 112 ++ .../UI/Models/StatusUpdatedEventArgs.cs | 28 + src/Ryujinx.Ava/UI/Models/TempProfile.cs | 61 + src/Ryujinx.Ava/UI/Models/TimeZone.cs | 16 + src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs | 19 + src/Ryujinx.Ava/UI/Models/UserProfile.cs | 103 ++ src/Ryujinx.Ava/UI/Renderer/EmbeddedWindow.cs | 288 +++ .../UI/Renderer/EmbeddedWindowOpenGL.cs | 87 + .../UI/Renderer/EmbeddedWindowVulkan.cs | 42 + .../UI/Renderer/OpenTKBindingsContext.cs | 20 + src/Ryujinx.Ava/UI/Renderer/RendererHost.axaml | 11 + src/Ryujinx.Ava/UI/Renderer/RendererHost.axaml.cs | 68 + src/Ryujinx.Ava/UI/Renderer/SPBOpenGLContext.cs | 47 + .../UI/ViewModels/AboutWindowViewModel.cs | 133 ++ .../UI/ViewModels/AmiiboWindowViewModel.cs | 467 +++++ .../UI/ViewModels/AvatarProfileViewModel.cs | 363 ++++ src/Ryujinx.Ava/UI/ViewModels/BaseModel.cs | 15 + .../UI/ViewModels/ControllerSettingsViewModel.cs | 899 +++++++++ .../DownloadableContentManagerViewModel.cs | 338 ++++ .../UI/ViewModels/MainWindowViewModel.cs | 1907 ++++++++++++++++++++ src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs | 585 ++++++ .../UI/ViewModels/TitleUpdateViewModel.cs | 252 +++ .../UserFirmwareAvatarSelectorViewModel.cs | 230 +++ .../UserProfileImageSelectorViewModel.cs | 18 + .../UI/ViewModels/UserProfileViewModel.cs | 25 + .../UI/ViewModels/UserSaveManagerViewModel.cs | 120 ++ .../UI/Views/Main/MainMenuBarView.axaml | 164 ++ .../UI/Views/Main/MainMenuBarView.axaml.cs | 236 +++ .../UI/Views/Main/MainStatusBarView.axaml | 232 +++ .../UI/Views/Main/MainStatusBarView.axaml.cs | 52 + .../UI/Views/Main/MainViewControls.axaml | 175 ++ .../UI/Views/Main/MainViewControls.axaml.cs | 54 + .../UI/Views/Settings/SettingsAudioView.axaml | 81 + .../UI/Views/Settings/SettingsAudioView.axaml.cs | 12 + .../UI/Views/Settings/SettingsCPUView.axaml | 78 + .../UI/Views/Settings/SettingsCPUView.axaml.cs | 12 + .../UI/Views/Settings/SettingsGraphicsView.axaml | 296 +++ .../Views/Settings/SettingsGraphicsView.axaml.cs | 12 + .../UI/Views/Settings/SettingsHotkeysView.axaml | 104 ++ .../UI/Views/Settings/SettingsHotkeysView.axaml.cs | 81 + .../UI/Views/Settings/SettingsInputView.axaml | 46 + .../UI/Views/Settings/SettingsInputView.axaml.cs | 17 + .../UI/Views/Settings/SettingsLoggingView.axaml | 121 ++ .../UI/Views/Settings/SettingsLoggingView.axaml.cs | 12 + .../UI/Views/Settings/SettingsNetworkView.axaml | 46 + .../UI/Views/Settings/SettingsNetworkView.axaml.cs | 12 + .../UI/Views/Settings/SettingsSystemView.axaml | 195 ++ .../UI/Views/Settings/SettingsSystemView.axaml.cs | 52 + .../UI/Views/Settings/SettingsUIView.axaml | 156 ++ .../UI/Views/Settings/SettingsUIView.axaml.cs | 82 + src/Ryujinx.Ava/UI/Views/User/UserEditorView.axaml | 123 ++ .../UI/Views/User/UserEditorView.axaml.cs | 165 ++ .../User/UserFirmwareAvatarSelectorView.axaml | 114 ++ .../User/UserFirmwareAvatarSelectorView.axaml.cs | 88 + .../Views/User/UserProfileImageSelectorView.axaml | 63 + .../User/UserProfileImageSelectorView.axaml.cs | 124 ++ .../UI/Views/User/UserRecovererView.axaml | 83 + .../UI/Views/User/UserRecovererView.axaml.cs | 51 + .../UI/Views/User/UserSaveManagerView.axaml | 215 +++ .../UI/Views/User/UserSaveManagerView.axaml.cs | 147 ++ .../UI/Views/User/UserSelectorView.axaml | 165 ++ .../UI/Views/User/UserSelectorView.axaml.cs | 128 ++ src/Ryujinx.Ava/UI/Windows/AboutWindow.axaml | 247 +++ src/Ryujinx.Ava/UI/Windows/AboutWindow.axaml.cs | 62 + src/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml | 74 + src/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs | 59 + src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml | 106 ++ src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs | 119 ++ .../UI/Windows/ContentDialogOverlayWindow.axaml | 25 + .../UI/Windows/ContentDialogOverlayWindow.axaml.cs | 25 + .../UI/Windows/ControllerSettingsWindow.axaml | 1169 ++++++++++++ .../UI/Windows/ControllerSettingsWindow.axaml.cs | 181 ++ .../Windows/DownloadableContentManagerWindow.axaml | 194 ++ .../DownloadableContentManagerWindow.axaml.cs | 115 ++ src/Ryujinx.Ava/UI/Windows/IconColorPicker.cs | 192 ++ src/Ryujinx.Ava/UI/Windows/MainWindow.axaml | 206 +++ src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs | 441 +++++ .../UI/Windows/MotionSettingsWindow.axaml | 141 ++ .../UI/Windows/MotionSettingsWindow.axaml.cs | 71 + .../UI/Windows/RumbleSettingsWindow.axaml | 57 + .../UI/Windows/RumbleSettingsWindow.axaml.cs | 57 + src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml | 128 ++ src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs | 103 ++ src/Ryujinx.Ava/UI/Windows/StyleableWindow.cs | 39 + src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml | 135 ++ .../UI/Windows/TitleUpdateWindow.axaml.cs | 96 + 125 files changed, 17588 insertions(+) create mode 100644 src/Ryujinx.Ava/UI/Applet/AvaHostUiHandler.cs create mode 100644 src/Ryujinx.Ava/UI/Applet/AvaloniaDynamicTextInputHandler.cs create mode 100644 src/Ryujinx.Ava/UI/Applet/AvaloniaHostUiTheme.cs create mode 100644 src/Ryujinx.Ava/UI/Applet/ErrorAppletWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Applet/ErrorAppletWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml create mode 100644 src/Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Controls/GameGridView.axaml create mode 100644 src/Ryujinx.Ava/UI/Controls/GameGridView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Controls/GameListView.axaml create mode 100644 src/Ryujinx.Ava/UI/Controls/GameListView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml create mode 100644 src/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Helpers/ApplicationOpenedEventArgs.cs create mode 100644 src/Ryujinx.Ava/UI/Helpers/BitmapArrayValueConverter.cs create mode 100644 src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs create mode 100644 src/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs create mode 100644 src/Ryujinx.Ava/UI/Helpers/Glyph.cs create mode 100644 src/Ryujinx.Ava/UI/Helpers/GlyphValueConverter.cs create mode 100644 src/Ryujinx.Ava/UI/Helpers/HotKeyControl.cs create mode 100644 src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs create mode 100644 src/Ryujinx.Ava/UI/Helpers/LoggerAdapter.cs create mode 100644 src/Ryujinx.Ava/UI/Helpers/MiniCommand.cs create mode 100644 src/Ryujinx.Ava/UI/Helpers/NotificationHelper.cs create mode 100644 src/Ryujinx.Ava/UI/Helpers/OffscreenTextBox.cs create mode 100644 src/Ryujinx.Ava/UI/Helpers/UserErrorDialog.cs create mode 100644 src/Ryujinx.Ava/UI/Helpers/UserResult.cs create mode 100644 src/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs create mode 100644 src/Ryujinx.Ava/UI/Models/CheatModel.cs create mode 100644 src/Ryujinx.Ava/UI/Models/CheatsList.cs create mode 100644 src/Ryujinx.Ava/UI/Models/ControllerModel.cs create mode 100644 src/Ryujinx.Ava/UI/Models/DeviceType.cs create mode 100644 src/Ryujinx.Ava/UI/Models/DownloadableContentModel.cs create mode 100644 src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs create mode 100644 src/Ryujinx.Ava/UI/Models/InputConfiguration.cs create mode 100644 src/Ryujinx.Ava/UI/Models/PlayerModel.cs create mode 100644 src/Ryujinx.Ava/UI/Models/ProfileImageModel.cs create mode 100644 src/Ryujinx.Ava/UI/Models/SaveModel.cs create mode 100644 src/Ryujinx.Ava/UI/Models/StatusUpdatedEventArgs.cs create mode 100644 src/Ryujinx.Ava/UI/Models/TempProfile.cs create mode 100644 src/Ryujinx.Ava/UI/Models/TimeZone.cs create mode 100644 src/Ryujinx.Ava/UI/Models/TitleUpdateModel.cs create mode 100644 src/Ryujinx.Ava/UI/Models/UserProfile.cs create mode 100644 src/Ryujinx.Ava/UI/Renderer/EmbeddedWindow.cs create mode 100644 src/Ryujinx.Ava/UI/Renderer/EmbeddedWindowOpenGL.cs create mode 100644 src/Ryujinx.Ava/UI/Renderer/EmbeddedWindowVulkan.cs create mode 100644 src/Ryujinx.Ava/UI/Renderer/OpenTKBindingsContext.cs create mode 100644 src/Ryujinx.Ava/UI/Renderer/RendererHost.axaml create mode 100644 src/Ryujinx.Ava/UI/Renderer/RendererHost.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Renderer/SPBOpenGLContext.cs create mode 100644 src/Ryujinx.Ava/UI/ViewModels/AboutWindowViewModel.cs create mode 100644 src/Ryujinx.Ava/UI/ViewModels/AmiiboWindowViewModel.cs create mode 100644 src/Ryujinx.Ava/UI/ViewModels/AvatarProfileViewModel.cs create mode 100644 src/Ryujinx.Ava/UI/ViewModels/BaseModel.cs create mode 100644 src/Ryujinx.Ava/UI/ViewModels/ControllerSettingsViewModel.cs create mode 100644 src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs create mode 100644 src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs create mode 100644 src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs create mode 100644 src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs create mode 100644 src/Ryujinx.Ava/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs create mode 100644 src/Ryujinx.Ava/UI/ViewModels/UserProfileImageSelectorViewModel.cs create mode 100644 src/Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs create mode 100644 src/Ryujinx.Ava/UI/ViewModels/UserSaveManagerViewModel.cs create mode 100644 src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Views/Settings/SettingsAudioView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/Settings/SettingsAudioView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Views/Settings/SettingsCPUView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/Settings/SettingsCPUView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Views/Settings/SettingsLoggingView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/Settings/SettingsLoggingView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Views/Settings/SettingsNetworkView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/Settings/SettingsNetworkView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Views/Settings/SettingsUIView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/Settings/SettingsUIView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Views/User/UserEditorView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/User/UserEditorView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/User/UserProfileImageSelectorView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/User/UserRecovererView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml create mode 100644 src/Ryujinx.Ava/UI/Views/User/UserSelectorView.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/AboutWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/AboutWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/IconColorPicker.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/MainWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/StyleableWindow.cs create mode 100644 src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml create mode 100644 src/Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs (limited to 'src/Ryujinx.Ava/UI') diff --git a/src/Ryujinx.Ava/UI/Applet/AvaHostUiHandler.cs b/src/Ryujinx.Ava/UI/Applet/AvaHostUiHandler.cs new file mode 100644 index 00000000..c87308cf --- /dev/null +++ b/src/Ryujinx.Ava/UI/Applet/AvaHostUiHandler.cs @@ -0,0 +1,197 @@ +using Avalonia.Controls; +using Avalonia.Threading; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Controls; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.HLE; +using Ryujinx.HLE.HOS.Applets; +using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; +using Ryujinx.HLE.Ui; +using System; +using System.Threading; + +namespace Ryujinx.Ava.UI.Applet +{ + internal class AvaHostUiHandler : IHostUiHandler + { + private readonly MainWindow _parent; + + public IHostUiTheme HostUiTheme { get; } + + public AvaHostUiHandler(MainWindow parent) + { + _parent = parent; + + HostUiTheme = new AvaloniaHostUiTheme(parent); + } + + public bool DisplayMessageDialog(ControllerAppletUiArgs args) + { + string message = LocaleManager.Instance.UpdateAndGetDynamicValue( + args.PlayerCountMin == args.PlayerCountMax ? LocaleKeys.DialogControllerAppletMessage : LocaleKeys.DialogControllerAppletMessagePlayerRange, + args.PlayerCountMin == args.PlayerCountMax ? args.PlayerCountMin.ToString() : $"{args.PlayerCountMin}-{args.PlayerCountMax}", + args.SupportedStyles, + string.Join(", ", args.SupportedPlayers), + args.IsDocked ? LocaleManager.Instance[LocaleKeys.DialogControllerAppletDockModeSet] : ""); + + return DisplayMessageDialog(LocaleManager.Instance[LocaleKeys.DialogControllerAppletTitle], message); + } + + public bool DisplayMessageDialog(string title, string message) + { + ManualResetEvent dialogCloseEvent = new(false); + + bool okPressed = false; + + Dispatcher.UIThread.InvokeAsync(async () => + { + try + { + ManualResetEvent deferEvent = new(false); + + bool opened = false; + + _parent.Activate(); + + UserResult response = await ContentDialogHelper.ShowDeferredContentDialog(_parent, + title, + message, + "", + LocaleManager.Instance[LocaleKeys.DialogOpenSettingsWindowLabel], + "", + LocaleManager.Instance[LocaleKeys.SettingsButtonClose], + (int)Symbol.Important, + deferEvent, + async (window) => + { + if (opened) + { + return; + } + + opened = true; + + _parent.SettingsWindow = new SettingsWindow(_parent.VirtualFileSystem, _parent.ContentManager); + + await _parent.SettingsWindow.ShowDialog(window); + + opened = false; + }); + + if (response == UserResult.Ok) + { + okPressed = true; + } + + dialogCloseEvent.Set(); + } + catch (Exception ex) + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogMessageDialogErrorExceptionMessage, ex)); + + dialogCloseEvent.Set(); + } + }); + + dialogCloseEvent.WaitOne(); + + return okPressed; + } + + public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText) + { + ManualResetEvent dialogCloseEvent = new(false); + + bool okPressed = false; + bool error = false; + string inputText = args.InitialText ?? ""; + + Dispatcher.UIThread.Post(async () => + { + try + { + var response = await SwkbdAppletDialog.ShowInputDialog(_parent, LocaleManager.Instance[LocaleKeys.SoftwareKeyboard], args); + + if (response.Result == UserResult.Ok) + { + inputText = response.Input; + okPressed = true; + } + } + catch (Exception ex) + { + error = true; + + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogSoftwareKeyboardErrorExceptionMessage, ex)); + } + finally + { + dialogCloseEvent.Set(); + } + }); + + dialogCloseEvent.WaitOne(); + + userText = error ? null : inputText; + + return error || okPressed; + } + + public void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value) + { + device.Configuration.UserChannelPersistence.ExecuteProgram(kind, value); + if (_parent.ViewModel.AppHost != null) + { + _parent.ViewModel.AppHost.Stop(); + } + } + + public bool DisplayErrorAppletDialog(string title, string message, string[] buttons) + { + ManualResetEvent dialogCloseEvent = new(false); + + bool showDetails = false; + + Dispatcher.UIThread.Post(async () => + { + try + { + ErrorAppletWindow msgDialog = new(_parent, buttons, message) + { + Title = title, + WindowStartupLocation = WindowStartupLocation.CenterScreen, + Width = 400 + }; + + object response = await msgDialog.Run(); + + if (response != null && buttons != null && buttons.Length > 1 && (int)response != buttons.Length - 1) + { + showDetails = true; + } + + dialogCloseEvent.Set(); + + msgDialog.Close(); + } + catch (Exception ex) + { + dialogCloseEvent.Set(); + + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogErrorAppletErrorExceptionMessage, ex)); + } + }); + + dialogCloseEvent.WaitOne(); + + return showDetails; + } + + public IDynamicTextInputHandler CreateDynamicTextInputHandler() + { + return new AvaloniaDynamicTextInputHandler(_parent); + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Applet/AvaloniaDynamicTextInputHandler.cs b/src/Ryujinx.Ava/UI/Applet/AvaloniaDynamicTextInputHandler.cs new file mode 100644 index 00000000..2dd65e36 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Applet/AvaloniaDynamicTextInputHandler.cs @@ -0,0 +1,164 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Threading; +using Ryujinx.Ava.Input; +using Ryujinx.Ava.UI.Controls; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.HLE.Ui; +using System; +using System.Threading; + +using HidKey = Ryujinx.Common.Configuration.Hid.Key; + +namespace Ryujinx.Ava.UI.Applet +{ + class AvaloniaDynamicTextInputHandler : IDynamicTextInputHandler + { + private MainWindow _parent; + private OffscreenTextBox _hiddenTextBox; + private bool _canProcessInput; + private IDisposable _textChangedSubscription; + private IDisposable _selectionStartChangedSubscription; + private IDisposable _selectionEndtextChangedSubscription; + + public AvaloniaDynamicTextInputHandler(MainWindow parent) + { + _parent = parent; + + (_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).KeyPressed += AvaloniaDynamicTextInputHandler_KeyPressed; + (_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).KeyRelease += AvaloniaDynamicTextInputHandler_KeyRelease; + (_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).TextInput += AvaloniaDynamicTextInputHandler_TextInput; + + _hiddenTextBox = _parent.HiddenTextBox; + + Dispatcher.UIThread.Post(() => + { + _textChangedSubscription = _hiddenTextBox.GetObservable(TextBox.TextProperty).Subscribe(TextChanged); + _selectionStartChangedSubscription = _hiddenTextBox.GetObservable(TextBox.SelectionStartProperty).Subscribe(SelectionChanged); + _selectionEndtextChangedSubscription = _hiddenTextBox.GetObservable(TextBox.SelectionEndProperty).Subscribe(SelectionChanged); + }); + } + + private void TextChanged(string text) + { + TextChangedEvent?.Invoke(text ?? string.Empty, _hiddenTextBox.SelectionStart, _hiddenTextBox.SelectionEnd, true); + } + + private void SelectionChanged(int selection) + { + if (_hiddenTextBox.SelectionEnd < _hiddenTextBox.SelectionStart) + { + _hiddenTextBox.SelectionStart = _hiddenTextBox.SelectionEnd; + } + + TextChangedEvent?.Invoke(_hiddenTextBox.Text ?? string.Empty, _hiddenTextBox.SelectionStart, _hiddenTextBox.SelectionEnd, true); + } + + private void AvaloniaDynamicTextInputHandler_TextInput(object sender, string text) + { + Dispatcher.UIThread.InvokeAsync(() => + { + if (_canProcessInput) + { + _hiddenTextBox.SendText(text); + } + }); + } + + private void AvaloniaDynamicTextInputHandler_KeyRelease(object sender, KeyEventArgs e) + { + var key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.Key); + + if (!(KeyReleasedEvent?.Invoke(key)).GetValueOrDefault(true)) + { + return; + } + + e.RoutedEvent = _hiddenTextBox.GetKeyUpRoutedEvent(); + + Dispatcher.UIThread.InvokeAsync(() => + { + if (_canProcessInput) + { + _hiddenTextBox.SendKeyUpEvent(e); + } + }); + } + + private void AvaloniaDynamicTextInputHandler_KeyPressed(object sender, KeyEventArgs e) + { + var key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.Key); + + if (!(KeyPressedEvent?.Invoke(key)).GetValueOrDefault(true)) + { + return; + } + + e.RoutedEvent = _hiddenTextBox.GetKeyUpRoutedEvent(); + + Dispatcher.UIThread.InvokeAsync(() => + { + if (_canProcessInput) + { + _hiddenTextBox.SendKeyDownEvent(e); + } + }); + } + + public bool TextProcessingEnabled + { + get + { + return Volatile.Read(ref _canProcessInput); + } + set + { + Volatile.Write(ref _canProcessInput, value); + } + } + + public event DynamicTextChangedHandler TextChangedEvent; + public event KeyPressedHandler KeyPressedEvent; + public event KeyReleasedHandler KeyReleasedEvent; + + public void Dispose() + { + (_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).KeyPressed -= AvaloniaDynamicTextInputHandler_KeyPressed; + (_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).KeyRelease -= AvaloniaDynamicTextInputHandler_KeyRelease; + (_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).TextInput -= AvaloniaDynamicTextInputHandler_TextInput; + + _textChangedSubscription?.Dispose(); + _selectionStartChangedSubscription?.Dispose(); + _selectionEndtextChangedSubscription?.Dispose(); + + Dispatcher.UIThread.Post(() => + { + _hiddenTextBox.Clear(); + _parent.ViewModel.RendererHostControl.Focus(); + + _parent = null; + }); + } + + public void SetText(string text, int cursorBegin) + { + Dispatcher.UIThread.Post(() => + { + _hiddenTextBox.Text = text; + _hiddenTextBox.CaretIndex = cursorBegin; + }); + } + + public void SetText(string text, int cursorBegin, int cursorEnd) + { + Dispatcher.UIThread.Post(() => + { + _hiddenTextBox.Text = text; + _hiddenTextBox.SelectionStart = cursorBegin; + _hiddenTextBox.SelectionEnd = cursorEnd; + }); + } + } +} diff --git a/src/Ryujinx.Ava/UI/Applet/AvaloniaHostUiTheme.cs b/src/Ryujinx.Ava/UI/Applet/AvaloniaHostUiTheme.cs new file mode 100644 index 00000000..77c7a2d2 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Applet/AvaloniaHostUiTheme.cs @@ -0,0 +1,43 @@ +using Avalonia.Media; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.HLE.Ui; +using System; + +namespace Ryujinx.Ava.UI.Applet +{ + class AvaloniaHostUiTheme : IHostUiTheme + { + public AvaloniaHostUiTheme(MainWindow parent) + { + FontFamily = OperatingSystem.IsWindows() && OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000, 0) ? "Segoe UI Variable" : parent.FontFamily.Name; + DefaultBackgroundColor = BrushToThemeColor(parent.Background); + DefaultForegroundColor = BrushToThemeColor(parent.Foreground); + DefaultBorderColor = BrushToThemeColor(parent.BorderBrush); + SelectionBackgroundColor = BrushToThemeColor(parent.ViewControls.SearchBox.SelectionBrush); + SelectionForegroundColor = BrushToThemeColor(parent.ViewControls.SearchBox.SelectionForegroundBrush); + } + + public string FontFamily { get; } + + public ThemeColor DefaultBackgroundColor { get; } + public ThemeColor DefaultForegroundColor { get; } + public ThemeColor DefaultBorderColor { get; } + public ThemeColor SelectionBackgroundColor { get; } + public ThemeColor SelectionForegroundColor { get; } + + private ThemeColor BrushToThemeColor(IBrush brush) + { + if (brush is SolidColorBrush solidColor) + { + return new ThemeColor((float)solidColor.Color.A / 255, + (float)solidColor.Color.R / 255, + (float)solidColor.Color.G / 255, + (float)solidColor.Color.B / 255); + } + else + { + return new ThemeColor(); + } + } + } +} diff --git a/src/Ryujinx.Ava/UI/Applet/ErrorAppletWindow.axaml b/src/Ryujinx.Ava/UI/Applet/ErrorAppletWindow.axaml new file mode 100644 index 00000000..211b4725 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Applet/ErrorAppletWindow.axaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Applet/ErrorAppletWindow.axaml.cs b/src/Ryujinx.Ava/UI/Applet/ErrorAppletWindow.axaml.cs new file mode 100644 index 00000000..4134797b --- /dev/null +++ b/src/Ryujinx.Ava/UI/Applet/ErrorAppletWindow.axaml.cs @@ -0,0 +1,80 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Threading; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Windows; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Applet +{ + internal partial class ErrorAppletWindow : StyleableWindow + { + private readonly Window _owner; + private object _buttonResponse; + + public ErrorAppletWindow(Window owner, string[] buttons, string message) + { + _owner = owner; + Message = message; + DataContext = this; + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + int responseId = 0; + + if (buttons != null) + { + foreach (string buttonText in buttons) + { + AddButton(buttonText, responseId); + responseId++; + } + } + else + { + AddButton(LocaleManager.Instance[LocaleKeys.InputDialogOk], 0); + } + } + + public ErrorAppletWindow() + { + DataContext = this; + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + public string Message { get; set; } + + private void AddButton(string label, object tag) + { + Dispatcher.UIThread.InvokeAsync(() => + { + Button button = new() { Content = label, Tag = tag }; + + button.Click += Button_Click; + ButtonStack.Children.Add(button); + }); + } + + private void Button_Click(object sender, RoutedEventArgs e) + { + if (sender is Button button) + { + _buttonResponse = button.Tag; + } + + Close(); + } + + public async Task Run() + { + await ShowDialog(_owner); + + return _buttonResponse; + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml b/src/Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml new file mode 100644 index 00000000..65504569 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml.cs b/src/Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml.cs new file mode 100644 index 00000000..cb69e96b --- /dev/null +++ b/src/Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml.cs @@ -0,0 +1,148 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using FluentAvalonia.Core; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.HLE.HOS.Applets; +using System; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Controls +{ + internal partial class SwkbdAppletDialog : UserControl + { + private Predicate _checkLength; + private int _inputMax; + private int _inputMin; + private string _placeholder; + + private ContentDialog _host; + + public SwkbdAppletDialog(string mainText, string secondaryText, string placeholder, string message) + { + MainText = mainText; + SecondaryText = secondaryText; + Message = message ?? ""; + DataContext = this; + _placeholder = placeholder; + InitializeComponent(); + + Input.Watermark = _placeholder; + + Input.AddHandler(TextInputEvent, Message_TextInput, RoutingStrategies.Tunnel, true); + + SetInputLengthValidation(0, int.MaxValue); // Disable by default. + } + + public SwkbdAppletDialog() + { + DataContext = this; + InitializeComponent(); + } + + protected override void OnGotFocus(GotFocusEventArgs e) + { + // FIXME: This does not work. Might be a bug in Avalonia with DialogHost + // Currently focus will be redirected to the overlay window instead. + Input.Focus(); + } + + public string Message { get; set; } = ""; + public string MainText { get; set; } = ""; + public string SecondaryText { get; set; } = ""; + + public static async Task<(UserResult Result, string Input)> ShowInputDialog(StyleableWindow window, string title, SoftwareKeyboardUiArgs args) + { + ContentDialog contentDialog = new ContentDialog(); + + UserResult result = UserResult.Cancel; + + SwkbdAppletDialog content = new SwkbdAppletDialog(args.HeaderText, args.SubtitleText, args.GuideText, args.InitialText); + + string input = string.Empty; + + content.SetInputLengthValidation(args.StringLengthMin, args.StringLengthMax); + + content._host = contentDialog; + contentDialog.Title = title; + contentDialog.PrimaryButtonText = args.SubmitText; + contentDialog.IsPrimaryButtonEnabled = content._checkLength(content.Message.Length); + contentDialog.SecondaryButtonText = ""; + contentDialog.CloseButtonText = LocaleManager.Instance[LocaleKeys.InputDialogCancel]; + contentDialog.Content = content; + + TypedEventHandler handler = (sender, eventArgs) => + { + if (eventArgs.Result == ContentDialogResult.Primary) + { + result = UserResult.Ok; + input = content.Input.Text; + } + }; + contentDialog.Closed += handler; + + await ContentDialogHelper.ShowAsync(contentDialog); + + return (result, input); + } + + public void SetInputLengthValidation(int min, int max) + { + _inputMin = Math.Min(min, max); + _inputMax = Math.Max(min, max); + + Error.IsVisible = false; + Error.FontStyle = FontStyle.Italic; + + if (_inputMin <= 0 && _inputMax == int.MaxValue) // Disable. + { + Error.IsVisible = false; + + _checkLength = length => true; + } + else if (_inputMin > 0 && _inputMax == int.MaxValue) + { + Error.IsVisible = true; + + Error.Text = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SwkbdMinCharacters, _inputMin); + + _checkLength = length => _inputMin <= length; + } + else + { + Error.IsVisible = true; + + Error.Text = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SwkbdMinRangeCharacters, _inputMin, _inputMax); + + _checkLength = length => _inputMin <= length && length <= _inputMax; + } + + Message_TextInput(this, new TextInputEventArgs()); + } + + private void Message_TextInput(object sender, TextInputEventArgs e) + { + if (_host != null) + { + _host.IsPrimaryButtonEnabled = _checkLength(Message.Length); + } + } + + private void Message_KeyUp(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter && _host.IsPrimaryButtonEnabled) + { + _host.Hide(ContentDialogResult.Primary); + } + else + { + _host.IsPrimaryButtonEnabled = _checkLength(Message.Length); + } + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Controls/GameGridView.axaml b/src/Ryujinx.Ava/UI/Controls/GameGridView.axaml new file mode 100644 index 00000000..32cabfaa --- /dev/null +++ b/src/Ryujinx.Ava/UI/Controls/GameGridView.axaml @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Controls/GameGridView.axaml.cs b/src/Ryujinx.Ava/UI/Controls/GameGridView.axaml.cs new file mode 100644 index 00000000..aa76b7c9 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Controls/GameGridView.axaml.cs @@ -0,0 +1,57 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ui.App.Common; +using System; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class GameGridView : UserControl + { + public static readonly RoutedEvent ApplicationOpenedEvent = + RoutedEvent.Register(nameof(ApplicationOpened), RoutingStrategies.Bubble); + + public event EventHandler ApplicationOpened + { + add { AddHandler(ApplicationOpenedEvent, value); } + remove { RemoveHandler(ApplicationOpenedEvent, value); } + } + + public GameGridView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public void GameList_DoubleTapped(object sender, RoutedEventArgs args) + { + if (sender is ListBox listBox) + { + if (listBox.SelectedItem is ApplicationData selected) + { + RaiseEvent(new ApplicationOpenedEventArgs(selected, ApplicationOpenedEvent)); + } + } + } + + public void GameList_SelectionChanged(object sender, SelectionChangedEventArgs args) + { + if (sender is ListBox listBox) + { + (DataContext as MainWindowViewModel).GridSelectedApplication = listBox.SelectedItem as ApplicationData; + } + } + + private void SearchBox_OnKeyUp(object sender, KeyEventArgs e) + { + (DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text; + } + } +} diff --git a/src/Ryujinx.Ava/UI/Controls/GameListView.axaml b/src/Ryujinx.Ava/UI/Controls/GameListView.axaml new file mode 100644 index 00000000..c13eaae8 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Controls/GameListView.axaml @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Controls/GameListView.axaml.cs b/src/Ryujinx.Ava/UI/Controls/GameListView.axaml.cs new file mode 100644 index 00000000..a6449709 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Controls/GameListView.axaml.cs @@ -0,0 +1,57 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ui.App.Common; +using System; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class GameListView : UserControl + { + public static readonly RoutedEvent ApplicationOpenedEvent = + RoutedEvent.Register(nameof(ApplicationOpened), RoutingStrategies.Bubble); + + public event EventHandler ApplicationOpened + { + add { AddHandler(ApplicationOpenedEvent, value); } + remove { RemoveHandler(ApplicationOpenedEvent, value); } + } + + public GameListView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public void GameList_DoubleTapped(object sender, RoutedEventArgs args) + { + if (sender is ListBox listBox) + { + if (listBox.SelectedItem is ApplicationData selected) + { + RaiseEvent(new ApplicationOpenedEventArgs(selected, ApplicationOpenedEvent)); + } + } + } + + public void GameList_SelectionChanged(object sender, SelectionChangedEventArgs args) + { + if (sender is ListBox listBox) + { + (DataContext as MainWindowViewModel).ListSelectedApplication = listBox.SelectedItem as ApplicationData; + } + } + + private void SearchBox_OnKeyUp(object sender, KeyEventArgs e) + { + (DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text; + } + } +} diff --git a/src/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml b/src/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml new file mode 100644 index 00000000..bf34b303 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs b/src/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs new file mode 100644 index 00000000..1b857fae --- /dev/null +++ b/src/Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs @@ -0,0 +1,218 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Styling; +using Avalonia.Threading; +using FluentAvalonia.Core; +using FluentAvalonia.UI.Controls; +using LibHac; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Shim; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.Models; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.Views.User; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using UserProfile = Ryujinx.Ava.UI.Models.UserProfile; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class NavigationDialogHost : UserControl + { + public AccountManager AccountManager { get; } + public ContentManager ContentManager { get; } + public VirtualFileSystem VirtualFileSystem { get; } + public HorizonClient HorizonClient { get; } + public UserProfileViewModel ViewModel { get; set; } + + public NavigationDialogHost() + { + InitializeComponent(); + } + + public NavigationDialogHost(AccountManager accountManager, ContentManager contentManager, + VirtualFileSystem virtualFileSystem, HorizonClient horizonClient) + { + AccountManager = accountManager; + ContentManager = contentManager; + VirtualFileSystem = virtualFileSystem; + HorizonClient = horizonClient; + ViewModel = new UserProfileViewModel(); + LoadProfiles(); + + if (contentManager.GetCurrentFirmwareVersion() != null) + { + Task.Run(() => + { + UserFirmwareAvatarSelectorViewModel.PreloadAvatars(contentManager, virtualFileSystem); + }); + } + InitializeComponent(); + } + + public void GoBack(object parameter = null) + { + if (ContentFrame.BackStack.Count > 0) + { + ContentFrame.GoBack(); + } + + LoadProfiles(); + } + + public void Navigate(Type sourcePageType, object parameter) + { + ContentFrame.Navigate(sourcePageType, parameter); + } + + public static async Task Show(AccountManager ownerAccountManager, ContentManager ownerContentManager, + VirtualFileSystem ownerVirtualFileSystem, HorizonClient ownerHorizonClient) + { + var content = new NavigationDialogHost(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient); + ContentDialog contentDialog = new ContentDialog + { + Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle], + PrimaryButtonText = "", + SecondaryButtonText = "", + CloseButtonText = "", + Content = content, + Padding = new Thickness(0) + }; + + contentDialog.Closed += (sender, args) => + { + content.ViewModel.Dispose(); + }; + + Style footer = new(x => x.Name("DialogSpace").Child().OfType()); + footer.Setters.Add(new Setter(IsVisibleProperty, false)); + + contentDialog.Styles.Add(footer); + + await contentDialog.ShowAsync(); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + Navigate(typeof(UserSelectorViews), this); + } + + public void LoadProfiles() + { + ViewModel.Profiles.Clear(); + ViewModel.LostProfiles.Clear(); + + var profiles = AccountManager.GetAllUsers().OrderBy(x => x.Name); + + foreach (var profile in profiles) + { + ViewModel.Profiles.Add(new UserProfile(profile, this)); + } + + var saveDataFilter = SaveDataFilter.Make(programId: default, saveType: SaveDataType.Account, default, saveDataId: default, index: default); + + using var saveDataIterator = new UniqueRef(); + + HorizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref, SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure(); + + Span saveDataInfo = stackalloc SaveDataInfo[10]; + + HashSet lostAccounts = new(); + + while (true) + { + saveDataIterator.Get.ReadSaveDataInfo(out long readCount, saveDataInfo).ThrowIfFailure(); + + if (readCount == 0) + { + break; + } + + for (int i = 0; i < readCount; i++) + { + var save = saveDataInfo[i]; + var id = new HLE.HOS.Services.Account.Acc.UserId((long)save.UserId.Id.Low, (long)save.UserId.Id.High); + if (ViewModel.Profiles.Cast().FirstOrDefault( x=> x.UserId == id) == null) + { + lostAccounts.Add(id); + } + } + } + + foreach(var account in lostAccounts) + { + ViewModel.LostProfiles.Add(new UserProfile(new HLE.HOS.Services.Account.Acc.UserProfile(account, "", null), this)); + } + + ViewModel.Profiles.Add(new BaseModel()); + } + + public async void DeleteUser(UserProfile userProfile) + { + var lastUserId = AccountManager.LastOpenedUser.UserId; + + if (userProfile.UserId == lastUserId) + { + // If we are deleting the currently open profile, then we must open something else before deleting. + var profile = ViewModel.Profiles.Cast().FirstOrDefault(x => x.UserId != lastUserId); + + if (profile == null) + { + async void Action() + { + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionWarningMessage]); + } + + Dispatcher.UIThread.Post(Action); + + return; + } + + AccountManager.OpenUser(profile.UserId); + } + + var result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionConfirmMessage], + "", + LocaleManager.Instance[LocaleKeys.InputDialogYes], + LocaleManager.Instance[LocaleKeys.InputDialogNo], + ""); + + if (result == UserResult.Yes) + { + GoBack(); + AccountManager.DeleteUser(userProfile.UserId); + } + + LoadProfiles(); + } + + public void AddUser() + { + Navigate(typeof(UserEditorView), (this, (UserProfile)null, true)); + } + + public void EditUser(UserProfile userProfile) + { + Navigate(typeof(UserEditorView), (this, userProfile, false)); + } + + public void RecoverLostAccounts() + { + Navigate(typeof(UserRecovererView), this); + } + + public void ManageSaves() + { + Navigate(typeof(UserSaveManagerView), (this, AccountManager, HorizonClient, VirtualFileSystem)); + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml b/src/Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml new file mode 100644 index 00000000..c5041230 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml.cs b/src/Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml.cs new file mode 100644 index 00000000..80a437e3 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml.cs @@ -0,0 +1,31 @@ +using Avalonia.Controls; +using Ryujinx.Ava.UI.Windows; +using System.Threading; + +namespace Ryujinx.Ava.UI.Controls +{ + public partial class UpdateWaitWindow : StyleableWindow + { + public UpdateWaitWindow(string primaryText, string secondaryText, CancellationTokenSource cancellationToken) : this(primaryText, secondaryText) + { + SystemDecorations = SystemDecorations.Full; + ShowInTaskbar = true; + + Closing += (_, _) => cancellationToken.Cancel(); + } + + public UpdateWaitWindow(string primaryText, string secondaryText) : this() + { + PrimaryText.Text = primaryText; + SecondaryText.Text = secondaryText; + WindowStartupLocation = WindowStartupLocation.CenterOwner; + SystemDecorations = SystemDecorations.BorderOnly; + ShowInTaskbar = false; + } + + public UpdateWaitWindow() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Helpers/ApplicationOpenedEventArgs.cs b/src/Ryujinx.Ava/UI/Helpers/ApplicationOpenedEventArgs.cs new file mode 100644 index 00000000..ebf5c16e --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/ApplicationOpenedEventArgs.cs @@ -0,0 +1,16 @@ +using Avalonia.Interactivity; +using Ryujinx.Ui.App.Common; + +namespace Ryujinx.Ava.UI.Helpers +{ + public class ApplicationOpenedEventArgs : RoutedEventArgs + { + public ApplicationData Application { get; } + + public ApplicationOpenedEventArgs(ApplicationData application, RoutedEvent routedEvent) + { + Application = application; + RoutedEvent = routedEvent; + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Helpers/BitmapArrayValueConverter.cs b/src/Ryujinx.Ava/UI/Helpers/BitmapArrayValueConverter.cs new file mode 100644 index 00000000..3fd368f8 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/BitmapArrayValueConverter.cs @@ -0,0 +1,35 @@ +using Avalonia.Data.Converters; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using System; +using System.Globalization; +using System.IO; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class BitmapArrayValueConverter : IValueConverter + { + public static BitmapArrayValueConverter Instance = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null) + { + return null; + } + + if (value is byte[] buffer && targetType == typeof(IImage)) + { + MemoryStream mem = new(buffer); + return new Bitmap(mem); + } + + throw new NotSupportedException(); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs b/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs new file mode 100644 index 00000000..6730b571 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs @@ -0,0 +1,118 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.LogicalTree; +using Avalonia.Threading; +using Ryujinx.Input; +using Ryujinx.Input.Assigner; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class ButtonKeyAssigner + { + internal class ButtonAssignedEventArgs : EventArgs + { + public ToggleButton Button { get; } + public bool IsAssigned { get; } + + public ButtonAssignedEventArgs(ToggleButton button, bool isAssigned) + { + Button = button; + IsAssigned = isAssigned; + } + } + + public ToggleButton ToggledButton { get; set; } + + private bool _isWaitingForInput; + private bool _shouldUnbind; + public event EventHandler ButtonAssigned; + + public ButtonKeyAssigner(ToggleButton toggleButton) + { + ToggledButton = toggleButton; + } + + public async void GetInputAndAssign(IButtonAssigner assigner, IKeyboard keyboard = null) + { + Dispatcher.UIThread.Post(() => + { + ToggledButton.IsChecked = true; + }); + + if (_isWaitingForInput) + { + Dispatcher.UIThread.Post(() => + { + Cancel(); + }); + + return; + } + + _isWaitingForInput = true; + + assigner.Initialize(); + + await Task.Run(async () => + { + while (true) + { + if (!_isWaitingForInput) + { + return; + } + + await Task.Delay(10); + + assigner.ReadInput(); + + if (assigner.HasAnyButtonPressed() || assigner.ShouldCancel() || (keyboard != null && keyboard.IsPressed(Key.Escape))) + { + break; + } + } + }); + + await Dispatcher.UIThread.InvokeAsync(() => + { + string pressedButton = assigner.GetPressedButton(); + + if (_shouldUnbind) + { + SetButtonText(ToggledButton, "Unbound"); + } + else if (pressedButton != "") + { + SetButtonText(ToggledButton, pressedButton); + } + + _shouldUnbind = false; + _isWaitingForInput = false; + + ToggledButton.IsChecked = false; + + ButtonAssigned?.Invoke(this, new ButtonAssignedEventArgs(ToggledButton, pressedButton != null)); + + static void SetButtonText(ToggleButton button, string text) + { + ILogical textBlock = button.GetLogicalDescendants().First(x => x is TextBlock); + + if (textBlock != null && textBlock is TextBlock block) + { + block.Text = text; + } + } + }); + } + + public void Cancel(bool shouldUnbind = false) + { + _isWaitingForInput = false; + ToggledButton.IsChecked = false; + _shouldUnbind = shouldUnbind; + } + } +} diff --git a/src/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs b/src/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs new file mode 100644 index 00000000..cb474506 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs @@ -0,0 +1,407 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Media; +using Avalonia.Threading; +using FluentAvalonia.Core; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Controls; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.Common.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Helpers +{ + public static class ContentDialogHelper + { + private static bool _isChoiceDialogOpen; + + public async static Task ShowContentDialog( + string title, + object content, + string primaryButton, + string secondaryButton, + string closeButton, + UserResult primaryButtonResult = UserResult.Ok, + ManualResetEvent deferResetEvent = null, + TypedEventHandler deferCloseAction = null) + { + UserResult result = UserResult.None; + + ContentDialog contentDialog = new() + { + Title = title, + PrimaryButtonText = primaryButton, + SecondaryButtonText = secondaryButton, + CloseButtonText = closeButton, + Content = content + }; + + contentDialog.PrimaryButtonCommand = MiniCommand.Create(() => + { + result = primaryButtonResult; + }); + + contentDialog.SecondaryButtonCommand = MiniCommand.Create(() => + { + result = UserResult.No; + contentDialog.PrimaryButtonClick -= deferCloseAction; + }); + + contentDialog.CloseButtonCommand = MiniCommand.Create(() => + { + result = UserResult.Cancel; + contentDialog.PrimaryButtonClick -= deferCloseAction; + }); + + if (deferResetEvent != null) + { + contentDialog.PrimaryButtonClick += deferCloseAction; + } + + await ShowAsync(contentDialog); + + return result; + } + + private async static Task ShowTextDialog( + string title, + string primaryText, + string secondaryText, + string primaryButton, + string secondaryButton, + string closeButton, + int iconSymbol, + UserResult primaryButtonResult = UserResult.Ok, + ManualResetEvent deferResetEvent = null, + TypedEventHandler deferCloseAction = null) + { + Grid content = CreateTextDialogContent(primaryText, secondaryText, iconSymbol); + + return await ShowContentDialog(title, content, primaryButton, secondaryButton, closeButton, primaryButtonResult, deferResetEvent, deferCloseAction); + } + + public async static Task ShowDeferredContentDialog( + StyleableWindow window, + string title, + string primaryText, + string secondaryText, + string primaryButton, + string secondaryButton, + string closeButton, + int iconSymbol, + ManualResetEvent deferResetEvent, + Func doWhileDeferred = null) + { + bool startedDeferring = false; + UserResult result = UserResult.None; + + return await ShowTextDialog( + title, + primaryText, + secondaryText, + primaryButton, + secondaryButton, + closeButton, + iconSymbol, + primaryButton == LocaleManager.Instance[LocaleKeys.InputDialogYes] ? UserResult.Yes : UserResult.Ok, + deferResetEvent, + DeferClose); + + async void DeferClose(ContentDialog sender, ContentDialogButtonClickEventArgs args) + { + if (startedDeferring) + { + return; + } + + sender.PrimaryButtonClick -= DeferClose; + + startedDeferring = true; + + var deferral = args.GetDeferral(); + + result = primaryButton == LocaleManager.Instance[LocaleKeys.InputDialogYes] ? UserResult.Yes : UserResult.Ok; + + sender.PrimaryButtonClick -= DeferClose; + + _ = Task.Run(() => + { + deferResetEvent.WaitOne(); + + Dispatcher.UIThread.Post(() => + { + deferral.Complete(); + }); + }); + + if (doWhileDeferred != null) + { + await doWhileDeferred(window); + + deferResetEvent.Set(); + } + } + } + + private static Grid CreateTextDialogContent(string primaryText, string secondaryText, int symbol) + { + Grid content = new() + { + RowDefinitions = new RowDefinitions() { new RowDefinition(), new RowDefinition() }, + ColumnDefinitions = new ColumnDefinitions() { new ColumnDefinition(GridLength.Auto), new ColumnDefinition() }, + + MinHeight = 80 + }; + + SymbolIcon icon = new() + { + Symbol = (Symbol)symbol, + Margin = new Thickness(10), + FontSize = 40, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center + }; + + Grid.SetColumn(icon, 0); + Grid.SetRowSpan(icon, 2); + Grid.SetRow(icon, 0); + + TextBlock primaryLabel = new() + { + Text = primaryText, + Margin = new Thickness(5), + TextWrapping = TextWrapping.Wrap, + MaxWidth = 450 + }; + + TextBlock secondaryLabel = new() + { + Text = secondaryText, + Margin = new Thickness(5), + TextWrapping = TextWrapping.Wrap, + MaxWidth = 450 + }; + + Grid.SetColumn(primaryLabel, 1); + Grid.SetColumn(secondaryLabel, 1); + Grid.SetRow(primaryLabel, 0); + Grid.SetRow(secondaryLabel, 1); + + content.Children.Add(icon); + content.Children.Add(primaryLabel); + content.Children.Add(secondaryLabel); + + return content; + } + + public static async Task CreateInfoDialog( + string primary, + string secondaryText, + string acceptButton, + string closeButton, + string title) + { + return await ShowTextDialog( + title, + primary, + secondaryText, + acceptButton, + "", + closeButton, + (int)Symbol.Important); + } + + internal static async Task CreateConfirmationDialog( + string primaryText, + string secondaryText, + string acceptButtonText, + string cancelButtonText, + string title, + UserResult primaryButtonResult = UserResult.Yes) + { + return await ShowTextDialog( + string.IsNullOrWhiteSpace(title) ? LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle] : title, + primaryText, + secondaryText, + acceptButtonText, + "", + cancelButtonText, + (int)Symbol.Help, + primaryButtonResult); + } + + internal static async Task CreateUpdaterInfoDialog(string primary, string secondaryText) + { + await ShowTextDialog( + LocaleManager.Instance[LocaleKeys.DialogUpdaterTitle], + primary, + secondaryText, + "", + "", + LocaleManager.Instance[LocaleKeys.InputDialogOk], + (int)Symbol.Important); + } + + internal static async Task CreateWarningDialog(string primary, string secondaryText) + { + await ShowTextDialog( + LocaleManager.Instance[LocaleKeys.DialogWarningTitle], + primary, + secondaryText, + "", + "", + LocaleManager.Instance[LocaleKeys.InputDialogOk], + (int)Symbol.Important); + } + + internal static async Task CreateErrorDialog(string errorMessage, string secondaryErrorMessage = "") + { + Logger.Error?.Print(LogClass.Application, errorMessage); + + await ShowTextDialog( + LocaleManager.Instance[LocaleKeys.DialogErrorTitle], + LocaleManager.Instance[LocaleKeys.DialogErrorMessage], + errorMessage, + secondaryErrorMessage, + "", + LocaleManager.Instance[LocaleKeys.InputDialogOk], + (int)Symbol.Dismiss); + } + + internal static async Task CreateChoiceDialog(string title, string primary, string secondaryText) + { + if (_isChoiceDialogOpen) + { + return false; + } + + _isChoiceDialogOpen = true; + + UserResult response = await ShowTextDialog( + title, + primary, + secondaryText, + LocaleManager.Instance[LocaleKeys.InputDialogYes], + "", + LocaleManager.Instance[LocaleKeys.InputDialogNo], + (int)Symbol.Help, + UserResult.Yes); + + _isChoiceDialogOpen = false; + + return response == UserResult.Yes; + } + + internal static async Task CreateExitDialog() + { + return await CreateChoiceDialog( + LocaleManager.Instance[LocaleKeys.DialogExitTitle], + LocaleManager.Instance[LocaleKeys.DialogExitMessage], + LocaleManager.Instance[LocaleKeys.DialogExitSubMessage]); + } + + internal static async Task CreateStopEmulationDialog() + { + return await CreateChoiceDialog( + LocaleManager.Instance[LocaleKeys.DialogStopEmulationTitle], + LocaleManager.Instance[LocaleKeys.DialogStopEmulationMessage], + LocaleManager.Instance[LocaleKeys.DialogExitSubMessage]); + } + + public static async Task ShowAsync(ContentDialog contentDialog) + { + ContentDialogResult result; + + ContentDialogOverlayWindow contentDialogOverlayWindow = null; + + Window parent = GetMainWindow(); + + if (parent != null && parent.IsActive && parent is MainWindow window && window.ViewModel.IsGameRunning) + { + contentDialogOverlayWindow = new() + { + Height = parent.Bounds.Height, + Width = parent.Bounds.Width, + Position = parent.PointToScreen(new Point()), + ShowInTaskbar = false + }; + + parent.PositionChanged += OverlayOnPositionChanged; + + void OverlayOnPositionChanged(object sender, PixelPointEventArgs e) + { + contentDialogOverlayWindow.Position = parent.PointToScreen(new Point()); + } + + contentDialogOverlayWindow.ContentDialog = contentDialog; + + bool opened = false; + + contentDialogOverlayWindow.Opened += OverlayOnActivated; + + async void OverlayOnActivated(object sender, EventArgs e) + { + if (opened) + { + return; + } + + opened = true; + + contentDialogOverlayWindow.Position = parent.PointToScreen(new Point()); + + result = await ShowDialog(); + } + + result = await contentDialogOverlayWindow.ShowDialog(parent); + } + else + { + result = await ShowDialog(); + } + + async Task ShowDialog() + { + if (contentDialogOverlayWindow is not null) + { + result = await contentDialog.ShowAsync(contentDialogOverlayWindow); + + contentDialogOverlayWindow!.Close(); + } + else + { + result = await contentDialog.ShowAsync(); + } + + return result; + } + + if (contentDialogOverlayWindow is not null) + { + contentDialogOverlayWindow.Content = null; + contentDialogOverlayWindow.Close(); + } + + return result; + } + + private static Window GetMainWindow() + { + if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime al) + { + foreach (Window item in al.Windows) + { + if (item.IsActive && item is MainWindow window) + { + return window; + } + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Helpers/Glyph.cs b/src/Ryujinx.Ava/UI/Helpers/Glyph.cs new file mode 100644 index 00000000..4aae854f --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/Glyph.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Ava.UI.Helpers +{ + public enum Glyph + { + List, + Grid, + Chip + } +} diff --git a/src/Ryujinx.Ava/UI/Helpers/GlyphValueConverter.cs b/src/Ryujinx.Ava/UI/Helpers/GlyphValueConverter.cs new file mode 100644 index 00000000..3d6c9c01 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/GlyphValueConverter.cs @@ -0,0 +1,49 @@ +using Avalonia.Data; +using Avalonia.Markup.Xaml; +using FluentAvalonia.UI.Controls; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Ava.UI.Helpers +{ + public class GlyphValueConverter : MarkupExtension + { + private string _key; + + private static Dictionary _glyphs = new Dictionary + { + { Glyph.List, char.ConvertFromUtf32((int)Symbol.List).ToString() }, + { Glyph.Grid, char.ConvertFromUtf32((int)Symbol.ViewAll).ToString() }, + { Glyph.Chip, char.ConvertFromUtf32(59748).ToString() } + }; + + public GlyphValueConverter(string key) + { + _key = key; + } + + public string this[string key] + { + get + { + if (_glyphs.TryGetValue(Enum.Parse(key), out var val)) + { + return val; + } + + return string.Empty; + } + } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension binding = new($"[{_key}]") + { + Mode = BindingMode.OneWay, + Source = this + }; + + return binding.ProvideValue(serviceProvider); + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Helpers/HotKeyControl.cs b/src/Ryujinx.Ava/UI/Helpers/HotKeyControl.cs new file mode 100644 index 00000000..f1fad157 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/HotKeyControl.cs @@ -0,0 +1,52 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using System; +using System.Windows.Input; + +namespace Ryujinx.Ava.UI.Helpers +{ + public class HotKeyControl : ContentControl, ICommandSource + { + public static readonly StyledProperty CommandParameterProperty = + AvaloniaProperty.Register(nameof(CommandParameter)); + + public static readonly DirectProperty CommandProperty = + AvaloniaProperty.RegisterDirect(nameof(Command), + control => control.Command, (control, command) => control.Command = command, enableDataValidation: true); + + public static readonly StyledProperty HotKeyProperty = HotKeyManager.HotKeyProperty.AddOwner