diff options
| author | Emmanuel Hansen <emmausssss@gmail.com> | 2022-05-15 11:30:15 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-05-15 13:30:15 +0200 |
| commit | deb99d2cae3e80bdf70cb52c6c160094dc7c9292 (patch) | |
| tree | e60f44d1b4bd45bbf36fcfa750fb99787febfdbe /Ryujinx.Ava/Ui/Controls | |
| parent | 9ba73ffbe5f78c0403cf102b95768f388da05122 (diff) | |
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 <Acoustik666@gmail.com>
* Apply suggestions from code review
Co-authored-by: Ac_K <Acoustik666@gmail.com>
* Apply suggestions from code review
Co-authored-by: Ac_K <Acoustik666@gmail.com>
* addressed review
* Update Ryujinx.Ava/Ui/Backend/OpenGl/OpenGlRenderTarget.cs
Co-authored-by: gdkchan <gab.dark.100@gmail.com>
* 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 <gab.dark.100@gmail.com>
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 <Acoustik666@gmail.com>
Co-authored-by: MostlyWhat <78652091+MostlyWhat@users.noreply.github.com>
Diffstat (limited to 'Ryujinx.Ava/Ui/Controls')
26 files changed, 1963 insertions, 0 deletions
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<UserResult> 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<UserResult> ShowDeferredContentDialog( + StyleableWindow window, + string title, + string primaryText, + string secondaryText, + string primaryButton, + string secondaryButton, + string closeButton, + int iconSymbol, + ManualResetEvent deferResetEvent, + Func<Window, Task> 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<UserResult> 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<UserResult> 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<bool> 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<bool> CreateExitDialog(StyleableWindow owner) + { + return await CreateChoiceDialog( + owner, + LocaleManager.Instance["DialogExitTitle"], + LocaleManager.Instance["DialogExitMessage"], + LocaleManager.Instance["DialogExitSubMessage"]); + } + + internal static async Task<bool> CreateStopEmulationDialog(StyleableWindow owner) + { + return await CreateChoiceDialog( + owner, + LocaleManager.Instance["DialogStopEmulationTitle"], + LocaleManager.Instance["DialogStopEmulationMessage"], + LocaleManager.Instance["DialogExitSubMessage"]); + } + + internal static async Task<string> 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 @@ +<UserControl xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" + xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + x:Class="Ryujinx.Ava.Ui.Controls.GameGridView"> + <UserControl.Resources> + <controls:BitmapArrayValueConverter x:Key="ByteImage" /> + <MenuFlyout x:Key="GameContextMenu" Opened="MenuBase_OnMenuOpened"> + <MenuItem + Command="{Binding ToggleFavorite}" + Header="{locale:Locale GameListContextMenuToggleFavorite}" + ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" /> + <Separator /> + <MenuItem + Command="{Binding OpenUserSaveDirectory}" + Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}" + ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" /> + <MenuItem + Command="{Binding OpenDeviceSaveDirectory}" + Header="{locale:Locale GameListContextMenuOpenUserDeviceDirectory}" + ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserDeviceDirectoryToolTip}" /> + <MenuItem + Command="{Binding OpenBcatSaveDirectory}" + Header="{locale:Locale GameListContextMenuOpenUserBcatDirectory}" + ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserBcatDirectoryToolTip}" /> + <Separator /> + <MenuItem + Command="{Binding OpenTitleUpdateManager}" + Header="{locale:Locale GameListContextMenuManageTitleUpdates}" + ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" /> + <MenuItem + Command="{Binding OpenDlcManager}" + Header="{locale:Locale GameListContextMenuManageDlc}" + ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" /> + <MenuItem + Command="{Binding OpenCheatManager}" + Header="{locale:Locale GameListContextMenuManageCheat}" + ToolTip.Tip="{locale:Locale GameListContextMenuManageCheatToolTip}" /> + <MenuItem + Command="{Binding OpenModsDirectory}" + Header="{locale:Locale GameListContextMenuOpenModsDirectory}" + ToolTip.Tip="{locale:Locale GameListContextMenuOpenModsDirectoryToolTip}" /> + <MenuItem + Command="{Binding OpenSdModsDirectory}" + Header="{locale:Locale GameListContextMenuOpenSdModsDirectory}" + ToolTip.Tip="{locale:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" /> + <Separator /> + <MenuItem Header="{locale:Locale GameListContextMenuCacheManagement}"> + <MenuItem + Command="{Binding PurgePtcCache}" + Header="{locale:Locale GameListContextMenuCacheManagementPurgePptc}" + ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgePptcToolTip}" /> + <MenuItem + Command="{Binding PurgeShaderCache}" + Header="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCache}" + ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCacheToolTip}" /> + <MenuItem + Command="{Binding OpenPtcDirectory}" + Header="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectory}" + ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectoryToolTip}" /> + <MenuItem + Command="{Binding OpenShaderCacheDirectory}" + Header="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectory}" + ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip}" /> + </MenuItem> + <MenuItem Header="{locale:Locale GameListContextMenuExtractData}"> + <MenuItem + Command="{Binding ExtractExeFs}" + Header="{locale:Locale GameListContextMenuExtractDataExeFS}" + ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataExeFSToolTip}" /> + <MenuItem + Command="{Binding ExtractRomFs}" + Header="{locale:Locale GameListContextMenuExtractDataRomFS}" + ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataRomFSToolTip}" /> + <MenuItem + Command="{Binding ExtractLogo}" + Header="{locale:Locale GameListContextMenuExtractDataLogo}" + ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" /> + </MenuItem> + </MenuFlyout> + </UserControl.Resources> + <Grid> + <Grid.RowDefinitions> + <RowDefinition Height="*" /> + </Grid.RowDefinitions> + <ListBox Grid.Row="0" + Padding="8" + HorizontalAlignment="Stretch" + DoubleTapped="GameList_DoubleTapped" + SelectionChanged="GameList_SelectionChanged" + ContextFlyout="{StaticResource GameContextMenu}" + VerticalAlignment="Stretch" + Items="{Binding AppsObservableList}"> + <ListBox.ItemsPanel> + <ItemsPanelTemplate> + <flex:FlexPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" JustifyContent="Center" + AlignContent="FlexStart" /> + </ItemsPanelTemplate> + </ListBox.ItemsPanel> + <ListBox.Styles> + <Style Selector="ListBoxItem"> + <Setter Property="Padding" Value="0" /> + <Setter Property="Margin" Value="5" /> + <Setter Property="CornerRadius" Value="5" /> + <Setter Property="Background" Value="{DynamicResource SystemAccentColorDark3}" /> + <Style.Animations> + <Animation Duration="0:0:0.7"> + <KeyFrame Cue="0%"> + <Setter Property="MaxWidth" Value="0"/> + <Setter Property="Opacity" Value="0.0"/> + </KeyFrame> + <KeyFrame Cue="50%"> + <Setter Property="MaxWidth" Value="1000"/> + <Setter Property="Opacity" Value="0.3"/> + </KeyFrame> + <KeyFrame Cue="100%"> + <Setter Property="MaxWidth" Value="1000"/> + <Setter Property="Opacity" Value="1.0"/> + </KeyFrame> + </Animation> + </Style.Animations> + </Style> + </ListBox.Styles> + <ListBox.ItemTemplate> + <DataTemplate> + <Grid> + <Grid.Styles> + <Style Selector="ui|SymbolIcon.small.icon"> + <Setter Property="FontSize" Value="15" /> + </Style> + <Style Selector="ui|SymbolIcon.normal.icon"> + <Setter Property="FontSize" Value="19" /> + </Style> + <Style Selector="ui|SymbolIcon.large.icon"> + <Setter Property="FontSize" Value="23" /> + </Style> + <Style Selector="ui|SymbolIcon.huge.icon"> + <Setter Property="FontSize" Value="26" /> + </Style> + </Grid.Styles> + <Border + Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}" + Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}" + Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}" + Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}" + HorizontalAlignment="Stretch" + Padding="{Binding $parent[UserControl].DataContext.GridItemPadding}" CornerRadius="5" + VerticalAlignment="Stretch" Margin="0" ClipToBounds="True"> + <Grid Margin="0"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <Image HorizontalAlignment="Stretch" VerticalAlignment="Top" Margin="0" Grid.Row="0" + Source="{Binding Icon, Converter={StaticResource ByteImage}}" /> + <StackPanel IsVisible="{Binding $parent[UserControl].DataContext.ShowNames}" + Height="50" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" + Margin="5" Grid.Row="1"> + <TextBlock Text="{Binding TitleName}" TextAlignment="Center" TextWrapping="Wrap" + HorizontalAlignment="Stretch" /> + </StackPanel> + </Grid> + </Border> + <ui:SymbolIcon Classes.icon="true" Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}" + Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}" + Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}" + Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}" + Foreground="Yellow" Symbol="StarFilled" + IsVisible="{Binding Favorite}" Margin="5" VerticalAlignment="Top" + HorizontalAlignment="Left" /> + <ui:SymbolIcon Classes.icon="true" Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}" + Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}" + Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}" + Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}" + Foreground="Black" Symbol="Star" + IsVisible="{Binding Favorite}" Margin="5" VerticalAlignment="Top" + HorizontalAlignment="Left" /> + </Grid> + </DataTemplate> + </ListBox.ItemTemplate> + </ListBox> + </Grid> +</UserControl>
\ 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<ApplicationOpenedEventArgs> ApplicationOpenedEvent = + RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble); + + public event EventHandler<ApplicationOpenedEventArgs> 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<object>)[2] as MenuItem).IsEnabled = canHaveUserSave; + ((menu.Items as AvaloniaList<object>)[3] as MenuItem).IsEnabled = canHaveDeviceSave; + ((menu.Items as AvaloniaList<object>)[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 @@ +<UserControl xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" + xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + x:Class="Ryujinx.Ava.Ui.Controls.GameListView"> + <UserControl.Resources> + <controls:BitmapArrayValueConverter x:Key="ByteImage" /> + <MenuFlyout x:Key="GameContextMenu" Opened="MenuBase_OnMenuOpened"> + <MenuItem + Command="{Binding ToggleFavorite}" + Header="{locale:Locale GameListContextMenuToggleFavorite}" + ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" /> + <Separator /> + <MenuItem + Command="{Binding OpenUserSaveDirectory}" + Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}" + ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" /> + <MenuItem + Command="{Binding OpenDeviceSaveDirectory}" + Header="{locale:Locale GameListContextMenuOpenUserDeviceDirectory}" + ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserDeviceDirectoryToolTip}" /> + <MenuItem + Command="{Binding OpenBcatSaveDirectory}" + Header="{locale:Locale GameListContextMenuOpenUserBcatDirectory}" + ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserBcatDirectoryToolTip}" /> + <Separator /> + <MenuItem + Command="{Binding OpenTitleUpdateManager}" + Header="{locale:Locale GameListContextMenuManageTitleUpdates}" + ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" /> + <MenuItem + Command="{Binding OpenDlcManager}" + Header="{locale:Locale GameListContextMenuManageDlc}" + ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" /> + <MenuItem + Command="{Binding OpenCheatManager}" + Header="{locale:Locale GameListContextMenuManageCheat}" + ToolTip.Tip="{locale:Locale GameListContextMenuManageCheatToolTip}" /> + <MenuItem + Command="{Binding OpenModsDirectory}" + Header="{locale:Locale GameListContextMenuOpenModsDirectory}" + ToolTip.Tip="{locale:Locale GameListContextMenuOpenModsDirectoryToolTip}" /> + <MenuItem + Command="{Binding OpenSdModsDirectory}" + Header="{locale:Locale GameListContextMenuOpenSdModsDirectory}" + ToolTip.Tip="{locale:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" /> + <Separator /> + <MenuItem Header="{locale:Locale GameListContextMenuCacheManagement}"> + <MenuItem + Command="{Binding PurgePtcCache}" + Header="{locale:Locale GameListContextMenuCacheManagementPurgePptc}" + ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgePptcToolTip}" /> + <MenuItem + Command="{Binding PurgeShaderCache}" + Header="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCache}" + ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCacheToolTip}" /> + <MenuItem + Command="{Binding OpenPtcDirectory}" + Header="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectory}" + ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectoryToolTip}" /> + <MenuItem + Command="{Binding OpenShaderCacheDirectory}" + Header="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectory}" + ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip}" /> + </MenuItem> + <MenuItem Header="{locale:Locale GameListContextMenuExtractData}"> + <MenuItem + Command="{Binding ExtractExeFs}" + Header="{locale:Locale GameListContextMenuExtractDataExeFS}" + ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataExeFSToolTip}" /> + <MenuItem + Command="{Binding ExtractRomFs}" + Header="{locale:Locale GameListContextMenuExtractDataRomFS}" + ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataRomFSToolTip}" /> + <MenuItem + Command="{Binding ExtractLogo}" + Header="{locale:Locale GameListContextMenuExtractDataLogo}" + ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" /> + </MenuItem> + </MenuFlyout> + </UserControl.Resources> + <Grid> + <Grid.RowDefinitions> + <RowDefinition Height="*" /> + </Grid.RowDefinitions> + <ListBox Grid.Row="0" + Padding="8" + HorizontalAlignment="Stretch" + DoubleTapped="GameList_DoubleTapped" + SelectionChanged="GameList_SelectionChanged" + ContextFlyout="{StaticResource GameContextMenu}" + VerticalAlignment="Stretch" + Name="GameListBox" + Items="{Binding AppsObservableList}"> + <ListBox.ItemsPanel> + <ItemsPanelTemplate> + <StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Orientation="Vertical" Spacing="2" /> + </ItemsPanelTemplate> + </ListBox.ItemsPanel> + <ListBox.Styles> + <Style Selector="ListBoxItem"> + <Setter Property="Padding" Value="0" /> + <Setter Property="Margin" Value="0" /> + <Setter Property="CornerRadius" Value="5" /> + <Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColorDark3}" /> + <Setter Property="BorderThickness" Value="2" /> + <Style.Animations> + <Animation Duration="0:0:0.7"> + <KeyFrame Cue="0%"> + <Setter Property="MaxHeight" Value="0"/> + <Setter Property="Opacity" Value="0.0"/> + </KeyFrame> + <KeyFrame Cue="50%"> + <Setter Property="MaxHeight" Value="1000"/> + <Setter Property="Opacity" Value="0.3"/> + </KeyFrame> + <KeyFrame Cue="100%"> + <Setter Property="MaxHeight" Value="1000"/> + <Setter Property="Opacity" Value="1.0"/> + </KeyFrame> + </Animation> + </Style.Animations> + </Style> + </ListBox.Styles> + <ListBox.ItemTemplate> + <DataTemplate> + <Grid> + <Border HorizontalAlignment="Stretch" + Padding="10" CornerRadius="5" + VerticalAlignment="Stretch" Margin="0" ClipToBounds="True"> + <Grid > + <Grid.ColumnDefinitions> + <ColumnDefinition Width="Auto"/> + <ColumnDefinition Width="10"/> + <ColumnDefinition Width="*"/> + <ColumnDefinition Width="Auto"/> + </Grid.ColumnDefinitions> + <Grid.RowDefinitions> + <RowDefinition/> + </Grid.RowDefinitions> + <Image + Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}" + Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}" + Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}" + Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}" + Grid.RowSpan="3" Grid.Column="0" Margin="0" + Source="{Binding Icon, Converter={StaticResource ByteImage}}" /> + <StackPanel Orientation="Vertical" Spacing="5" VerticalAlignment="Top" HorizontalAlignment="Left" + Grid.Column="2"> + <TextBlock Text="{Binding TitleName}" TextAlignment="Left" TextWrapping="Wrap" + HorizontalAlignment="Stretch" /> + <TextBlock Text="{Binding Developer}" TextAlignment="Left" TextWrapping="Wrap" + HorizontalAlignment="Stretch" /> + <TextBlock Text="{Binding Version}" TextAlignment="Left" TextWrapping="Wrap" + HorizontalAlignment="Stretch" /> + </StackPanel> + <StackPanel Orientation="Vertical" Spacing="5" VerticalAlignment="Top" HorizontalAlignment="Right" + Grid.Column="3"> + <TextBlock Text="{Binding TimePlayed}" TextAlignment="Right" TextWrapping="Wrap" + HorizontalAlignment="Stretch" /> + <TextBlock Text="{Binding LastPlayed}" TextAlignment="Right" TextWrapping="Wrap" + HorizontalAlignment="Stretch" /> + <TextBlock Text="{Binding FileSize}" TextAlignment="Right" TextWrapping="Wrap" + HorizontalAlignment="Stretch" /> + </StackPanel> + <ui:SymbolIcon Grid.Row="0" Grid.Column="0" FontSize="20" + Foreground="Yellow" + Symbol="StarFilled" + IsVisible="{Binding Favorite}" Margin="-5, -5, 0, 0" VerticalAlignment="Top" + HorizontalAlignment="Left" /> + <ui:SymbolIcon Grid.Row="0" Grid.Column="0" FontSize="20" + Foreground="Black" + Symbol="Star" + IsVisible="{Binding Favorite}" Margin="-5, -5, 0, 0" VerticalAlignment="Top" + HorizontalAlignment="Left" /> + </Grid> + </Border> + </Grid> + </DataTemplate> + </ListBox.ItemTemplate> + </ListBox> + </Grid> +</UserControl>
\ 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<ApplicationOpenedEventArgs> ApplicationOpenedEvent = + RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble); + + public event EventHandler<ApplicationOpenedEventArgs> 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<object>)[2] as MenuItem).IsEnabled = canHaveUserSave; + ((menu.Items as AvaloniaList<object>)[3] as MenuItem).IsEnabled = canHaveDeviceSave; + ((menu.Items as AvaloniaList<object>)[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<Glyph, string> _glyphs = new Dictionary<Glyph, string> + { + { 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<Glyph>(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<object> CommandParameterProperty = + AvaloniaProperty.Register<HotKeyControl, object>(nameof(CommandParameter)); + + public static readonly DirectProperty<HotKeyControl, ICommand> CommandProperty = + AvaloniaProperty.RegisterDirect<HotKeyControl, ICommand>(nameof(Command), + control => control.Command, (control, command) => control.Command = command, enableDataValidation: true); + + public static readonly StyledProperty<KeyGesture> HotKeyProperty = HotKeyManager.HotKeyProperty.AddOwner<Button>(); + + private ICommand _command; + private bool _commandCanExecute; + + public ICommand Command + { + get { return _command; } + set { SetAndRaise(CommandProperty, ref _command, value); } + } + + public KeyGesture HotKey + { + get { return GetValue(HotKeyProperty); } + set { SetValue(HotKeyProperty, value); } + } + + public object CommandParameter + { + get { return GetValue(CommandParameterProperty); } + set { SetValue(CommandParameterProperty, value); } + } + + public void CanExecuteChanged(object sender, EventArgs e) + { + var canExecute = Command == null || Command.CanExecute(CommandParameter); + + if (canExecute != _commandCanExecute) + { + _commandCanExecute = canExecute; + UpdateIsEffectivelyEnabled(); + } + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/IGlContextExtension.cs b/Ryujinx.Ava/Ui/Controls/IGlContextExtension.cs new file mode 100644 index 00000000..4ca5bd59 --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/IGlContextExtension.cs @@ -0,0 +1,25 @@ +using Avalonia.OpenGL; +using SPB.Graphics.OpenGL; +using System; + +namespace Ryujinx.Ava.Ui.Controls +{ + public static class IGlContextExtension + { + public static OpenGLContextBase AsOpenGLContextBase(this IGlContext context) + { + var handle = (IntPtr)context.GetType().GetProperty("Handle").GetValue(context); + + if (OperatingSystem.IsWindows()) + { + return new AvaloniaWglContext(handle); + } + else if (OperatingSystem.IsLinux()) + { + return new AvaloniaGlxContext(handle); + } + + return null; + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/InputDialog.axaml b/Ryujinx.Ava/Ui/Controls/InputDialog.axaml new file mode 100644 index 00000000..6f320301 --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/InputDialog.axaml @@ -0,0 +1,18 @@ +<UserControl xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + mc:Ignorable="d" + x:Class="Ryujinx.Ava.Ui.Controls.InputDialog"> + <Grid HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="5,10,5, 5"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <TextBlock HorizontalAlignment="Center" Text="{Binding Message}" /> + <TextBox MaxLength="{Binding MaxLength}" Grid.Row="1" Margin="10" Width="300" HorizontalAlignment="Center" + Text="{Binding Input, Mode=TwoWay}" /> + <TextBlock Grid.Row="2" Margin="5, 5, 5, 10" HorizontalAlignment="Center" Text="{Binding SubMessage}" /> + </Grid> +</UserControl>
\ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/InputDialog.axaml.cs b/Ryujinx.Ava/Ui/Controls/InputDialog.axaml.cs new file mode 100644 index 00000000..b9bbb66d --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/InputDialog.axaml.cs @@ -0,0 +1,67 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Ui.Models; +using Ryujinx.Ava.Ui.Windows; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.Ui.Controls +{ + public class InputDialog : UserControl + { + public string Message { get; set; } + public string Input { get; set; } + public string SubMessage { get; set; } + + public uint MaxLength { get; } + + public InputDialog(string message, string input = "", string subMessage = "", uint maxLength = int.MaxValue) + { + Message = message; + Input = input; + SubMessage = subMessage; + MaxLength = maxLength; + + DataContext = this; + + InitializeComponent(); + } + + public InputDialog() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public static async Task<(UserResult Result, string Input)> ShowInputDialog(StyleableWindow window, string title, string message, string input = "", string subMessage = "", uint maxLength = int.MaxValue) + { + ContentDialog contentDialog = window.ContentDialog; + + UserResult result = UserResult.Cancel; + + InputDialog content = new InputDialog(message, input = "", subMessage = "", maxLength); + + if (contentDialog != null) + { + contentDialog.Title = title; + contentDialog.PrimaryButtonText = LocaleManager.Instance["InputDialogOk"]; + contentDialog.SecondaryButtonText = ""; + contentDialog.CloseButtonText = LocaleManager.Instance["InputDialogCancel"]; + contentDialog.Content = content; + contentDialog.PrimaryButtonCommand = MiniCommand.Create(() => + { + result = UserResult.Ok; + input = content.Input; + }); + await contentDialog.ShowAsync(); + } + + return (result, input); + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/KeyValueConverter.cs b/Ryujinx.Ava/Ui/Controls/KeyValueConverter.cs new file mode 100644 index 00000000..8b3a30e6 --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/KeyValueConverter.cs @@ -0,0 +1,46 @@ +using Avalonia.Data.Converters; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using System; +using System.Globalization; + +namespace Ryujinx.Ava.Ui.Controls +{ + public class KeyValueConverter : IValueConverter + { + public static KeyValueConverter Instance = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null) + { + return null; + } + + return value.ToString(); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + object key = null; + + if (value != null) + { + if (targetType == typeof(Key)) + { + key = Enum.Parse<Key>(value.ToString()); + } + else if (targetType == typeof(GamepadInputId)) + { + key = Enum.Parse<GamepadInputId>(value.ToString()); + } + else if (targetType == typeof(StickInputId)) + { + key = Enum.Parse<StickInputId>(value.ToString()); + } + } + + return key; + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/MiniCommand.cs b/Ryujinx.Ava/Ui/Controls/MiniCommand.cs new file mode 100644 index 00000000..e14cfa6f --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/MiniCommand.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace Ryujinx.Ava.Ui.Models +{ + public sealed class MiniCommand<T> : MiniCommand, ICommand + { + private readonly Action<T> _callback; + private bool _busy; + private Func<T, Task> _asyncCallback; + + public MiniCommand(Action<T> callback) + { + _callback = callback; + } + + public MiniCommand(Func<T, Task> callback) + { + _asyncCallback = callback; + } + + private bool Busy + { + get => _busy; + set + { + _busy = value; + CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } + } + + public override event EventHandler CanExecuteChanged; + public override bool CanExecute(object parameter) => !_busy; + + public override async void Execute(object parameter) + { + if (Busy) + { + return; + } + try + { + Busy = true; + if (_callback != null) + { + _callback((T)parameter); + } + else + { + await _asyncCallback((T)parameter); + } + } + finally + { + Busy = false; + } + } + } + + public abstract class MiniCommand : ICommand + { + public static MiniCommand Create(Action callback) => new MiniCommand<object>(_ => callback()); + public static MiniCommand Create<TArg>(Action<TArg> callback) => new MiniCommand<TArg>(callback); + public static MiniCommand CreateFromTask(Func<Task> callback) => new MiniCommand<object>(_ => callback()); + + public abstract bool CanExecute(object parameter); + public abstract void Execute(object parameter); + public abstract event EventHandler CanExecuteChanged; + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/OffscreenTextBox.cs b/Ryujinx.Ava/Ui/Controls/OffscreenTextBox.cs new file mode 100644 index 00000000..ffe5bdde --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/OffscreenTextBox.cs @@ -0,0 +1,40 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace Ryujinx.Ava.Ui.Controls +{ + public class OffscreenTextBox : TextBox + { + public RoutedEvent<KeyEventArgs> GetKeyDownRoutedEvent() + { + return KeyDownEvent; + } + + public RoutedEvent<KeyEventArgs> GetKeyUpRoutedEvent() + { + return KeyUpEvent; + } + + public void SendKeyDownEvent(KeyEventArgs keyEvent) + { + OnKeyDown(keyEvent); + } + + public void SendKeyUpEvent(KeyEventArgs keyEvent) + { + OnKeyUp(keyEvent); + } + + public void SendText(string text) + { + OnTextInput(new TextInputEventArgs() + { + Text = text, + Device = KeyboardDevice.Instance, + Source = this, + RoutedEvent = TextInputEvent + }); + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/OpenToolkitBindingsContext.cs b/Ryujinx.Ava/Ui/Controls/OpenToolkitBindingsContext.cs new file mode 100644 index 00000000..a1baba9e --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/OpenToolkitBindingsContext.cs @@ -0,0 +1,20 @@ +using OpenTK; +using System; + +namespace Ryujinx.Ava.Ui.Controls +{ + public class OpenToolkitBindingsContext : IBindingsContext + { + private readonly Func<string, IntPtr> _getProcAddress; + + public OpenToolkitBindingsContext(Func<string, IntPtr> getProcAddress) + { + _getProcAddress = getProcAddress; + } + + public IntPtr GetProcAddress(string procName) + { + return _getProcAddress(procName); + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/RenderTimer.cs b/Ryujinx.Ava/Ui/Controls/RenderTimer.cs new file mode 100644 index 00000000..577115ea --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/RenderTimer.cs @@ -0,0 +1,100 @@ +using Avalonia.Rendering; +using System; +using System.Threading; +using System.Timers; + +namespace Ryujinx.Ava.Ui.Controls +{ + internal class RenderTimer : IRenderTimer, IDisposable + { + public event Action<TimeSpan> Tick + { + add + { + _tick += value; + + if (_subscriberCount++ == 0) + { + Start(); + } + } + + remove + { + if (--_subscriberCount == 0) + { + Stop(); + } + + _tick -= value; + } + } + + private Thread _tickThread; + private readonly System.Timers.Timer _timer; + + private Action<TimeSpan> _tick; + private int _subscriberCount; + + private bool _isRunning; + + private AutoResetEvent _resetEvent; + + public RenderTimer() + { + _timer = new System.Timers.Timer(15); + _resetEvent = new AutoResetEvent(true); + _timer.Elapsed += Timer_Elapsed; + } + + private void Timer_Elapsed(object sender, ElapsedEventArgs e) + { + TickNow(); + } + + public void Start() + { + _timer.Start(); + if (_tickThread == null) + { + _tickThread = new Thread(RunTick); + _tickThread.Name = "RenderTimerTickThread"; + _tickThread.IsBackground = true; + _isRunning = true; + _tickThread.Start(); + } + } + + public void RunTick() + { + while (_isRunning) + { + _resetEvent.WaitOne(); + _tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount)); + } + } + + public void TickNow() + { + lock (_timer) + { + _resetEvent.Set(); + } + } + + public void Stop() + { + _timer.Stop(); + } + + public void Dispose() + { + _timer.Elapsed -= Timer_Elapsed; + _timer.Stop(); + _isRunning = false; + _resetEvent.Set(); + _tickThread.Join(); + _resetEvent.Dispose(); + } + } +} diff --git a/Ryujinx.Ava/Ui/Controls/RendererControl.cs b/Ryujinx.Ava/Ui/Controls/RendererControl.cs new file mode 100644 index 00000000..8321a04e --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/RendererControl.cs @@ -0,0 +1,273 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Media; +using Avalonia.OpenGL; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Skia; +using Avalonia.Threading; +using OpenTK.Graphics.OpenGL; +using Ryujinx.Common.Configuration; +using SkiaSharp; +using SPB.Graphics; +using SPB.Graphics.OpenGL; +using SPB.Platform; +using SPB.Windowing; +using System; + +namespace Ryujinx.Ava.Ui.Controls +{ + public class RendererControl : Control + { + private int _image; + + static RendererControl() + { + AffectsRender<RendererControl>(ImageProperty); + } + + public readonly static StyledProperty<int> ImageProperty = + AvaloniaProperty.Register<RendererControl, int>(nameof(Image), 0, inherits: true, defaultBindingMode: BindingMode.TwoWay); + + protected int Image + { + get => _image; + set => SetAndRaise(ImageProperty, ref _image, value); + } + + public event EventHandler<EventArgs> GlInitialized; + public event EventHandler<Size> SizeChanged; + + protected Size RenderSize { get; private set; } + public bool IsStarted { get; private set; } + + public int Major { get; } + public int Minor { get; } + public GraphicsDebugLevel DebugLevel { get; } + public OpenGLContextBase GameContext { get; set; } + + public static OpenGLContextBase PrimaryContext => AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>().PrimaryContext.AsOpenGLContextBase(); + + private SwappableNativeWindowBase _gameBackgroundWindow; + + private bool _isInitialized; + + private IntPtr _fence; + + private GlDrawOperation _glDrawOperation; + + public RendererControl(int major, int minor, GraphicsDebugLevel graphicsDebugLevel) + { + Major = major; + Minor = minor; + DebugLevel = graphicsDebugLevel; + IObservable<Rect> resizeObservable = this.GetObservable(BoundsProperty); + + resizeObservable.Subscribe(Resized); + + Focusable = true; + } + + private void Resized(Rect rect) + { + SizeChanged?.Invoke(this, rect.Size); + + RenderSize = rect.Size * Program.WindowScaleFactor; + + _glDrawOperation?.Dispose(); + _glDrawOperation = new GlDrawOperation(this); + } + + public override void Render(DrawingContext context) + { + if (!_isInitialized) + { + CreateWindow(); + + OnGlInitialized(); + _isInitialized = true; + } + + if (GameContext == null || !IsStarted || Image == 0) + { + return; + } + + if (_glDrawOperation != null) + { + context.Custom(_glDrawOperation); + } + + base.Render(context); + } + + protected void OnGlInitialized() + { + GlInitialized?.Invoke(this, EventArgs.Empty); + } + + public void QueueRender() + { + Program.RenderTimer.TickNow(); + } + + internal void Present(object image) + { + Dispatcher.UIThread.InvokeAsync(() => + { + Image = (int)image; + }).Wait(); + + if (_fence != IntPtr.Zero) + { + GL.DeleteSync(_fence); + } + + _fence = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None); + + QueueRender(); + + _gameBackgroundWindow.SwapBuffers(); + } + + internal void Start() + { + IsStarted = true; + QueueRender(); + } + + internal void Stop() + { + IsStarted = false; + } + + public void DestroyBackgroundContext() + { + _image = 0; + + if (_fence != IntPtr.Zero) + { + _glDrawOperation.Dispose(); + GL.DeleteSync(_fence); + } + + GlDrawOperation.DeleteFramebuffer(); + + GameContext?.Dispose(); + + _gameBackgroundWindow?.Dispose(); + } + + internal void MakeCurrent() + { + GameContext.MakeCurrent(_gameBackgroundWindow); + } + + internal void MakeCurrent(SwappableNativeWindowBase window) + { + GameContext.MakeCurrent(window); + } + + protected void CreateWindow() + { + var flags = OpenGLContextFlags.Compat; + if (DebugLevel != GraphicsDebugLevel.None) + { + flags |= OpenGLContextFlags.Debug; + } + _gameBackgroundWindow = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100); + _gameBackgroundWindow.Hide(); + + GameContext = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, Major, Minor, flags, shareContext: PrimaryContext); + GameContext.Initialize(_gameBackgroundWindow); + } + + private class GlDrawOperation : ICustomDrawOperation + { + private static int _framebuffer; + + public Rect Bounds { get; } + + private readonly RendererControl _control; + + public GlDrawOperation(RendererControl control) + { + _control = control; + Bounds = _control.Bounds; + } + + public void Dispose() { } + + public static void DeleteFramebuffer() + { + if (_framebuffer == 0) + { + GL.DeleteFramebuffer(_framebuffer); + } + + _framebuffer = 0; + } + + public bool Equals(ICustomDrawOperation other) + { + return other is GlDrawOperation operation && Equals(this, operation) && operation.Bounds == Bounds; + } + + public bool HitTest(Point p) + { + return Bounds.Contains(p); + } + + private void CreateRenderTarget() + { + _framebuffer = GL.GenFramebuffer(); + } + + public void Render(IDrawingContextImpl context) + { + if (_control.Image == 0) + { + return; + } + + if (_framebuffer == 0) + { + CreateRenderTarget(); + } + + int currentFramebuffer = GL.GetInteger(GetPName.FramebufferBinding); + + var image = _control.Image; + var fence = _control._fence; + + GL.BindFramebuffer(FramebufferTarget.Framebuffer, _framebuffer); + GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, image, 0); + GL.BindFramebuffer(FramebufferTarget.Framebuffer, currentFramebuffer); + + if (context is not ISkiaDrawingContextImpl skiaDrawingContextImpl) + { + return; + } + + var imageInfo = new SKImageInfo((int)_control.RenderSize.Width, (int)_control.RenderSize.Height, SKColorType.Rgba8888); + var glInfo = new GRGlFramebufferInfo((uint)_framebuffer, SKColorType.Rgba8888.ToGlSizedFormat()); + + GL.WaitSync(fence, WaitSyncFlags.None, ulong.MaxValue); + + using var backendTexture = new GRBackendRenderTarget(imageInfo.Width, imageInfo.Height, 1, 0, glInfo); + using var surface = SKSurface.Create(skiaDrawingContextImpl.GrContext, backendTexture, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888); + + if (surface == null) + { + return; + } + + var rect = new Rect(new Point(), _control.RenderSize); + + using var snapshot = surface.Snapshot(); + skiaDrawingContextImpl.SkCanvas.DrawImage(snapshot, rect.ToSKRect(), _control.Bounds.ToSKRect(), new SKPaint()); + } + } + } +} diff --git a/Ryujinx.Ava/Ui/Controls/SPBOpenGLContext.cs b/Ryujinx.Ava/Ui/Controls/SPBOpenGLContext.cs new file mode 100644 index 00000000..45c6187e --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/SPBOpenGLContext.cs @@ -0,0 +1,47 @@ +using OpenTK.Graphics.OpenGL; +using Ryujinx.Graphics.OpenGL; +using SPB.Graphics; +using SPB.Graphics.OpenGL; +using SPB.Platform; +using SPB.Windowing; + +namespace Ryujinx.Ava.Ui.Controls +{ + class SPBOpenGLContext : IOpenGLContext + { + private OpenGLContextBase _context; + private NativeWindowBase _window; + + private SPBOpenGLContext(OpenGLContextBase context, NativeWindowBase window) + { + _context = context; + _window = window; + } + + public void Dispose() + { + _context.Dispose(); + _window.Dispose(); + } + + public void MakeCurrent() + { + _context.MakeCurrent(_window); + } + + public static SPBOpenGLContext CreateBackgroundContext(OpenGLContextBase sharedContext) + { + OpenGLContextBase context = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, 3, 3, OpenGLContextFlags.Compat, true, sharedContext); + NativeWindowBase window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100); + + context.Initialize(window); + context.MakeCurrent(window); + + GL.LoadBindings(new OpenToolkitBindingsContext(context.GetProcAddress)); + + context.MakeCurrent(null); + + return new SPBOpenGLContext(context, window); + } + } +} diff --git a/Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml b/Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml new file mode 100644 index 00000000..24aded24 --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml @@ -0,0 +1,28 @@ +<Window xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows" + mc:Ignorable="d" + x:Class="Ryujinx.Ava.Ui.Controls.UpdateWaitWindow" + WindowStartupLocation="CenterOwner" + SizeToContent="WidthAndHeight" + Title="Ryujinx - Waiting"> + <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="20"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition /> + </Grid.ColumnDefinitions> + <Image Grid.Row="1" Margin="5, 10, 20 , 10" Source="resm:Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.Ui.Common" + Height="70" + MinWidth="50" /> + <StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Orientation="Vertical"> + <TextBlock Margin="5" Name="PrimaryText" /> + <TextBlock VerticalAlignment="Center" Name="SecondaryText" Margin="5" /> + </StackPanel> + </Grid> +</Window>
\ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml.cs b/Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml.cs new file mode 100644 index 00000000..e4108ba4 --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml.cs @@ -0,0 +1,35 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Ryujinx.Ava.Ui.Windows; + +namespace Ryujinx.Ava.Ui.Controls +{ + public class UpdateWaitWindow : StyleableWindow + { + public UpdateWaitWindow(string primaryText, string secondaryText) : this() + { + PrimaryText.Text = primaryText; + SecondaryText.Text = secondaryText; + WindowStartupLocation = WindowStartupLocation.CenterOwner; + } + + public UpdateWaitWindow() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + public TextBlock PrimaryText { get; private set; } + public TextBlock SecondaryText { get; private set; } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + PrimaryText = this.FindControl<TextBlock>("PrimaryText"); + SecondaryText = this.FindControl<TextBlock>("SecondaryText"); + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/UserErrorDialog.cs b/Ryujinx.Ava/Ui/Controls/UserErrorDialog.cs new file mode 100644 index 00000000..1f8f68e3 --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/UserErrorDialog.cs @@ -0,0 +1,91 @@ +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Ui.Windows; +using Ryujinx.Ui.Common; +using Ryujinx.Ui.Common.Helper; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.Ui.Controls +{ + internal class UserErrorDialog + { + private const string SetupGuideUrl = "https://github.com/Ryujinx/Ryujinx/wiki/Ryujinx-Setup-&-Configuration-Guide"; + + private static string GetErrorCode(UserError error) + { + return $"RYU-{(uint)error:X4}"; + } + + private static string GetErrorTitle(UserError error) + { + return error switch + { + UserError.NoKeys => LocaleManager.Instance["UserErrorNoKeys"], + UserError.NoFirmware => LocaleManager.Instance["UserErrorNoFirmware"], + UserError.FirmwareParsingFailed => LocaleManager.Instance["UserErrorFirmwareParsingFailed"], + UserError.ApplicationNotFound => LocaleManager.Instance["UserErrorApplicationNotFound"], + UserError.Unknown => LocaleManager.Instance["UserErrorUnknown"], + _ => LocaleManager.Instance["UserErrorUndefined"] + }; + } + + private static string GetErrorDescription(UserError error) + { + return error switch + { + UserError.NoKeys => LocaleManager.Instance["UserErrorNoKeysDescription"], + UserError.NoFirmware => LocaleManager.Instance["UserErrorNoFirmwareDescription"], + UserError.FirmwareParsingFailed => LocaleManager.Instance["UserErrorFirmwareParsingFailedDescription"], + UserError.ApplicationNotFound => LocaleManager.Instance["UserErrorApplicationNotFoundDescription"], + UserError.Unknown => LocaleManager.Instance["UserErrorUnknownDescription"], + _ => LocaleManager.Instance["UserErrorUndefinedDescription"] + }; + } + + private static bool IsCoveredBySetupGuide(UserError error) + { + return error switch + { + UserError.NoKeys or + UserError.NoFirmware or + UserError.FirmwareParsingFailed => true, + _ => false + }; + } + + private static string GetSetupGuideUrl(UserError error) + { + if (!IsCoveredBySetupGuide(error)) + { + return null; + } + + return error switch + { + UserError.NoKeys => SetupGuideUrl + "#initial-setup---placement-of-prodkeys", + UserError.NoFirmware => SetupGuideUrl + "#initial-setup-continued---installation-of-firmware", + _ => SetupGuideUrl + }; + } + + public static async Task ShowUserErrorDialog(UserError error, StyleableWindow owner) + { + string errorCode = GetErrorCode(error); + + bool isInSetupGuide = IsCoveredBySetupGuide(error); + + string setupButtonLabel = isInSetupGuide ? LocaleManager.Instance["OpenSetupGuideMessage"] : ""; + + var result = await ContentDialogHelper.CreateInfoDialog(owner, + string.Format(LocaleManager.Instance["DialogUserErrorDialogMessage"], errorCode, GetErrorTitle(error)), + GetErrorDescription(error) + (isInSetupGuide + ? LocaleManager.Instance["DialogUserErrorDialogInfoMessage"] + : ""), setupButtonLabel, LocaleManager.Instance["InputDialogOk"], + string.Format(LocaleManager.Instance["DialogUserErrorDialogTitle"], errorCode)); + + if (result == UserResult.Ok) + { + OpenHelper.OpenUrl(GetSetupGuideUrl(error)); + } + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/UserResult.cs b/Ryujinx.Ava/Ui/Controls/UserResult.cs new file mode 100644 index 00000000..6eb89a90 --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/UserResult.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.Ava.Ui.Controls +{ + public enum UserResult + { + Ok, + Yes, + No, + Abort, + Cancel, + None, + } +}
\ No newline at end of file |
