From 6e02cac952f1a9a34d2777199dde657eba0784e6 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Sun, 24 Jul 2022 17:38:38 +0000 Subject: Avalonia - Use content dialog for user profile manager (#3455) * remove content dialog placeholder from all windows * remove redundant window argument * redesign user profile window * wip * use avalonia auto name generator * add edit and new user options * move profile image selection to content dialog * remove usings * fix updater * address review * adjust avatar dialog size * add validation for user editor * fix typo * Shorten some labels --- Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs | 127 ++++++++------------- Ryujinx.Ava/Ui/Controls/InputDialog.axaml.cs | 37 +++--- Ryujinx.Ava/Ui/Controls/NavigationDialogHost.axaml | 10 ++ .../Ui/Controls/NavigationDialogHost.axaml.cs | 85 ++++++++++++++ .../Ui/Controls/ProfileImageSelectionDialog.axaml | 10 +- .../Controls/ProfileImageSelectionDialog.axaml.cs | 70 ++++++------ Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml.cs | 12 +- Ryujinx.Ava/Ui/Controls/UserEditor.axaml | 55 +++++++++ Ryujinx.Ava/Ui/Controls/UserEditor.axaml.cs | 123 ++++++++++++++++++++ Ryujinx.Ava/Ui/Controls/UserErrorDialog.cs | 2 +- Ryujinx.Ava/Ui/Controls/UserSelector.axaml | 90 +++++++++++++++ Ryujinx.Ava/Ui/Controls/UserSelector.axaml.cs | 79 +++++++++++++ 12 files changed, 542 insertions(+), 158 deletions(-) create mode 100644 Ryujinx.Ava/Ui/Controls/NavigationDialogHost.axaml create mode 100644 Ryujinx.Ava/Ui/Controls/NavigationDialogHost.axaml.cs create mode 100644 Ryujinx.Ava/Ui/Controls/UserEditor.axaml create mode 100644 Ryujinx.Ava/Ui/Controls/UserEditor.axaml.cs create mode 100644 Ryujinx.Ava/Ui/Controls/UserSelector.axaml create mode 100644 Ryujinx.Ava/Ui/Controls/UserSelector.axaml.cs (limited to 'Ryujinx.Ava/Ui/Controls') diff --git a/Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs b/Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs index cdc5de93..15ecaa77 100644 --- a/Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs +++ b/Ryujinx.Ava/Ui/Controls/ContentDialogHelper.cs @@ -16,7 +16,6 @@ namespace Ryujinx.Ava.Ui.Controls private static bool _isChoiceDialogOpen; private async static Task ShowContentDialog( - StyleableWindow window, string title, string primaryText, string secondaryText, @@ -28,35 +27,32 @@ namespace Ryujinx.Ava.Ui.Controls { UserResult result = UserResult.None; - ContentDialog contentDialog = window.ContentDialog; + ContentDialog contentDialog = new 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.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; - }); + 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); - }; + await contentDialog.ShowAsync(ContentDialogPlacement.Popup); } return result; @@ -78,35 +74,30 @@ namespace Ryujinx.Ava.Ui.Controls UserResult result = UserResult.None; - ContentDialog contentDialog = window.ContentDialog; - - Window overlay = window; - - if (contentDialog != null) + ContentDialog contentDialog = new ContentDialog { - 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(() => + Title = title, + PrimaryButtonText = primaryButton, + SecondaryButtonText = secondaryButton, + CloseButtonText = closeButton, + Content = CreateDialogTextContent(primaryText, secondaryText, iconSymbol), + PrimaryButtonCommand = MiniCommand.Create(() => { result = primaryButton == LocaleManager.Instance["InputDialogYes"] ? UserResult.Yes : UserResult.Ok; - }); - contentDialog.SecondaryButtonCommand = MiniCommand.Create(() => - { - contentDialog.PrimaryButtonClick -= DeferClose; - result = UserResult.No; - }); - contentDialog.CloseButtonCommand = MiniCommand.Create(() => - { - contentDialog.PrimaryButtonClick -= DeferClose; - result = UserResult.Cancel; - }); - await contentDialog.ShowAsync(ContentDialogPlacement.Popup); + }), }; + contentDialog.SecondaryButtonCommand = MiniCommand.Create(() => + { + contentDialog.PrimaryButtonClick -= DeferClose; + result = UserResult.No; + }); + contentDialog.CloseButtonCommand = MiniCommand.Create(() => + { + contentDialog.PrimaryButtonClick -= DeferClose; + result = UserResult.Cancel; + }); + contentDialog.PrimaryButtonClick += DeferClose; + await contentDialog.ShowAsync(ContentDialogPlacement.Popup); return result; @@ -141,7 +132,7 @@ namespace Ryujinx.Ava.Ui.Controls if (doWhileDeferred != null) { - await doWhileDeferred(overlay); + await doWhileDeferred(window); deferResetEvent.Set(); } @@ -191,7 +182,6 @@ namespace Ryujinx.Ava.Ui.Controls } public static async Task CreateInfoDialog( - StyleableWindow window, string primary, string secondaryText, string acceptButton, @@ -199,7 +189,6 @@ namespace Ryujinx.Ava.Ui.Controls string title) { return await ShowContentDialog( - window, title, primary, secondaryText, @@ -210,7 +199,6 @@ namespace Ryujinx.Ava.Ui.Controls } internal static async Task CreateConfirmationDialog( - StyleableWindow window, string primaryText, string secondaryText, string acceptButtonText, @@ -219,7 +207,6 @@ namespace Ryujinx.Ava.Ui.Controls UserResult primaryButtonResult = UserResult.Yes) { return await ShowContentDialog( - window, string.IsNullOrWhiteSpace(title) ? LocaleManager.Instance["DialogConfirmationTitle"] : title, primaryText, secondaryText, @@ -235,10 +222,9 @@ namespace Ryujinx.Ava.Ui.Controls return new(mainText, secondaryText); } - internal static async Task CreateUpdaterInfoDialog(StyleableWindow window, string primary, string secondaryText) + internal static async Task CreateUpdaterInfoDialog(string primary, string secondaryText) { await ShowContentDialog( - window, LocaleManager.Instance["DialogUpdaterTitle"], primary, secondaryText, @@ -248,24 +234,9 @@ namespace Ryujinx.Ava.Ui.Controls (int)Symbol.Important); } - internal static async Task 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 Task CreateWarningDialog(StyleableWindow window, string primary, string secondaryText) + internal static async Task CreateWarningDialog(string primary, string secondaryText) { await ShowContentDialog( - window, LocaleManager.Instance["DialogWarningTitle"], primary, secondaryText, @@ -275,12 +246,11 @@ namespace Ryujinx.Ava.Ui.Controls (int)Symbol.Important); } - internal static async Task CreateErrorDialog(StyleableWindow owner, string errorMessage, string secondaryErrorMessage = "") + internal static async Task CreateErrorDialog(string errorMessage, string secondaryErrorMessage = "") { Logger.Error?.Print(LogClass.Application, errorMessage); await ShowContentDialog( - owner, LocaleManager.Instance["DialogErrorTitle"], LocaleManager.Instance["DialogErrorMessage"], errorMessage, @@ -290,7 +260,7 @@ namespace Ryujinx.Ava.Ui.Controls (int)Symbol.Dismiss); } - internal static async Task CreateChoiceDialog(StyleableWindow window, string title, string primary, string secondaryText) + internal static async Task CreateChoiceDialog(string title, string primary, string secondaryText) { if (_isChoiceDialogOpen) { @@ -301,7 +271,6 @@ namespace Ryujinx.Ava.Ui.Controls UserResult response = await ShowContentDialog( - window, title, primary, secondaryText, @@ -316,19 +285,17 @@ namespace Ryujinx.Ava.Ui.Controls return response == UserResult.Yes; } - internal static async Task CreateExitDialog(StyleableWindow owner) + internal static async Task CreateExitDialog() { return await CreateChoiceDialog( - owner, LocaleManager.Instance["DialogExitTitle"], LocaleManager.Instance["DialogExitMessage"], LocaleManager.Instance["DialogExitSubMessage"]); } - internal static async Task CreateStopEmulationDialog(StyleableWindow owner) + internal static async Task CreateStopEmulationDialog() { return await CreateChoiceDialog( - owner, LocaleManager.Instance["DialogStopEmulationTitle"], LocaleManager.Instance["DialogStopEmulationMessage"], LocaleManager.Instance["DialogExitSubMessage"]); @@ -338,12 +305,10 @@ namespace Ryujinx.Ava.Ui.Controls string title, string mainText, string subText, - StyleableWindow owner, uint maxLength = int.MaxValue, string input = "") { var result = await InputDialog.ShowInputDialog( - owner, title, mainText, input, diff --git a/Ryujinx.Ava/Ui/Controls/InputDialog.axaml.cs b/Ryujinx.Ava/Ui/Controls/InputDialog.axaml.cs index b9bbb66d..e4b37dec 100644 --- a/Ryujinx.Ava/Ui/Controls/InputDialog.axaml.cs +++ b/Ryujinx.Ava/Ui/Controls/InputDialog.axaml.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; namespace Ryujinx.Ava.Ui.Controls { - public class InputDialog : UserControl + public partial class InputDialog : UserControl { public string Message { get; set; } public string Input { get; set; } @@ -24,8 +24,6 @@ namespace Ryujinx.Ava.Ui.Controls MaxLength = maxLength; DataContext = this; - - InitializeComponent(); } public InputDialog() @@ -33,33 +31,26 @@ namespace Ryujinx.Ava.Ui.Controls InitializeComponent(); } - private void InitializeComponent() + public static async Task<(UserResult Result, string Input)> ShowInputDialog(string title, string message, + string input = "", string subMessage = "", uint maxLength = int.MaxValue) { - 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) + InputDialog content = new InputDialog(message, input, subMessage, maxLength); + ContentDialog contentDialog = new ContentDialog { - contentDialog.Title = title; - contentDialog.PrimaryButtonText = LocaleManager.Instance["InputDialogOk"]; - contentDialog.SecondaryButtonText = ""; - contentDialog.CloseButtonText = LocaleManager.Instance["InputDialogCancel"]; - contentDialog.Content = content; - contentDialog.PrimaryButtonCommand = MiniCommand.Create(() => + Title = title, + PrimaryButtonText = LocaleManager.Instance["InputDialogOk"], + SecondaryButtonText = "", + CloseButtonText = LocaleManager.Instance["InputDialogCancel"], + Content = content, + PrimaryButtonCommand = MiniCommand.Create(() => { result = UserResult.Ok; input = content.Input; - }); - await contentDialog.ShowAsync(); - } + }) + }; + await contentDialog.ShowAsync(); return (result, input); } diff --git a/Ryujinx.Ava/Ui/Controls/NavigationDialogHost.axaml b/Ryujinx.Ava/Ui/Controls/NavigationDialogHost.axaml new file mode 100644 index 00000000..b0ab5d30 --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/NavigationDialogHost.axaml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/NavigationDialogHost.axaml.cs b/Ryujinx.Ava/Ui/Controls/NavigationDialogHost.axaml.cs new file mode 100644 index 00000000..9ba631ad --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/NavigationDialogHost.axaml.cs @@ -0,0 +1,85 @@ +using Avalonia; +using Avalonia.Controls; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Ui.ViewModels; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using System; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.Ui.Controls +{ + public partial class NavigationDialogHost : UserControl + { + public AccountManager AccountManager { get; } + public ContentManager ContentManager { get; } + public UserProfileViewModel ViewModel { get; set; } + + public NavigationDialogHost() + { + InitializeComponent(); + } + + public NavigationDialogHost(AccountManager accountManager, ContentManager contentManager, + VirtualFileSystem virtualFileSystem) + { + AccountManager = accountManager; + ContentManager = contentManager; + ViewModel = new UserProfileViewModel(this); + + + if (contentManager.GetCurrentFirmwareVersion() != null) + { + Task.Run(() => + { + AvatarProfileViewModel.PreloadAvatars(contentManager, virtualFileSystem); + }); + } + InitializeComponent(); + } + + public void GoBack(object parameter = null) + { + if (ContentFrame.BackStack.Count > 0) + { + ContentFrame.GoBack(); + } + + ViewModel.LoadProfiles(); + } + + public void Navigate(Type sourcePageType, object parameter) + { + ContentFrame.Navigate(sourcePageType, parameter); + } + + public static async Task Show(AccountManager ownerAccountManager, ContentManager ownerContentManager, VirtualFileSystem ownerVirtualFileSystem) + { + var content = new NavigationDialogHost(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem); + ContentDialog contentDialog = new ContentDialog + { + Title = LocaleManager.Instance["UserProfileWindowTitle"], + PrimaryButtonText = "", + SecondaryButtonText = "", + CloseButtonText = LocaleManager.Instance["UserProfilesClose"], + Content = content, + Padding = new Thickness(0) + }; + + contentDialog.Closed += (sender, args) => + { + content.ViewModel.Dispose(); + }; + + await contentDialog.ShowAsync(); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + Navigate(typeof(UserSelector), this); + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml b/Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml index c6f43f43..750edf8b 100644 --- a/Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml +++ b/Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml @@ -1,14 +1,10 @@ - + x:Class="Ryujinx.Ava.Ui.Controls.ProfileImageSelectionDialog"> @@ -32,4 +28,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml.cs b/Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml.cs index 728b8906..5d361af9 100644 --- a/Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml.cs +++ b/Ryujinx.Ava/Ui/Controls/ProfileImageSelectionDialog.axaml.cs @@ -1,8 +1,10 @@ -using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; -using Avalonia.Markup.Xaml; +using Avalonia.VisualTree; +using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Navigation; using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Ui.Models; using Ryujinx.Ava.Ui.Windows; using Ryujinx.HLE.FileSystem; using SixLabors.ImageSharp; @@ -12,36 +14,40 @@ using Image = SixLabors.ImageSharp.Image; namespace Ryujinx.Ava.Ui.Controls { - public class ProfileImageSelectionDialog : StyleableWindow + public partial class ProfileImageSelectionDialog : UserControl { - private readonly ContentManager _contentManager; + private ContentManager _contentManager; + private NavigationDialogHost _parent; + private TempProfile _profile; public bool FirmwareFound => _contentManager.GetCurrentFirmwareVersion() != null; - public byte[] BufferImageProfile { get; set; } - - public ProfileImageSelectionDialog(ContentManager contentManager) - { - _contentManager = contentManager; - DataContext = this; - InitializeComponent(); -#if DEBUG - this.AttachDevTools(); -#endif - } - public ProfileImageSelectionDialog() { - DataContext = this; InitializeComponent(); -#if DEBUG - this.AttachDevTools(); -#endif + AddHandler(Frame.NavigatedToEvent, (s, e) => + { + NavigatedTo(e); + }, RoutingStrategies.Direct); } - private void InitializeComponent() + private void NavigatedTo(NavigationEventArgs arg) { - AvaloniaXamlLoader.Load(this); + if (Program.PreviewerDetached) + { + switch (arg.NavigationMode) + { + case NavigationMode.New: + (_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter; + _contentManager = _parent.ContentManager; + break; + case NavigationMode.Back: + _parent.GoBack(); + break; + } + + DataContext = this; + } } private async void Import_OnClick(object sender, RoutedEventArgs e) @@ -58,7 +64,7 @@ namespace Ryujinx.Ava.Ui.Controls dialog.AllowMultiple = false; - string[] image = await dialog.ShowAsync(this); + string[] image = await dialog.ShowAsync(((TopLevel)_parent.GetVisualRoot()) as Window); if (image != null) { @@ -66,28 +72,22 @@ namespace Ryujinx.Ava.Ui.Controls { string imageFile = image[0]; - ProcessProfileImage(File.ReadAllBytes(imageFile)); + _profile.Image = ProcessProfileImage(File.ReadAllBytes(imageFile)); } - Close(); + _parent.GoBack(); } } - private async void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e) + private void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e) { if (FirmwareFound) { - AvatarWindow window = new(_contentManager); - - await window.ShowDialog(this); - - BufferImageProfile = window.SelectedImage; - - Close(); + _parent.Navigate(typeof(AvatarWindow), (_parent, _profile)); } } - private void ProcessProfileImage(byte[] buffer) + private static byte[] ProcessProfileImage(byte[] buffer) { using (Image image = Image.Load(buffer)) { @@ -97,7 +97,7 @@ namespace Ryujinx.Ava.Ui.Controls { image.SaveAsJpeg(streamJpg); - BufferImageProfile = streamJpg.ToArray(); + return streamJpg.ToArray(); } } } diff --git a/Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml.cs b/Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml.cs index e4108ba4..eff15c7b 100644 --- a/Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml.cs +++ b/Ryujinx.Ava/Ui/Controls/UpdateWaitWindow.axaml.cs @@ -5,7 +5,7 @@ using Ryujinx.Ava.Ui.Windows; namespace Ryujinx.Ava.Ui.Controls { - public class UpdateWaitWindow : StyleableWindow + public partial class UpdateWaitWindow : StyleableWindow { public UpdateWaitWindow(string primaryText, string secondaryText) : this() { @@ -21,15 +21,5 @@ namespace Ryujinx.Ava.Ui.Controls this.AttachDevTools(); #endif } - - public TextBlock PrimaryText { get; private set; } - public TextBlock SecondaryText { get; private set; } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - PrimaryText = this.FindControl("PrimaryText"); - SecondaryText = this.FindControl("SecondaryText"); - } } } \ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/UserEditor.axaml b/Ryujinx.Ava/Ui/Controls/UserEditor.axaml new file mode 100644 index 00000000..fed5f8cb --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/UserEditor.axaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + +