From deb99d2cae3e80bdf70cb52c6c160094dc7c9292 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Sun, 15 May 2022 11:30:15 +0000 Subject: Avalonia UI - Part 1 (#3270) * avalonia part 1 * remove vulkan ui backend * move ui common files to ui common project * get name for oading screen from device * rebase. * review 1 * review 1.1 * review * cleanup * addressed review * use cancellation token * review * review * rebased * cancel library loading when closing window * remove star image, use fonticon instead * delete render control frame buffer when game ends. change position of fav star * addressed @Thog review * ensure the right ui is downloaded in updates * fix crash when showing not supported dialog during controller request * add prefix to artifact names * Auto-format Avalonia project * Fix input * Fix build, simplify app disposal * remove nv stutter thread * addressed review * add missing change * maintain window size if new size is zero length * add game, handheld, docked to local * reverse scale main window * Update de_DE.json * Update de_DE.json * Update de_DE.json * Update italian json * Update it_IT.json * let render timer poll with no wait * remove unused code * more unused code * enabled tiered compilation and trimming * check if window event is not closed before signaling * fix atmospher case * locale fix * locale fix * remove explicit tiered compilation declarations * Remove ) it_IT.json * Remove ) de_DE.json * Update it_IT.json * Update pt_BR locale with latest strings * Remove ')' * add more strings to locale * update locale * remove extra slash * remove extra slash * set firmware version to 0 if key's not found * fix * revert timer changes * lock on object instead * Update it_IT.json * remove unused method * add load screen text to locale * drop swap event * Update de_DE.json * Update de_DE.json * do null check when stopping emulator * Update de_DE.json * Create tr_TR.json * Add tr_TR * Add tr_TR + Turkish * Update it_IT.json * Update Ryujinx.Ava/Input/AvaloniaMappingHelper.cs Co-authored-by: Ac_K * Apply suggestions from code review Co-authored-by: Ac_K * Apply suggestions from code review Co-authored-by: Ac_K * addressed review * Update Ryujinx.Ava/Ui/Backend/OpenGl/OpenGlRenderTarget.cs Co-authored-by: gdkchan * use avalonia's inbuilt renderer on linux * removed whitespace * workaround for queue render crash with vsync off * drop custom backend * format files * fix not closing issue * remove warnings * rebase * update avalonia library * Reposition the Text and Button on About Page * Assign build version * Remove appveyor text Co-authored-by: gdk Co-authored-by: Niwu34 <67392333+Niwu34@users.noreply.github.com> Co-authored-by: Antonio Brugnolo <36473846+AntoSkate@users.noreply.github.com> Co-authored-by: aegiff <99728970+aegiff@users.noreply.github.com> Co-authored-by: Ac_K Co-authored-by: MostlyWhat <78652091+MostlyWhat@users.noreply.github.com> --- .../Ui/Controls/ApplicationOpenedEventArgs.cs | 16 + Ryujinx.Ava/Ui/Controls/AvaloniaGlxContext.cs | 16 + Ryujinx.Ava/Ui/Controls/AvaloniaWglContext.cs | 16 + .../Ui/Controls/BitmapArrayValueConverter.cs | 35 ++ Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs | 357 +++++++++++++++++++++ Ryujinx.Ava/Ui/Controls/GameGridView.axaml | 188 +++++++++++ Ryujinx.Ava/Ui/Controls/GameGridView.axaml.cs | 82 +++++ Ryujinx.Ava/Ui/Controls/GameListView.axaml | 188 +++++++++++ Ryujinx.Ava/Ui/Controls/GameListView.axaml.cs | 82 +++++ Ryujinx.Ava/Ui/Controls/Glyph.cs | 9 + Ryujinx.Ava/Ui/Controls/GlyphValueConverter.cs | 49 +++ Ryujinx.Ava/Ui/Controls/HotKeyControl.cs | 52 +++ Ryujinx.Ava/Ui/Controls/IGlContextExtension.cs | 25 ++ Ryujinx.Ava/Ui/Controls/InputDialog.axaml | 18 ++ Ryujinx.Ava/Ui/Controls/InputDialog.axaml.cs | 67 ++++ Ryujinx.Ava/Ui/Controls/KeyValueConverter.cs | 46 +++ Ryujinx.Ava/Ui/Controls/MiniCommand.cs | 71 ++++ Ryujinx.Ava/Ui/Controls/OffscreenTextBox.cs | 40 +++ .../Ui/Controls/OpenToolkitBindingsContext.cs | 20 ++ Ryujinx.Ava/Ui/Controls/RenderTimer.cs | 100 ++++++ Ryujinx.Ava/Ui/Controls/RendererControl.cs | 273 ++++++++++++++++ Ryujinx.Ava/Ui/Controls/SPBOpenGLContext.cs | 47 +++ Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml | 28 ++ Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml.cs | 35 ++ Ryujinx.Ava/Ui/Controls/UserErrorDialog.cs | 91 ++++++ Ryujinx.Ava/Ui/Controls/UserResult.cs | 12 + 26 files changed, 1963 insertions(+) create mode 100644 Ryujinx.Ava/Ui/Controls/ApplicationOpenedEventArgs.cs create mode 100644 Ryujinx.Ava/Ui/Controls/AvaloniaGlxContext.cs create mode 100644 Ryujinx.Ava/Ui/Controls/AvaloniaWglContext.cs create mode 100644 Ryujinx.Ava/Ui/Controls/BitmapArrayValueConverter.cs create mode 100644 Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs create mode 100644 Ryujinx.Ava/Ui/Controls/GameGridView.axaml create mode 100644 Ryujinx.Ava/Ui/Controls/GameGridView.axaml.cs create mode 100644 Ryujinx.Ava/Ui/Controls/GameListView.axaml create mode 100644 Ryujinx.Ava/Ui/Controls/GameListView.axaml.cs create mode 100644 Ryujinx.Ava/Ui/Controls/Glyph.cs create mode 100644 Ryujinx.Ava/Ui/Controls/GlyphValueConverter.cs create mode 100644 Ryujinx.Ava/Ui/Controls/HotKeyControl.cs create mode 100644 Ryujinx.Ava/Ui/Controls/IGlContextExtension.cs create mode 100644 Ryujinx.Ava/Ui/Controls/InputDialog.axaml create mode 100644 Ryujinx.Ava/Ui/Controls/InputDialog.axaml.cs create mode 100644 Ryujinx.Ava/Ui/Controls/KeyValueConverter.cs create mode 100644 Ryujinx.Ava/Ui/Controls/MiniCommand.cs create mode 100644 Ryujinx.Ava/Ui/Controls/OffscreenTextBox.cs create mode 100644 Ryujinx.Ava/Ui/Controls/OpenToolkitBindingsContext.cs create mode 100644 Ryujinx.Ava/Ui/Controls/RenderTimer.cs create mode 100644 Ryujinx.Ava/Ui/Controls/RendererControl.cs create mode 100644 Ryujinx.Ava/Ui/Controls/SPBOpenGLContext.cs create mode 100644 Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml create mode 100644 Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml.cs create mode 100644 Ryujinx.Ava/Ui/Controls/UserErrorDialog.cs create mode 100644 Ryujinx.Ava/Ui/Controls/UserResult.cs (limited to 'Ryujinx.Ava/Ui/Controls') diff --git a/Ryujinx.Ava/Ui/Controls/ApplicationOpenedEventArgs.cs b/Ryujinx.Ava/Ui/Controls/ApplicationOpenedEventArgs.cs new file mode 100644 index 00000000..9909bd6a --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/ApplicationOpenedEventArgs.cs @@ -0,0 +1,16 @@ +using Avalonia.Interactivity; +using Ryujinx.Ui.App.Common; + +namespace Ryujinx.Ava.Ui.Controls +{ + 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/Ryujinx.Ava/Ui/Controls/AvaloniaGlxContext.cs b/Ryujinx.Ava/Ui/Controls/AvaloniaGlxContext.cs new file mode 100644 index 00000000..05f28cb1 --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/AvaloniaGlxContext.cs @@ -0,0 +1,16 @@ +using SPB.Graphics; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Ava.Ui.Controls +{ + [SupportedOSPlatform("linux")] + public class AvaloniaGlxContext : SPB.Platform.GLX.GLXOpenGLContext + { + public AvaloniaGlxContext(IntPtr handle) + : base(FramebufferFormat.Default, 0, 0, 0, false, null) + { + ContextHandle = handle; + } + } +} diff --git a/Ryujinx.Ava/Ui/Controls/AvaloniaWglContext.cs b/Ryujinx.Ava/Ui/Controls/AvaloniaWglContext.cs new file mode 100644 index 00000000..d7781fac --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/AvaloniaWglContext.cs @@ -0,0 +1,16 @@ +using SPB.Graphics; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Ava.Ui.Controls +{ + [SupportedOSPlatform("windows")] + public class AvaloniaWglContext : SPB.Platform.WGL.WGLOpenGLContext + { + public AvaloniaWglContext(IntPtr handle) + : base(FramebufferFormat.Default, 0, 0, 0, false, null) + { + ContextHandle = handle; + } + } +} diff --git a/Ryujinx.Ava/Ui/Controls/BitmapArrayValueConverter.cs b/Ryujinx.Ava/Ui/Controls/BitmapArrayValueConverter.cs new file mode 100644 index 00000000..7222d67b --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/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.Controls +{ + public 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/Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs b/Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs new file mode 100644 index 00000000..2794a815 --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs @@ -0,0 +1,357 @@ +using Avalonia.Controls; +using Avalonia.Threading; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Ui.Models; +using Ryujinx.Ava.Ui.Windows; +using Ryujinx.Common.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.Ui.Controls +{ + public static class ContentDialogHelper + { + private static bool _isChoiceDialogOpen; + + private async static Task ShowContentDialog( + StyleableWindow window, + string title, + string primaryText, + string secondaryText, + string primaryButton, + string secondaryButton, + string closeButton, + int iconSymbol, + UserResult primaryButtonResult = UserResult.Ok) + { + UserResult result = UserResult.None; + + ContentDialog contentDialog = window.ContentDialog; + + await ShowDialog(); + + async Task ShowDialog() + { + if (contentDialog != null) + { + contentDialog.Title = title; + contentDialog.PrimaryButtonText = primaryButton; + contentDialog.SecondaryButtonText = secondaryButton; + contentDialog.CloseButtonText = closeButton; + contentDialog.Content = CreateDialogTextContent(primaryText, secondaryText, iconSymbol); + + contentDialog.PrimaryButtonCommand = MiniCommand.Create(() => + { + result = primaryButtonResult; + }); + contentDialog.SecondaryButtonCommand = MiniCommand.Create(() => + { + result = UserResult.No; + }); + contentDialog.CloseButtonCommand = MiniCommand.Create(() => + { + result = UserResult.Cancel; + }); + + await contentDialog.ShowAsync(ContentDialogPlacement.Popup); + }; + } + + return result; + } + + 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; + + ContentDialog contentDialog = window.ContentDialog; + + Window overlay = window; + + if (contentDialog != null) + { + contentDialog.PrimaryButtonClick += DeferClose; + contentDialog.Title = title; + contentDialog.PrimaryButtonText = primaryButton; + contentDialog.SecondaryButtonText = secondaryButton; + contentDialog.CloseButtonText = closeButton; + contentDialog.Content = CreateDialogTextContent(primaryText, secondaryText, iconSymbol); + + contentDialog.PrimaryButtonCommand = MiniCommand.Create(() => + { + result = primaryButton == LocaleManager.Instance["InputDialogYes"] ? UserResult.Yes : UserResult.Ok; + }); + contentDialog.SecondaryButtonCommand = MiniCommand.Create(() => + { + result = UserResult.No; + }); + contentDialog.CloseButtonCommand = MiniCommand.Create(() => + { + result = UserResult.Cancel; + }); + await contentDialog.ShowAsync(ContentDialogPlacement.Popup); + }; + + return result; + + async void DeferClose(ContentDialog sender, ContentDialogButtonClickEventArgs args) + { + if (startedDeferring) + { + return; + } + + startedDeferring = true; + + var deferral = args.GetDeferral(); + + result = primaryButton == LocaleManager.Instance["InputDialogYes"] ? UserResult.Yes : UserResult.Ok; + + contentDialog.PrimaryButtonClick -= DeferClose; + +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + Task.Run(() => + { + deferResetEvent.WaitOne(); + + Dispatcher.UIThread.Post(() => + { + deferral.Complete(); + }); + }); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + + if (doWhileDeferred != null) + { + await doWhileDeferred(overlay); + + deferResetEvent.Set(); + } + } + } + + private static Grid CreateDialogTextContent(string primaryText, string secondaryText, int symbol) + { + Grid content = new Grid(); + content.RowDefinitions = new RowDefinitions() { new RowDefinition(), new RowDefinition() }; + content.ColumnDefinitions = new ColumnDefinitions() { new ColumnDefinition(GridLength.Auto), new ColumnDefinition() }; + + content.MinHeight = 80; + + SymbolIcon icon = new SymbolIcon { Symbol = (Symbol)symbol, Margin = new Avalonia.Thickness(10) }; + icon.FontSize = 40; + icon.VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center; + Grid.SetColumn(icon, 0); + Grid.SetRowSpan(icon, 2); + Grid.SetRow(icon, 0); + + TextBlock primaryLabel = new TextBlock() + { + Text = primaryText, + Margin = new Avalonia.Thickness(5), + TextWrapping = Avalonia.Media.TextWrapping.Wrap, + MaxWidth = 450 + }; + TextBlock secondaryLabel = new TextBlock() + { + Text = secondaryText, + Margin = new Avalonia.Thickness(5), + TextWrapping = Avalonia.Media.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( + StyleableWindow window, + string primary, + string secondaryText, + string acceptButton, + string closeButton, + string title) + { + return await ShowContentDialog( + window, + title, + primary, + secondaryText, + acceptButton, + "", + closeButton, + (int)Symbol.Important); + } + + internal static async Task CreateConfirmationDialog( + StyleableWindow window, + string primaryText, + string secondaryText, + string acceptButtonText, + string cancelButtonText, + string title, + UserResult primaryButtonResult = UserResult.Yes) + { + return await ShowContentDialog( + window, + string.IsNullOrWhiteSpace(title) ? LocaleManager.Instance["DialogConfirmationTitle"] : title, + primaryText, + secondaryText, + acceptButtonText, + "", + cancelButtonText, + (int)Symbol.Help, + primaryButtonResult); + } + + internal static UpdateWaitWindow CreateWaitingDialog(string mainText, string secondaryText) + { + return new(mainText, secondaryText); + } + + internal static async void CreateUpdaterInfoDialog(StyleableWindow window, string primary, string secondaryText) + { + await ShowContentDialog( + window, + LocaleManager.Instance["DialogUpdaterTitle"], + primary, + secondaryText, + "", + "", + LocaleManager.Instance["InputDialogOk"], + (int)Symbol.Important); + } + + internal static async void ShowNotAvailableMessage(StyleableWindow window) + { + // Temporary placeholder for features to be added + await ShowContentDialog( + window, + "Feature Not Available", + "The selected feature is not available in this version.", + "", + "", + "", + LocaleManager.Instance["InputDialogOk"], + (int)Symbol.Important); + } + + internal static async void CreateWarningDialog(StyleableWindow window, string primary, string secondaryText) + { + await ShowContentDialog( + window, + LocaleManager.Instance["DialogWarningTitle"], + primary, + secondaryText, + "", + "", + LocaleManager.Instance["InputDialogOk"], + (int)Symbol.Important); + } + + internal static async void CreateErrorDialog(StyleableWindow owner, string errorMessage, string secondaryErrorMessage = "") + { + Logger.Error?.Print(LogClass.Application, errorMessage); + + await ShowContentDialog( + owner, + LocaleManager.Instance["DialogErrorTitle"], + LocaleManager.Instance["DialogErrorMessage"], + errorMessage, + secondaryErrorMessage, + "", + LocaleManager.Instance["InputDialogOk"], + (int)Symbol.Dismiss); + } + + internal static async Task CreateChoiceDialog(StyleableWindow window, string title, string primary, string secondaryText) + { + if (_isChoiceDialogOpen) + { + return false; + } + + _isChoiceDialogOpen = true; + + UserResult response = + await ShowContentDialog( + window, + title, + primary, + secondaryText, + LocaleManager.Instance["InputDialogYes"], + "", + LocaleManager.Instance["InputDialogNo"], + (int)Symbol.Help, + UserResult.Yes); + + _isChoiceDialogOpen = false; + + return response == UserResult.Yes; + } + + internal static async Task CreateExitDialog(StyleableWindow owner) + { + return await CreateChoiceDialog( + owner, + LocaleManager.Instance["DialogExitTitle"], + LocaleManager.Instance["DialogExitMessage"], + LocaleManager.Instance["DialogExitSubMessage"]); + } + + internal static async Task CreateStopEmulationDialog(StyleableWindow owner) + { + return await CreateChoiceDialog( + owner, + LocaleManager.Instance["DialogStopEmulationTitle"], + LocaleManager.Instance["DialogStopEmulationMessage"], + LocaleManager.Instance["DialogExitSubMessage"]); + } + + internal static async Task CreateInputDialog( + string title, + string mainText, + string subText, + StyleableWindow owner, + uint maxLength = int.MaxValue, + string input = "") + { + var result = await InputDialog.ShowInputDialog( + owner, + title, + mainText, + input, + subText, + maxLength); + + if (result.Result == UserResult.Ok) + { + return result.Input; + } + + return string.Empty; + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/GameGridView.axaml b/Ryujinx.Ava/Ui/Controls/GameGridView.axaml new file mode 100644 index 00000000..13b75f11 --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/GameGridView.axaml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/GameGridView.axaml.cs b/Ryujinx.Ava/Ui/Controls/GameGridView.axaml.cs new file mode 100644 index 00000000..4dfe4f0e --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/GameGridView.axaml.cs @@ -0,0 +1,82 @@ +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using LibHac.Common; +using Ryujinx.Ava.Ui.ViewModels; +using Ryujinx.Ui.App.Common; +using System; + +namespace Ryujinx.Ava.Ui.Controls +{ + public partial class GameGridView : UserControl + { + private ApplicationData _selectedApplication; + public static readonly RoutedEvent ApplicationOpenedEvent = + RoutedEvent.Register(nameof(ApplicationOpened), RoutingStrategies.Bubble); + + public event EventHandler ApplicationOpened + { + add { AddHandler(ApplicationOpenedEvent, value); } + remove { RemoveHandler(ApplicationOpenedEvent, value); } + } + + 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) + { + var selected = listBox.SelectedItem as ApplicationData; + + _selectedApplication = selected; + } + } + + public ApplicationData SelectedApplication => _selectedApplication; + + public GameGridView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void SearchBox_OnKeyUp(object sender, KeyEventArgs e) + { + (DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text; + } + + private void MenuBase_OnMenuOpened(object sender, EventArgs e) + { + var selection = SelectedApplication; + + if (selection != null) + { + if (sender is ContextMenu menu) + { + bool canHaveUserSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.UserAccountSaveDataSize > 0; + bool canHaveDeviceSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.DeviceSaveDataSize > 0; + bool canHaveBcatSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0; + + ((menu.Items as AvaloniaList)[2] as MenuItem).IsEnabled = canHaveUserSave; + ((menu.Items as AvaloniaList)[3] as MenuItem).IsEnabled = canHaveDeviceSave; + ((menu.Items as AvaloniaList)[4] as MenuItem).IsEnabled = canHaveBcatSave; + } + } + } + } +} diff --git a/Ryujinx.Ava/Ui/Controls/GameListView.axaml b/Ryujinx.Ava/Ui/Controls/GameListView.axaml new file mode 100644 index 00000000..7ab79c23 --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/GameListView.axaml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/GameListView.axaml.cs b/Ryujinx.Ava/Ui/Controls/GameListView.axaml.cs new file mode 100644 index 00000000..1cca3bea --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/GameListView.axaml.cs @@ -0,0 +1,82 @@ +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using LibHac.Common; +using Ryujinx.Ava.Ui.ViewModels; +using Ryujinx.Ui.App.Common; +using System; + +namespace Ryujinx.Ava.Ui.Controls +{ + public partial class GameListView : UserControl + { + private ApplicationData _selectedApplication; + public static readonly RoutedEvent ApplicationOpenedEvent = + RoutedEvent.Register(nameof(ApplicationOpened), RoutingStrategies.Bubble); + + public event EventHandler ApplicationOpened + { + add { AddHandler(ApplicationOpenedEvent, value); } + remove { RemoveHandler(ApplicationOpenedEvent, value); } + } + + 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) + { + var selected = listBox.SelectedItem as ApplicationData; + + _selectedApplication = selected; + } + } + + public ApplicationData SelectedApplication => _selectedApplication; + + public GameListView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void SearchBox_OnKeyUp(object sender, KeyEventArgs e) + { + (DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text; + } + + private void MenuBase_OnMenuOpened(object sender, EventArgs e) + { + var selection = SelectedApplication; + + if (selection != null) + { + if (sender is ContextMenu menu) + { + bool canHaveUserSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.UserAccountSaveDataSize > 0; + bool canHaveDeviceSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.DeviceSaveDataSize > 0; + bool canHaveBcatSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0; + + ((menu.Items as AvaloniaList)[2] as MenuItem).IsEnabled = canHaveUserSave; + ((menu.Items as AvaloniaList)[3] as MenuItem).IsEnabled = canHaveDeviceSave; + ((menu.Items as AvaloniaList)[4] as MenuItem).IsEnabled = canHaveBcatSave; + } + } + } + } +} diff --git a/Ryujinx.Ava/Ui/Controls/Glyph.cs b/Ryujinx.Ava/Ui/Controls/Glyph.cs new file mode 100644 index 00000000..74a3c94a --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/Glyph.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Ava.Ui.Controls +{ + public enum Glyph : int + { + List, + Grid, + Chip + } +} diff --git a/Ryujinx.Ava/Ui/Controls/GlyphValueConverter.cs b/Ryujinx.Ava/Ui/Controls/GlyphValueConverter.cs new file mode 100644 index 00000000..63c6a17d --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/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.Controls +{ + 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/Ryujinx.Ava/Ui/Controls/HotKeyControl.cs b/Ryujinx.Ava/Ui/Controls/HotKeyControl.cs new file mode 100644 index 00000000..d3ab1e8f --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/HotKeyControl.cs @@ -0,0 +1,52 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using System; +using System.Windows.Input; + +namespace Ryujinx.Ava.Ui.Controls +{ + 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