diff options
| author | TSR Berry <20988865+TSRBerry@users.noreply.github.com> | 2023-04-08 01:22:00 +0200 |
|---|---|---|
| committer | Mary <thog@protonmail.com> | 2023-04-27 23:51:14 +0200 |
| commit | cee712105850ac3385cd0091a923438167433f9f (patch) | |
| tree | 4a5274b21d8b7f938c0d0ce18736d3f2993b11b1 /src/Ryujinx.Ava/UI/Helpers | |
| parent | cd124bda587ef09668a971fa1cac1c3f0cfc9f21 (diff) | |
Move solution and projects to src
Diffstat (limited to 'src/Ryujinx.Ava/UI/Helpers')
| -rw-r--r-- | src/Ryujinx.Ava/UI/Helpers/ApplicationOpenedEventArgs.cs | 16 | ||||
| -rw-r--r-- | src/Ryujinx.Ava/UI/Helpers/BitmapArrayValueConverter.cs | 35 | ||||
| -rw-r--r-- | src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs | 118 | ||||
| -rw-r--r-- | src/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs | 407 | ||||
| -rw-r--r-- | src/Ryujinx.Ava/UI/Helpers/Glyph.cs | 9 | ||||
| -rw-r--r-- | src/Ryujinx.Ava/UI/Helpers/GlyphValueConverter.cs | 49 | ||||
| -rw-r--r-- | src/Ryujinx.Ava/UI/Helpers/HotKeyControl.cs | 52 | ||||
| -rw-r--r-- | src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs | 46 | ||||
| -rw-r--r-- | src/Ryujinx.Ava/UI/Helpers/LoggerAdapter.cs | 115 | ||||
| -rw-r--r-- | src/Ryujinx.Ava/UI/Helpers/MiniCommand.cs | 71 | ||||
| -rw-r--r-- | src/Ryujinx.Ava/UI/Helpers/NotificationHelper.cs | 65 | ||||
| -rw-r--r-- | src/Ryujinx.Ava/UI/Helpers/OffscreenTextBox.cs | 40 | ||||
| -rw-r--r-- | src/Ryujinx.Ava/UI/Helpers/UserErrorDialog.cs | 91 | ||||
| -rw-r--r-- | src/Ryujinx.Ava/UI/Helpers/UserResult.cs | 12 | ||||
| -rw-r--r-- | src/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs | 123 |
15 files changed, 1249 insertions, 0 deletions
diff --git a/src/Ryujinx.Ava/UI/Helpers/ApplicationOpenedEventArgs.cs b/src/Ryujinx.Ava/UI/Helpers/ApplicationOpenedEventArgs.cs new file mode 100644 index 00000000..ebf5c16e --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/ApplicationOpenedEventArgs.cs @@ -0,0 +1,16 @@ +using Avalonia.Interactivity; +using Ryujinx.Ui.App.Common; + +namespace Ryujinx.Ava.UI.Helpers +{ + public class ApplicationOpenedEventArgs : RoutedEventArgs + { + public ApplicationData Application { get; } + + public ApplicationOpenedEventArgs(ApplicationData application, RoutedEvent routedEvent) + { + Application = application; + RoutedEvent = routedEvent; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Helpers/BitmapArrayValueConverter.cs b/src/Ryujinx.Ava/UI/Helpers/BitmapArrayValueConverter.cs new file mode 100644 index 00000000..3fd368f8 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/BitmapArrayValueConverter.cs @@ -0,0 +1,35 @@ +using Avalonia.Data.Converters; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using System; +using System.Globalization; +using System.IO; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class BitmapArrayValueConverter : IValueConverter + { + public static BitmapArrayValueConverter Instance = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null) + { + return null; + } + + if (value is byte[] buffer && targetType == typeof(IImage)) + { + MemoryStream mem = new(buffer); + return new Bitmap(mem); + } + + throw new NotSupportedException(); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs b/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs new file mode 100644 index 00000000..6730b571 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs @@ -0,0 +1,118 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.LogicalTree; +using Avalonia.Threading; +using Ryujinx.Input; +using Ryujinx.Input.Assigner; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class ButtonKeyAssigner + { + internal class ButtonAssignedEventArgs : EventArgs + { + public ToggleButton Button { get; } + public bool IsAssigned { get; } + + public ButtonAssignedEventArgs(ToggleButton button, bool isAssigned) + { + Button = button; + IsAssigned = isAssigned; + } + } + + public ToggleButton ToggledButton { get; set; } + + private bool _isWaitingForInput; + private bool _shouldUnbind; + public event EventHandler<ButtonAssignedEventArgs> ButtonAssigned; + + public ButtonKeyAssigner(ToggleButton toggleButton) + { + ToggledButton = toggleButton; + } + + public async void GetInputAndAssign(IButtonAssigner assigner, IKeyboard keyboard = null) + { + Dispatcher.UIThread.Post(() => + { + ToggledButton.IsChecked = true; + }); + + if (_isWaitingForInput) + { + Dispatcher.UIThread.Post(() => + { + Cancel(); + }); + + return; + } + + _isWaitingForInput = true; + + assigner.Initialize(); + + await Task.Run(async () => + { + while (true) + { + if (!_isWaitingForInput) + { + return; + } + + await Task.Delay(10); + + assigner.ReadInput(); + + if (assigner.HasAnyButtonPressed() || assigner.ShouldCancel() || (keyboard != null && keyboard.IsPressed(Key.Escape))) + { + break; + } + } + }); + + await Dispatcher.UIThread.InvokeAsync(() => + { + string pressedButton = assigner.GetPressedButton(); + + if (_shouldUnbind) + { + SetButtonText(ToggledButton, "Unbound"); + } + else if (pressedButton != "") + { + SetButtonText(ToggledButton, pressedButton); + } + + _shouldUnbind = false; + _isWaitingForInput = false; + + ToggledButton.IsChecked = false; + + ButtonAssigned?.Invoke(this, new ButtonAssignedEventArgs(ToggledButton, pressedButton != null)); + + static void SetButtonText(ToggleButton button, string text) + { + ILogical textBlock = button.GetLogicalDescendants().First(x => x is TextBlock); + + if (textBlock != null && textBlock is TextBlock block) + { + block.Text = text; + } + } + }); + } + + public void Cancel(bool shouldUnbind = false) + { + _isWaitingForInput = false; + ToggledButton.IsChecked = false; + _shouldUnbind = shouldUnbind; + } + } +} diff --git a/src/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs b/src/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs new file mode 100644 index 00000000..cb474506 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs @@ -0,0 +1,407 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Media; +using Avalonia.Threading; +using FluentAvalonia.Core; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Controls; +using Ryujinx.Ava.UI.Windows; +using Ryujinx.Common.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Helpers +{ + public static class ContentDialogHelper + { + private static bool _isChoiceDialogOpen; + + public async static Task<UserResult> ShowContentDialog( + string title, + object content, + string primaryButton, + string secondaryButton, + string closeButton, + UserResult primaryButtonResult = UserResult.Ok, + ManualResetEvent deferResetEvent = null, + TypedEventHandler<ContentDialog, ContentDialogButtonClickEventArgs> deferCloseAction = null) + { + UserResult result = UserResult.None; + + ContentDialog contentDialog = new() + { + Title = title, + PrimaryButtonText = primaryButton, + SecondaryButtonText = secondaryButton, + CloseButtonText = closeButton, + Content = content + }; + + contentDialog.PrimaryButtonCommand = MiniCommand.Create(() => + { + result = primaryButtonResult; + }); + + contentDialog.SecondaryButtonCommand = MiniCommand.Create(() => + { + result = UserResult.No; + contentDialog.PrimaryButtonClick -= deferCloseAction; + }); + + contentDialog.CloseButtonCommand = MiniCommand.Create(() => + { + result = UserResult.Cancel; + contentDialog.PrimaryButtonClick -= deferCloseAction; + }); + + if (deferResetEvent != null) + { + contentDialog.PrimaryButtonClick += deferCloseAction; + } + + await ShowAsync(contentDialog); + + return result; + } + + private async static Task<UserResult> ShowTextDialog( + string title, + string primaryText, + string secondaryText, + string primaryButton, + string secondaryButton, + string closeButton, + int iconSymbol, + UserResult primaryButtonResult = UserResult.Ok, + ManualResetEvent deferResetEvent = null, + TypedEventHandler<ContentDialog, ContentDialogButtonClickEventArgs> deferCloseAction = null) + { + Grid content = CreateTextDialogContent(primaryText, secondaryText, iconSymbol); + + return await ShowContentDialog(title, content, primaryButton, secondaryButton, closeButton, primaryButtonResult, deferResetEvent, deferCloseAction); + } + + 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; + + return await ShowTextDialog( + title, + primaryText, + secondaryText, + primaryButton, + secondaryButton, + closeButton, + iconSymbol, + primaryButton == LocaleManager.Instance[LocaleKeys.InputDialogYes] ? UserResult.Yes : UserResult.Ok, + deferResetEvent, + DeferClose); + + async void DeferClose(ContentDialog sender, ContentDialogButtonClickEventArgs args) + { + if (startedDeferring) + { + return; + } + + sender.PrimaryButtonClick -= DeferClose; + + startedDeferring = true; + + var deferral = args.GetDeferral(); + + result = primaryButton == LocaleManager.Instance[LocaleKeys.InputDialogYes] ? UserResult.Yes : UserResult.Ok; + + sender.PrimaryButtonClick -= DeferClose; + + _ = Task.Run(() => + { + deferResetEvent.WaitOne(); + + Dispatcher.UIThread.Post(() => + { + deferral.Complete(); + }); + }); + + if (doWhileDeferred != null) + { + await doWhileDeferred(window); + + deferResetEvent.Set(); + } + } + } + + private static Grid CreateTextDialogContent(string primaryText, string secondaryText, int symbol) + { + Grid content = new() + { + RowDefinitions = new RowDefinitions() { new RowDefinition(), new RowDefinition() }, + ColumnDefinitions = new ColumnDefinitions() { new ColumnDefinition(GridLength.Auto), new ColumnDefinition() }, + + MinHeight = 80 + }; + + SymbolIcon icon = new() + { + Symbol = (Symbol)symbol, + Margin = new Thickness(10), + FontSize = 40, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center + }; + + Grid.SetColumn(icon, 0); + Grid.SetRowSpan(icon, 2); + Grid.SetRow(icon, 0); + + TextBlock primaryLabel = new() + { + Text = primaryText, + Margin = new Thickness(5), + TextWrapping = TextWrapping.Wrap, + MaxWidth = 450 + }; + + TextBlock secondaryLabel = new() + { + Text = secondaryText, + Margin = new Thickness(5), + TextWrapping = TextWrapping.Wrap, + MaxWidth = 450 + }; + + Grid.SetColumn(primaryLabel, 1); + Grid.SetColumn(secondaryLabel, 1); + Grid.SetRow(primaryLabel, 0); + Grid.SetRow(secondaryLabel, 1); + + content.Children.Add(icon); + content.Children.Add(primaryLabel); + content.Children.Add(secondaryLabel); + + return content; + } + + public static async Task<UserResult> CreateInfoDialog( + string primary, + string secondaryText, + string acceptButton, + string closeButton, + string title) + { + return await ShowTextDialog( + title, + primary, + secondaryText, + acceptButton, + "", + closeButton, + (int)Symbol.Important); + } + + internal static async Task<UserResult> CreateConfirmationDialog( + string primaryText, + string secondaryText, + string acceptButtonText, + string cancelButtonText, + string title, + UserResult primaryButtonResult = UserResult.Yes) + { + return await ShowTextDialog( + string.IsNullOrWhiteSpace(title) ? LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle] : title, + primaryText, + secondaryText, + acceptButtonText, + "", + cancelButtonText, + (int)Symbol.Help, + primaryButtonResult); + } + + internal static async Task CreateUpdaterInfoDialog(string primary, string secondaryText) + { + await ShowTextDialog( + LocaleManager.Instance[LocaleKeys.DialogUpdaterTitle], + primary, + secondaryText, + "", + "", + LocaleManager.Instance[LocaleKeys.InputDialogOk], + (int)Symbol.Important); + } + + internal static async Task CreateWarningDialog(string primary, string secondaryText) + { + await ShowTextDialog( + LocaleManager.Instance[LocaleKeys.DialogWarningTitle], + primary, + secondaryText, + "", + "", + LocaleManager.Instance[LocaleKeys.InputDialogOk], + (int)Symbol.Important); + } + + internal static async Task CreateErrorDialog(string errorMessage, string secondaryErrorMessage = "") + { + Logger.Error?.Print(LogClass.Application, errorMessage); + + await ShowTextDialog( + LocaleManager.Instance[LocaleKeys.DialogErrorTitle], + LocaleManager.Instance[LocaleKeys.DialogErrorMessage], + errorMessage, + secondaryErrorMessage, + "", + LocaleManager.Instance[LocaleKeys.InputDialogOk], + (int)Symbol.Dismiss); + } + + internal static async Task<bool> CreateChoiceDialog(string title, string primary, string secondaryText) + { + if (_isChoiceDialogOpen) + { + return false; + } + + _isChoiceDialogOpen = true; + + UserResult response = await ShowTextDialog( + title, + primary, + secondaryText, + LocaleManager.Instance[LocaleKeys.InputDialogYes], + "", + LocaleManager.Instance[LocaleKeys.InputDialogNo], + (int)Symbol.Help, + UserResult.Yes); + + _isChoiceDialogOpen = false; + + return response == UserResult.Yes; + } + + internal static async Task<bool> CreateExitDialog() + { + return await CreateChoiceDialog( + LocaleManager.Instance[LocaleKeys.DialogExitTitle], + LocaleManager.Instance[LocaleKeys.DialogExitMessage], + LocaleManager.Instance[LocaleKeys.DialogExitSubMessage]); + } + + internal static async Task<bool> CreateStopEmulationDialog() + { + return await CreateChoiceDialog( + LocaleManager.Instance[LocaleKeys.DialogStopEmulationTitle], + LocaleManager.Instance[LocaleKeys.DialogStopEmulationMessage], + LocaleManager.Instance[LocaleKeys.DialogExitSubMessage]); + } + + public static async Task<ContentDialogResult> ShowAsync(ContentDialog contentDialog) + { + ContentDialogResult result; + + ContentDialogOverlayWindow contentDialogOverlayWindow = null; + + Window parent = GetMainWindow(); + + if (parent != null && parent.IsActive && parent is MainWindow window && window.ViewModel.IsGameRunning) + { + contentDialogOverlayWindow = new() + { + Height = parent.Bounds.Height, + Width = parent.Bounds.Width, + Position = parent.PointToScreen(new Point()), + ShowInTaskbar = false + }; + + parent.PositionChanged += OverlayOnPositionChanged; + + void OverlayOnPositionChanged(object sender, PixelPointEventArgs e) + { + contentDialogOverlayWindow.Position = parent.PointToScreen(new Point()); + } + + contentDialogOverlayWindow.ContentDialog = contentDialog; + + bool opened = false; + + contentDialogOverlayWindow.Opened += OverlayOnActivated; + + async void OverlayOnActivated(object sender, EventArgs e) + { + if (opened) + { + return; + } + + opened = true; + + contentDialogOverlayWindow.Position = parent.PointToScreen(new Point()); + + result = await ShowDialog(); + } + + result = await contentDialogOverlayWindow.ShowDialog<ContentDialogResult>(parent); + } + else + { + result = await ShowDialog(); + } + + async Task<ContentDialogResult> ShowDialog() + { + if (contentDialogOverlayWindow is not null) + { + result = await contentDialog.ShowAsync(contentDialogOverlayWindow); + + contentDialogOverlayWindow!.Close(); + } + else + { + result = await contentDialog.ShowAsync(); + } + + return result; + } + + if (contentDialogOverlayWindow is not null) + { + contentDialogOverlayWindow.Content = null; + contentDialogOverlayWindow.Close(); + } + + return result; + } + + private static Window GetMainWindow() + { + if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime al) + { + foreach (Window item in al.Windows) + { + if (item.IsActive && item is MainWindow window) + { + return window; + } + } + } + + return null; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Helpers/Glyph.cs b/src/Ryujinx.Ava/UI/Helpers/Glyph.cs new file mode 100644 index 00000000..4aae854f --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/Glyph.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Ava.UI.Helpers +{ + public enum Glyph + { + List, + Grid, + Chip + } +} diff --git a/src/Ryujinx.Ava/UI/Helpers/GlyphValueConverter.cs b/src/Ryujinx.Ava/UI/Helpers/GlyphValueConverter.cs new file mode 100644 index 00000000..3d6c9c01 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/GlyphValueConverter.cs @@ -0,0 +1,49 @@ +using Avalonia.Data; +using Avalonia.Markup.Xaml; +using FluentAvalonia.UI.Controls; +using System; +using System.Collections.Generic; + +namespace Ryujinx.Ava.UI.Helpers +{ + public class GlyphValueConverter : MarkupExtension + { + private string _key; + + private static Dictionary<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/src/Ryujinx.Ava/UI/Helpers/HotKeyControl.cs b/src/Ryujinx.Ava/UI/Helpers/HotKeyControl.cs new file mode 100644 index 00000000..f1fad157 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/HotKeyControl.cs @@ -0,0 +1,52 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using System; +using System.Windows.Input; + +namespace Ryujinx.Ava.UI.Helpers +{ + public class HotKeyControl : ContentControl, ICommandSource + { + public static readonly StyledProperty<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/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs b/src/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs new file mode 100644 index 00000000..8d5c2815 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/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.Helpers +{ + internal 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/src/Ryujinx.Ava/UI/Helpers/LoggerAdapter.cs b/src/Ryujinx.Ava/UI/Helpers/LoggerAdapter.cs new file mode 100644 index 00000000..7a29cc19 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/LoggerAdapter.cs @@ -0,0 +1,115 @@ +using Avalonia.Utilities; +using System; +using System.Text; + +namespace Ryujinx.Ava.UI.Helpers +{ + using AvaLogger = Avalonia.Logging.Logger; + using AvaLogLevel = Avalonia.Logging.LogEventLevel; + using RyuLogClass = Ryujinx.Common.Logging.LogClass; + using RyuLogger = Ryujinx.Common.Logging.Logger; + + internal class LoggerAdapter : Avalonia.Logging.ILogSink + { + public static void Register() + { + AvaLogger.Sink = new LoggerAdapter(); + } + + private static RyuLogger.Log? GetLog(AvaLogLevel level) + { + return level switch + { + AvaLogLevel.Verbose => RyuLogger.Debug, + AvaLogLevel.Debug => RyuLogger.Debug, + AvaLogLevel.Information => RyuLogger.Debug, + AvaLogLevel.Warning => RyuLogger.Debug, + AvaLogLevel.Error => RyuLogger.Error, + AvaLogLevel.Fatal => RyuLogger.Error, + _ => throw new ArgumentOutOfRangeException(nameof(level), level, null) + }; + } + + public bool IsEnabled(AvaLogLevel level, string area) + { + return GetLog(level) != null; + } + + public void Log(AvaLogLevel level, string area, object source, string messageTemplate) + { + GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(level, area, messageTemplate, source, null)); + } + + public void Log<T0>(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0) + { + GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(level, area, messageTemplate, source, new object[] { propertyValue0 })); + } + + public void Log<T0, T1>(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(level, area, messageTemplate, source, new object[] { propertyValue0, propertyValue1 })); + } + + public void Log<T0, T1, T2>(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(level, area, messageTemplate, source, new object[] { propertyValue0, propertyValue1, propertyValue2 })); + } + + public void Log(AvaLogLevel level, string area, object source, string messageTemplate, params object[] propertyValues) + { + GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(level, area, messageTemplate, source, propertyValues)); + } + + private static string Format(AvaLogLevel level, string area, string template, object source, object[] v) + { + var result = new StringBuilder(); + var r = new CharacterReader(template.AsSpan()); + int i = 0; + + result.Append('['); + result.Append(level); + result.Append("] "); + + result.Append('['); + result.Append(area); + result.Append("] "); + + while (!r.End) + { + var c = r.Take(); + + if (c != '{') + { + result.Append(c); + } + else + { + if (r.Peek != '{') + { + result.Append('\''); + result.Append(i < v.Length ? v[i++] : null); + result.Append('\''); + r.TakeUntil('}'); + r.Take(); + } + else + { + result.Append('{'); + r.Take(); + } + } + } + + if (source != null) + { + result.Append(" ("); + result.Append(source.GetType().Name); + result.Append(" #"); + result.Append(source.GetHashCode()); + result.Append(')'); + } + + return result.ToString(); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Helpers/MiniCommand.cs b/src/Ryujinx.Ava/UI/Helpers/MiniCommand.cs new file mode 100644 index 00000000..305182c9 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/MiniCommand.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace Ryujinx.Ava.UI.Helpers +{ + 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/src/Ryujinx.Ava/UI/Helpers/NotificationHelper.cs b/src/Ryujinx.Ava/UI/Helpers/NotificationHelper.cs new file mode 100644 index 00000000..7e2afb8b --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/NotificationHelper.cs @@ -0,0 +1,65 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Notifications; +using Avalonia.Threading; +using Ryujinx.Ava.Common.Locale; +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Helpers +{ + public static class NotificationHelper + { + private const int MaxNotifications = 4; + private const int NotificationDelayInMs = 5000; + + private static WindowNotificationManager _notificationManager; + + private static readonly ManualResetEvent _templateAppliedEvent = new(false); + private static readonly BlockingCollection<Notification> _notifications = new(); + + public static void SetNotificationManager(Window host) + { + _notificationManager = new WindowNotificationManager(host) + { + Position = NotificationPosition.BottomRight, + MaxItems = MaxNotifications, + Margin = new Thickness(0, 0, 15, 40) + }; + + _notificationManager.TemplateApplied += (sender, args) => + { + _templateAppliedEvent.Set(); + }; + + Task.Run(async () => + { + _templateAppliedEvent.WaitOne(); + + foreach (var notification in _notifications.GetConsumingEnumerable()) + { + Dispatcher.UIThread.Post(() => + { + _notificationManager.Show(notification); + }); + + await Task.Delay(NotificationDelayInMs / MaxNotifications); + } + }); + } + + public static void Show(string title, string text, NotificationType type, bool waitingExit = false, Action onClick = null, Action onClose = null) + { + var delay = waitingExit ? TimeSpan.FromMilliseconds(0) : TimeSpan.FromMilliseconds(NotificationDelayInMs); + + _notifications.Add(new Notification(title, text, type, delay, onClick, onClose)); + } + + public static void ShowError(string message) + { + Show(LocaleManager.Instance[LocaleKeys.DialogErrorTitle], $"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}", NotificationType.Error); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Helpers/OffscreenTextBox.cs b/src/Ryujinx.Ava/UI/Helpers/OffscreenTextBox.cs new file mode 100644 index 00000000..785e785c --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/OffscreenTextBox.cs @@ -0,0 +1,40 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace Ryujinx.Ava.UI.Helpers +{ + 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/src/Ryujinx.Ava/UI/Helpers/UserErrorDialog.cs b/src/Ryujinx.Ava/UI/Helpers/UserErrorDialog.cs new file mode 100644 index 00000000..4ed629ff --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/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.Helpers +{ + 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[LocaleKeys.UserErrorNoKeys], + UserError.NoFirmware => LocaleManager.Instance[LocaleKeys.UserErrorNoFirmware], + UserError.FirmwareParsingFailed => LocaleManager.Instance[LocaleKeys.UserErrorFirmwareParsingFailed], + UserError.ApplicationNotFound => LocaleManager.Instance[LocaleKeys.UserErrorApplicationNotFound], + UserError.Unknown => LocaleManager.Instance[LocaleKeys.UserErrorUnknown], + _ => LocaleManager.Instance[LocaleKeys.UserErrorUndefined] + }; + } + + private static string GetErrorDescription(UserError error) + { + return error switch + { + UserError.NoKeys => LocaleManager.Instance[LocaleKeys.UserErrorNoKeysDescription], + UserError.NoFirmware => LocaleManager.Instance[LocaleKeys.UserErrorNoFirmwareDescription], + UserError.FirmwareParsingFailed => LocaleManager.Instance[LocaleKeys.UserErrorFirmwareParsingFailedDescription], + UserError.ApplicationNotFound => LocaleManager.Instance[LocaleKeys.UserErrorApplicationNotFoundDescription], + UserError.Unknown => LocaleManager.Instance[LocaleKeys.UserErrorUnknownDescription], + _ => LocaleManager.Instance[LocaleKeys.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[LocaleKeys.OpenSetupGuideMessage] : ""; + + var result = await ContentDialogHelper.CreateInfoDialog( + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogUserErrorDialogMessage, errorCode, GetErrorTitle(error)), + GetErrorDescription(error) + (isInSetupGuide + ? LocaleManager.Instance[LocaleKeys.DialogUserErrorDialogInfoMessage] + : ""), setupButtonLabel, LocaleManager.Instance[LocaleKeys.InputDialogOk], + LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogUserErrorDialogTitle, errorCode)); + + if (result == UserResult.Ok) + { + OpenHelper.OpenUrl(GetSetupGuideUrl(error)); + } + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Helpers/UserResult.cs b/src/Ryujinx.Ava/UI/Helpers/UserResult.cs new file mode 100644 index 00000000..57802804 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/UserResult.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.Ava.UI.Helpers +{ + public enum UserResult + { + Ok, + Yes, + No, + Abort, + Cancel, + None, + } +}
\ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs b/src/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs new file mode 100644 index 00000000..03d3a49f --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs @@ -0,0 +1,123 @@ +using System; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace Ryujinx.Ava.UI.Helpers +{ + [SupportedOSPlatform("windows")] + internal partial class Win32NativeInterop + { + [Flags] + public enum ClassStyles : uint + { + CS_CLASSDC = 0x40, + CS_OWNDC = 0x20, + } + + [Flags] + public enum WindowStyles : uint + { + WS_CHILD = 0x40000000 + } + + public enum Cursors : uint + { + IDC_ARROW = 32512 + } + + public enum WindowsMessages : uint + { + MOUSEMOVE = 0x0200, + LBUTTONDOWN = 0x0201, + LBUTTONUP = 0x0202, + LBUTTONDBLCLK = 0x0203, + RBUTTONDOWN = 0x0204, + RBUTTONUP = 0x0205, + RBUTTONDBLCLK = 0x0206, + MBUTTONDOWN = 0x0207, + MBUTTONUP = 0x0208, + MBUTTONDBLCLK = 0x0209, + MOUSEWHEEL = 0x020A, + XBUTTONDOWN = 0x020B, + XBUTTONUP = 0x020C, + XBUTTONDBLCLK = 0x020D, + MOUSEHWHEEL = 0x020E, + MOUSELAST = 0x020E + } + + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + internal delegate IntPtr WindowProc(IntPtr hWnd, WindowsMessages msg, IntPtr wParam, IntPtr lParam); + + [StructLayout(LayoutKind.Sequential)] + public struct WNDCLASSEX + { + public int cbSize; + public ClassStyles style; + public IntPtr lpfnWndProc; // not WndProc + public int cbClsExtra; + public int cbWndExtra; + public IntPtr hInstance; + public IntPtr hIcon; + public IntPtr hCursor; + public IntPtr hbrBackground; + public IntPtr lpszMenuName; + public IntPtr lpszClassName; + public IntPtr hIconSm; + + public WNDCLASSEX() + { + cbSize = Marshal.SizeOf<WNDCLASSEX>(); + } + } + + public static IntPtr CreateEmptyCursor() + { + return CreateCursor(IntPtr.Zero, 0, 0, 1, 1, new byte[] { 0xFF }, new byte[] { 0x00 }); + } + + public static IntPtr CreateArrowCursor() + { + return LoadCursor(IntPtr.Zero, (IntPtr)Cursors.IDC_ARROW); + } + + [LibraryImport("user32.dll")] + public static partial IntPtr SetCursor(IntPtr handle); + + [LibraryImport("user32.dll")] + public static partial IntPtr CreateCursor(IntPtr hInst, int xHotSpot, int yHotSpot, int nWidth, int nHeight, byte[] pvANDPlane, byte[] pvXORPlane); + + [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "RegisterClassExW")] + public static partial ushort RegisterClassEx(ref WNDCLASSEX param); + + [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "UnregisterClassW")] + public static partial short UnregisterClass([MarshalAs(UnmanagedType.LPWStr)] string lpClassName, IntPtr instance); + + [LibraryImport("user32.dll", EntryPoint = "DefWindowProcW")] + public static partial IntPtr DefWindowProc(IntPtr hWnd, WindowsMessages msg, IntPtr wParam, IntPtr lParam); + + [LibraryImport("kernel32.dll", EntryPoint = "GetModuleHandleA")] + public static partial IntPtr GetModuleHandle([MarshalAs(UnmanagedType.LPStr)] string lpModuleName); + + [LibraryImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool DestroyWindow(IntPtr hwnd); + + [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "LoadCursorA")] + public static partial IntPtr LoadCursor(IntPtr hInstance, IntPtr lpCursorName); + + [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "CreateWindowExW")] + public static partial IntPtr CreateWindowEx( + uint dwExStyle, + [MarshalAs(UnmanagedType.LPWStr)] string lpClassName, + [MarshalAs(UnmanagedType.LPWStr)] string lpWindowName, + WindowStyles dwStyle, + int x, + int y, + int nWidth, + int nHeight, + IntPtr hWndParent, + IntPtr hMenu, + IntPtr hInstance, + IntPtr lpParam); + } +} |
