diff options
| author | Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com> | 2022-12-29 14:24:05 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-12-29 15:24:05 +0100 |
| commit | 76671d63d4f3ea18f8ad99e9ce9f0b2ec9a2599d (patch) | |
| tree | 05013214e4696a9254369d0706173f58877f6a83 /Ryujinx.Ava/UI/Helpers | |
| parent | 3d1a0bf3749afa14da5b5ba1e0666fdb78c99beb (diff) | |
Ava GUI: Restructure `Ryujinx.Ava` (#4165)
* Restructure `Ryujinx.Ava`
* Stylistic consistency
* Update Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Controls/UserSelector.axaml.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
* Fix redundancies
* Remove redunancies
* Add back elses
Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
Diffstat (limited to 'Ryujinx.Ava/UI/Helpers')
21 files changed, 1537 insertions, 0 deletions
diff --git a/Ryujinx.Ava/UI/Helpers/ApplicationOpenedEventArgs.cs b/Ryujinx.Ava/UI/Helpers/ApplicationOpenedEventArgs.cs new file mode 100644 index 00000000..ebf5c16e --- /dev/null +++ b/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/Ryujinx.Ava/UI/Helpers/AvaloniaGlxContext.cs b/Ryujinx.Ava/UI/Helpers/AvaloniaGlxContext.cs new file mode 100644 index 00000000..6b696ba7 --- /dev/null +++ b/Ryujinx.Ava/UI/Helpers/AvaloniaGlxContext.cs @@ -0,0 +1,16 @@ +using SPB.Graphics; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Ava.UI.Helpers +{ + [SupportedOSPlatform("linux")] + internal 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/Helpers/AvaloniaWglContext.cs b/Ryujinx.Ava/UI/Helpers/AvaloniaWglContext.cs new file mode 100644 index 00000000..b63a973a --- /dev/null +++ b/Ryujinx.Ava/UI/Helpers/AvaloniaWglContext.cs @@ -0,0 +1,16 @@ +using SPB.Graphics; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Ava.UI.Helpers +{ + [SupportedOSPlatform("windows")] + internal 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/Helpers/BitmapArrayValueConverter.cs b/Ryujinx.Ava/UI/Helpers/BitmapArrayValueConverter.cs new file mode 100644 index 00000000..3fd368f8 --- /dev/null +++ b/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/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs b/Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs new file mode 100644 index 00000000..6730b571 --- /dev/null +++ b/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/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs b/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs new file mode 100644 index 00000000..cf30d99b --- /dev/null +++ b/Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs @@ -0,0 +1,400 @@ +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; + + private async static Task<UserResult> ShowContentDialog( + string title, + string primaryText, + string secondaryText, + string primaryButton, + string secondaryButton, + string closeButton, + int iconSymbol, + UserResult primaryButtonResult = UserResult.Ok, + ManualResetEvent deferResetEvent = null, + Func<Window, Task> doWhileDeferred = null, + TypedEventHandler<ContentDialog, ContentDialogButtonClickEventArgs> deferCloseAction = null) + { + UserResult result = UserResult.None; + + bool useOverlay = false; + Window mainWindow = null; + + if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime al) + { + foreach (var item in al.Windows) + { + if (item.IsActive && item is MainWindow window && window.ViewModel.IsGameRunning) + { + mainWindow = window; + useOverlay = true; + break; + } + } + } + + ContentDialog contentDialog = null; + ContentDialogOverlayWindow overlay = null; + + if (useOverlay) + { + overlay = new ContentDialogOverlayWindow() + { + Height = mainWindow.Bounds.Height, + Width = mainWindow.Bounds.Width, + Position = mainWindow.PointToScreen(new Point()) + }; + + mainWindow.PositionChanged += OverlayOnPositionChanged; + + void OverlayOnPositionChanged(object sender, PixelPointEventArgs e) + { + overlay.Position = mainWindow.PointToScreen(new Point()); + } + + contentDialog = overlay.ContentDialog; + + bool opened = false; + + overlay.Opened += OverlayOnActivated; + + async void OverlayOnActivated(object sender, EventArgs e) + { + if (opened) + { + return; + } + + opened = true; + + overlay.Position = mainWindow.PointToScreen(new Point()); + + await ShowDialog(); + } + + await overlay.ShowDialog(mainWindow); + } + else + { + contentDialog = new ContentDialog(); + + await ShowDialog(); + } + + async Task ShowDialog() + { + 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.PrimaryButtonClick -= deferCloseAction; + }); + contentDialog.CloseButtonCommand = MiniCommand.Create(() => + { + result = UserResult.Cancel; + contentDialog.PrimaryButtonClick -= deferCloseAction; + }); + + if (deferResetEvent != null) + { + contentDialog.PrimaryButtonClick += deferCloseAction; + } + + if (useOverlay) + { + await contentDialog.ShowAsync(overlay, ContentDialogPlacement.Popup); + + overlay!.Close(); + } + else + { + await contentDialog.ShowAsync(ContentDialogPlacement.Popup); + } + } + + if (useOverlay) + { + overlay.Content = null; + overlay.Close(); + } + + 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; + + return await ShowContentDialog( + title, + primaryText, + secondaryText, + primaryButton, + secondaryButton, + closeButton, + iconSymbol, + primaryButton == LocaleManager.Instance["InputDialogYes"] ? UserResult.Yes : UserResult.Ok, + deferResetEvent, + doWhileDeferred, + DeferClose); + + async void DeferClose(ContentDialog sender, ContentDialogButtonClickEventArgs args) + { + if (startedDeferring) + { + return; + } + + sender.PrimaryButtonClick -= DeferClose; + + startedDeferring = true; + + var deferral = args.GetDeferral(); + + result = primaryButton == LocaleManager.Instance["InputDialogYes"] ? UserResult.Yes : UserResult.Ok; + + sender.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(window); + + 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 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 Thickness(5), + TextWrapping = TextWrapping.Wrap, + MaxWidth = 450 + }; + TextBlock secondaryLabel = new TextBlock() + { + 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 ShowContentDialog( + 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 ShowContentDialog( + 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 Task CreateUpdaterInfoDialog(string primary, string secondaryText) + { + await ShowContentDialog( + LocaleManager.Instance["DialogUpdaterTitle"], + primary, + secondaryText, + "", + "", + LocaleManager.Instance["InputDialogOk"], + (int)Symbol.Important); + } + + internal static async Task CreateWarningDialog(string primary, string secondaryText) + { + await ShowContentDialog( + LocaleManager.Instance["DialogWarningTitle"], + primary, + secondaryText, + "", + "", + LocaleManager.Instance["InputDialogOk"], + (int)Symbol.Important); + } + + internal static async Task CreateErrorDialog(string errorMessage, string secondaryErrorMessage = "") + { + Logger.Error?.Print(LogClass.Application, errorMessage); + + await ShowContentDialog( + LocaleManager.Instance["DialogErrorTitle"], + LocaleManager.Instance["DialogErrorMessage"], + errorMessage, + secondaryErrorMessage, + "", + LocaleManager.Instance["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 ShowContentDialog( + 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() + { + return await CreateChoiceDialog( + LocaleManager.Instance["DialogExitTitle"], + LocaleManager.Instance["DialogExitMessage"], + LocaleManager.Instance["DialogExitSubMessage"]); + } + + internal static async Task<bool> CreateStopEmulationDialog() + { + return await CreateChoiceDialog( + LocaleManager.Instance["DialogStopEmulationTitle"], + LocaleManager.Instance["DialogStopEmulationMessage"], + LocaleManager.Instance["DialogExitSubMessage"]); + } + + internal static async Task<string> CreateInputDialog( + string title, + string mainText, + string subText, + uint maxLength = int.MaxValue, + string input = "") + { + var result = await InputDialog.ShowInputDialog( + 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/Helpers/EmbeddedWindow.cs b/Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs new file mode 100644 index 00000000..bdeceaea --- /dev/null +++ b/Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs @@ -0,0 +1,233 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Platform; +using Ryujinx.Ava.UI.Helper; +using SPB.Graphics; +using SPB.Platform; +using SPB.Platform.GLX; +using System; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Threading.Tasks; +using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop; + +namespace Ryujinx.Ava.UI.Helpers +{ + public class EmbeddedWindow : NativeControlHost + { + private WindowProc _wndProcDelegate; + private string _className; + + protected GLXWindow X11Window { get; set; } + protected IntPtr WindowHandle { get; set; } + protected IntPtr X11Display { get; set; } + protected IntPtr NsView { get; set; } + protected IntPtr MetalLayer { get; set; } + + private UpdateBoundsCallbackDelegate _updateBoundsCallback; + + public event EventHandler<IntPtr> WindowCreated; + public event EventHandler<Size> SizeChanged; + + protected virtual void OnWindowDestroyed() { } + protected virtual void OnWindowDestroying() + { + WindowHandle = IntPtr.Zero; + X11Display = IntPtr.Zero; + } + + public EmbeddedWindow() + { + var stateObserverable = this.GetObservable(BoundsProperty); + + stateObserverable.Subscribe(StateChanged); + + this.Initialized += NativeEmbeddedWindow_Initialized; + } + + public virtual void OnWindowCreated() { } + + private void NativeEmbeddedWindow_Initialized(object sender, EventArgs e) + { + OnWindowCreated(); + + Task.Run(() => + { + WindowCreated?.Invoke(this, WindowHandle); + }); + } + + private void StateChanged(Rect rect) + { + SizeChanged?.Invoke(this, rect.Size); + _updateBoundsCallback?.Invoke(rect); + } + + protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent) + { + if (OperatingSystem.IsLinux()) + { + return CreateLinux(parent); + } + else if (OperatingSystem.IsWindows()) + { + return CreateWin32(parent); + } + else if (OperatingSystem.IsMacOS()) + { + return CreateMacOs(parent); + } + + return base.CreateNativeControlCore(parent); + } + + protected override void DestroyNativeControlCore(IPlatformHandle control) + { + OnWindowDestroying(); + + if (OperatingSystem.IsLinux()) + { + DestroyLinux(); + } + else if (OperatingSystem.IsWindows()) + { + DestroyWin32(control); + } + else if (OperatingSystem.IsMacOS()) + { + DestroyMacOS(); + } + else + { + base.DestroyNativeControlCore(control); + } + + OnWindowDestroyed(); + } + + [SupportedOSPlatform("linux")] + protected virtual IPlatformHandle CreateLinux(IPlatformHandle parent) + { + X11Window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100) as GLXWindow; + WindowHandle = X11Window.WindowHandle.RawHandle; + X11Display = X11Window.DisplayHandle.RawHandle; + + return new PlatformHandle(WindowHandle, "X11"); + } + + [SupportedOSPlatform("windows")] + IPlatformHandle CreateWin32(IPlatformHandle parent) + { + _className = "NativeWindow-" + Guid.NewGuid(); + _wndProcDelegate = WndProc; + var wndClassEx = new WNDCLASSEX + { + cbSize = Marshal.SizeOf<WNDCLASSEX>(), + hInstance = GetModuleHandle(null), + lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate), + style = ClassStyles.CS_OWNDC, + lpszClassName = Marshal.StringToHGlobalUni(_className), + hCursor = LoadCursor(IntPtr.Zero, (IntPtr)Cursors.IDC_ARROW) + }; + + var atom = RegisterClassEx(ref wndClassEx); + + var handle = CreateWindowEx( + 0, + _className, + "NativeWindow", + WindowStyles.WS_CHILD, + 0, + 0, + 640, + 480, + parent.Handle, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero); + + WindowHandle = handle; + + Marshal.FreeHGlobal(wndClassEx.lpszClassName); + + return new PlatformHandle(WindowHandle, "HWND"); + } + + [SupportedOSPlatform("windows")] + IntPtr WndProc(IntPtr hWnd, WindowsMessages msg, IntPtr wParam, IntPtr lParam) + { + var point = new Point((long)lParam & 0xFFFF, ((long)lParam >> 16) & 0xFFFF); + var root = VisualRoot as Window; + bool isLeft = false; + switch (msg) + { + case WindowsMessages.LBUTTONDOWN: + case WindowsMessages.RBUTTONDOWN: + isLeft = msg == WindowsMessages.LBUTTONDOWN; + this.RaiseEvent(new PointerPressedEventArgs( + this, + new Pointer(0, PointerType.Mouse, true), + root, + this.TranslatePoint(point, root).Value, + (ulong)Environment.TickCount64, + new PointerPointProperties(isLeft ? RawInputModifiers.LeftMouseButton : RawInputModifiers.RightMouseButton, isLeft ? PointerUpdateKind.LeftButtonPressed : PointerUpdateKind.RightButtonPressed), + KeyModifiers.None)); + break; + case WindowsMessages.LBUTTONUP: + case WindowsMessages.RBUTTONUP: + isLeft = msg == WindowsMessages.LBUTTONUP; + this.RaiseEvent(new PointerReleasedEventArgs( + this, + new Pointer(0, PointerType.Mouse, true), + root, + this.TranslatePoint(point, root).Value, + (ulong)Environment.TickCount64, + new PointerPointProperties(isLeft ? RawInputModifiers.LeftMouseButton : RawInputModifiers.RightMouseButton, isLeft ? PointerUpdateKind.LeftButtonReleased : PointerUpdateKind.RightButtonReleased), + KeyModifiers.None, + isLeft ? MouseButton.Left : MouseButton.Right)); + break; + case WindowsMessages.MOUSEMOVE: + this.RaiseEvent(new PointerEventArgs( + PointerMovedEvent, + this, + new Pointer(0, PointerType.Mouse, true), + root, + this.TranslatePoint(point, root).Value, + (ulong)Environment.TickCount64, + new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.Other), + KeyModifiers.None)); + break; + } + return DefWindowProc(hWnd, msg, wParam, lParam); + } + + [SupportedOSPlatform("macos")] + IPlatformHandle CreateMacOs(IPlatformHandle parent) + { + MetalLayer = MetalHelper.GetMetalLayer(out IntPtr nsView, out _updateBoundsCallback); + + NsView = nsView; + + return new PlatformHandle(nsView, "NSView"); + } + + void DestroyLinux() + { + X11Window?.Dispose(); + } + + [SupportedOSPlatform("windows")] + void DestroyWin32(IPlatformHandle handle) + { + DestroyWindow(handle.Handle); + UnregisterClass(_className, GetModuleHandle(null)); + } + + [SupportedOSPlatform("macos")] + void DestroyMacOS() + { + MetalHelper.DestroyMetalLayer(NsView, MetalLayer); + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Helpers/Glyph.cs b/Ryujinx.Ava/UI/Helpers/Glyph.cs new file mode 100644 index 00000000..4aae854f --- /dev/null +++ b/Ryujinx.Ava/UI/Helpers/Glyph.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Ava.UI.Helpers +{ + public enum Glyph + { + List, + Grid, + Chip + } +} diff --git a/Ryujinx.Ava/UI/Helpers/GlyphValueConverter.cs b/Ryujinx.Ava/UI/Helpers/GlyphValueConverter.cs new file mode 100644 index 00000000..3d6c9c01 --- /dev/null +++ b/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/Ryujinx.Ava/UI/Helpers/HotKeyControl.cs b/Ryujinx.Ava/UI/Helpers/HotKeyControl.cs new file mode 100644 index 00000000..f1fad157 --- /dev/null +++ b/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/Ryujinx.Ava/UI/Helpers/IGlContextExtension.cs b/Ryujinx.Ava/UI/Helpers/IGlContextExtension.cs new file mode 100644 index 00000000..e69774c3 --- /dev/null +++ b/Ryujinx.Ava/UI/Helpers/IGlContextExtension.cs @@ -0,0 +1,25 @@ +using Avalonia.OpenGL; +using SPB.Graphics.OpenGL; +using System; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal 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/Helpers/KeyValueConverter.cs b/Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs new file mode 100644 index 00000000..8d5c2815 --- /dev/null +++ b/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/Ryujinx.Ava/UI/Helpers/MiniCommand.cs b/Ryujinx.Ava/UI/Helpers/MiniCommand.cs new file mode 100644 index 00000000..305182c9 --- /dev/null +++ b/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/Ryujinx.Ava/UI/Helpers/OffscreenTextBox.cs b/Ryujinx.Ava/UI/Helpers/OffscreenTextBox.cs new file mode 100644 index 00000000..785e785c --- /dev/null +++ b/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/Ryujinx.Ava/UI/Helpers/OpenGLEmbeddedWindow.cs b/Ryujinx.Ava/UI/Helpers/OpenGLEmbeddedWindow.cs new file mode 100644 index 00000000..db77f66b --- /dev/null +++ b/Ryujinx.Ava/UI/Helpers/OpenGLEmbeddedWindow.cs @@ -0,0 +1,82 @@ +using OpenTK.Graphics.OpenGL; +using Ryujinx.Common.Configuration; +using SPB.Graphics; +using SPB.Graphics.OpenGL; +using SPB.Platform; +using SPB.Platform.WGL; +using SPB.Windowing; +using System; + +namespace Ryujinx.Ava.UI.Helpers +{ + public class OpenGLEmbeddedWindow : EmbeddedWindow + { + private readonly int _major; + private readonly int _minor; + private readonly GraphicsDebugLevel _graphicsDebugLevel; + private SwappableNativeWindowBase _window; + public OpenGLContextBase Context { get; set; } + + public OpenGLEmbeddedWindow(int major, int minor, GraphicsDebugLevel graphicsDebugLevel) + { + _major = major; + _minor = minor; + _graphicsDebugLevel = graphicsDebugLevel; + } + + protected override void OnWindowDestroying() + { + Context.Dispose(); + base.OnWindowDestroying(); + } + + public override void OnWindowCreated() + { + base.OnWindowCreated(); + + if (OperatingSystem.IsWindows()) + { + _window = new WGLWindow(new NativeHandle(WindowHandle)); + } + else if (OperatingSystem.IsLinux()) + { + _window = X11Window; + } + else + { + throw new PlatformNotSupportedException(); + } + + var flags = OpenGLContextFlags.Compat; + if (_graphicsDebugLevel != GraphicsDebugLevel.None) + { + flags |= OpenGLContextFlags.Debug; + } + + Context = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, _major, _minor, flags); + + Context.Initialize(_window); + Context.MakeCurrent(_window); + + var bindingsContext = new OpenToolkitBindingsContext(Context.GetProcAddress); + + GL.LoadBindings(bindingsContext); + Context.MakeCurrent(null); + } + + public void MakeCurrent() + { + Context?.MakeCurrent(_window); + } + + public void MakeCurrent(NativeWindowBase window) + { + Context?.MakeCurrent(window); + } + + public void SwapBuffers() + { + _window.SwapBuffers(); + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Helpers/OpenToolkitBindingsContext.cs b/Ryujinx.Ava/UI/Helpers/OpenToolkitBindingsContext.cs new file mode 100644 index 00000000..efb703ba --- /dev/null +++ b/Ryujinx.Ava/UI/Helpers/OpenToolkitBindingsContext.cs @@ -0,0 +1,20 @@ +using OpenTK; +using System; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal 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/Helpers/SPBOpenGLContext.cs b/Ryujinx.Ava/UI/Helpers/SPBOpenGLContext.cs new file mode 100644 index 00000000..21f206c8 --- /dev/null +++ b/Ryujinx.Ava/UI/Helpers/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.Helpers +{ + 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/Helpers/UserErrorDialog.cs b/Ryujinx.Ava/UI/Helpers/UserErrorDialog.cs new file mode 100644 index 00000000..ab8d6edc --- /dev/null +++ b/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["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( + 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/Helpers/UserResult.cs b/Ryujinx.Ava/UI/Helpers/UserResult.cs new file mode 100644 index 00000000..57802804 --- /dev/null +++ b/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/Ryujinx.Ava/UI/Helpers/VulkanEmbeddedWindow.cs b/Ryujinx.Ava/UI/Helpers/VulkanEmbeddedWindow.cs new file mode 100644 index 00000000..6581610b --- /dev/null +++ b/Ryujinx.Ava/UI/Helpers/VulkanEmbeddedWindow.cs @@ -0,0 +1,52 @@ +using Avalonia.Platform; +using Silk.NET.Vulkan; +using SPB.Graphics.Vulkan; +using SPB.Platform.GLX; +using SPB.Platform.Metal; +using SPB.Platform.Win32; +using SPB.Platform.X11; +using SPB.Windowing; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Ava.UI.Helpers +{ + public class VulkanEmbeddedWindow : EmbeddedWindow + { + private NativeWindowBase _window; + + [SupportedOSPlatform("linux")] + protected override IPlatformHandle CreateLinux(IPlatformHandle parent) + { + X11Window = new GLXWindow(new NativeHandle(X11.DefaultDisplay), new NativeHandle(parent.Handle)); + WindowHandle = X11Window.WindowHandle.RawHandle; + X11Display = X11Window.DisplayHandle.RawHandle; + + X11Window.Hide(); + + return new PlatformHandle(WindowHandle, "X11"); + } + + public SurfaceKHR CreateSurface(Instance instance) + { + if (OperatingSystem.IsWindows()) + { + _window = new SimpleWin32Window(new NativeHandle(WindowHandle)); + } + else if (OperatingSystem.IsLinux()) + { + _window = new SimpleX11Window(new NativeHandle(X11Display), new NativeHandle(WindowHandle)); + } + else if (OperatingSystem.IsMacOS()) + { + _window = new SimpleMetalWindow(new NativeHandle(NsView), new NativeHandle(MetalLayer)); + } + else + { + throw new PlatformNotSupportedException(); + } + + return new SurfaceKHR((ulong?)VulkanHelper.CreateWindowSurface(instance.Handle, _window)); + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs b/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs new file mode 100644 index 00000000..1e6e3c3b --- /dev/null +++ b/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs @@ -0,0 +1,107 @@ +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>(); + } + } + + [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); + } +} |
