mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2025-07-24 04:17:11 +02:00
infra: Make Avalonia the default UI (#6375)
* misc: Move Ryujinx project to Ryujinx.Gtk3 This breaks release CI for now but that's fine. Signed-off-by: Mary Guillemard <mary@mary.zone> * misc: Move Ryujinx.Ava project to Ryujinx This breaks CI for now, but it's fine. Signed-off-by: Mary Guillemard <mary@mary.zone> * infra: Make Avalonia the default UI Should fix CI after the previous changes. GTK3 isn't build by the release job anymore, only by PR CI. This also ensure that the test-ava update package is still generated to allow update from the old testing channel. Signed-off-by: Mary Guillemard <mary@mary.zone> * Fix missing copy in create_app_bundle.sh Signed-off-by: Mary Guillemard <mary@mary.zone> * Fix syntax error Signed-off-by: Mary Guillemard <mary@mary.zone> --------- Signed-off-by: Mary Guillemard <mary@mary.zone>
This commit is contained in:
parent
53b5985da6
commit
ec6cb0abb4
239 changed files with 1235 additions and 1232 deletions
16
src/Ryujinx/UI/Helpers/ApplicationOpenedEventArgs.cs
Normal file
16
src/Ryujinx/UI/Helpers/ApplicationOpenedEventArgs.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
36
src/Ryujinx/UI/Helpers/BitmapArrayValueConverter.cs
Normal file
36
src/Ryujinx/UI/Helpers/BitmapArrayValueConverter.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
118
src/Ryujinx/UI/Helpers/ButtonKeyAssigner.cs
Normal file
118
src/Ryujinx/UI/Helpers/ButtonKeyAssigner.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
425
src/Ryujinx/UI/Helpers/ContentDialogHelper.cs
Normal file
425
src/Ryujinx/UI/Helpers/ContentDialogHelper.cs
Normal file
|
@ -0,0 +1,425 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.Core;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
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 static ContentDialogOverlayWindow _contentDialogOverlayWindow;
|
||||
|
||||
private 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,
|
||||
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;
|
||||
}
|
||||
|
||||
public 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;
|
||||
|
||||
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();
|
||||
|
||||
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(), new() },
|
||||
ColumnDefinitions = new ColumnDefinitions { new(GridLength.Auto), new() },
|
||||
|
||||
MinHeight = 80,
|
||||
};
|
||||
|
||||
SymbolIcon icon = new()
|
||||
{
|
||||
Symbol = (Symbol)symbol,
|
||||
Margin = new Thickness(10),
|
||||
FontSize = 40,
|
||||
VerticalAlignment = 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;
|
||||
bool isTopDialog = true;
|
||||
|
||||
Window parent = GetMainWindow();
|
||||
|
||||
if (_contentDialogOverlayWindow != null)
|
||||
{
|
||||
isTopDialog = false;
|
||||
}
|
||||
|
||||
if (parent is MainWindow window)
|
||||
{
|
||||
parent.Activate();
|
||||
|
||||
_contentDialogOverlayWindow = new ContentDialogOverlayWindow
|
||||
{
|
||||
Height = parent.Bounds.Height,
|
||||
Width = parent.Bounds.Width,
|
||||
Position = parent.PointToScreen(new Point()),
|
||||
ShowInTaskbar = false,
|
||||
};
|
||||
|
||||
parent.PositionChanged += OverlayOnPositionChanged;
|
||||
|
||||
void OverlayOnPositionChanged(object sender, PixelPointEventArgs e)
|
||||
{
|
||||
if (_contentDialogOverlayWindow is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_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 = ContentDialogResult.None;
|
||||
|
||||
Logger.Warning?.Print(LogClass.UI, "Content dialog overlay failed to populate. Default value has been returned.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (isTopDialog && _contentDialogOverlayWindow is not null)
|
||||
{
|
||||
_contentDialogOverlayWindow.Content = null;
|
||||
_contentDialogOverlayWindow.Close();
|
||||
_contentDialogOverlayWindow = null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Task ShowWindowAsync(Window dialogWindow, Window mainWindow = null)
|
||||
{
|
||||
mainWindow ??= GetMainWindow();
|
||||
|
||||
return dialogWindow.ShowDialog(_contentDialogOverlayWindow ?? mainWindow);
|
||||
}
|
||||
|
||||
private static Window GetMainWindow()
|
||||
{
|
||||
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime al)
|
||||
{
|
||||
foreach (Window item in al.Windows)
|
||||
{
|
||||
if (item is MainWindow window)
|
||||
{
|
||||
return window;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
9
src/Ryujinx/UI/Helpers/Glyph.cs
Normal file
9
src/Ryujinx/UI/Helpers/Glyph.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public enum Glyph
|
||||
{
|
||||
List,
|
||||
Grid,
|
||||
Chip,
|
||||
}
|
||||
}
|
42
src/Ryujinx/UI/Helpers/GlyphValueConverter.cs
Normal file
42
src/Ryujinx/UI/Helpers/GlyphValueConverter.cs
Normal file
|
@ -0,0 +1,42 @@
|
|||
using Avalonia.Markup.Xaml;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public class GlyphValueConverter : MarkupExtension
|
||||
{
|
||||
private readonly string _key;
|
||||
|
||||
private static readonly Dictionary<Glyph, string> _glyphs = new()
|
||||
{
|
||||
{ Glyph.List, char.ConvertFromUtf32((int)Symbol.List) },
|
||||
{ Glyph.Grid, char.ConvertFromUtf32((int)Symbol.ViewAll) },
|
||||
{ Glyph.Chip, char.ConvertFromUtf32(59748) },
|
||||
};
|
||||
|
||||
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)
|
||||
{
|
||||
return this[_key];
|
||||
}
|
||||
}
|
||||
}
|
46
src/Ryujinx/UI/Helpers/KeyValueConverter.cs
Normal file
46
src/Ryujinx/UI/Helpers/KeyValueConverter.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
43
src/Ryujinx/UI/Helpers/LocalizedNeverConverter.cs
Normal file
43
src/Ryujinx/UI/Helpers/LocalizedNeverConverter.cs
Normal file
|
@ -0,0 +1,43 @@
|
|||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.UI.Common.Helper;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// This <see cref="IValueConverter"/> makes sure that the string "Never" that's returned by <see cref="ValueFormatUtils.FormatDateTime"/> is properly localized in the Avalonia UI.
|
||||
/// After the Avalonia UI has been made the default and the GTK UI is removed, <see cref="ValueFormatUtils"/> should be updated to directly return a localized string.
|
||||
/// </summary>
|
||||
internal class LocalizedNeverConverter : MarkupExtension, IValueConverter
|
||||
{
|
||||
private static readonly LocalizedNeverConverter _instance = new();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not string valStr)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if (valStr == "Never")
|
||||
{
|
||||
return LocaleManager.Instance[LocaleKeys.Never];
|
||||
}
|
||||
|
||||
return valStr;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||
{
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
}
|
102
src/Ryujinx/UI/Helpers/LoggerAdapter.cs
Normal file
102
src/Ryujinx/UI/Helpers/LoggerAdapter.cs
Normal file
|
@ -0,0 +1,102 @@
|
|||
using Avalonia.Logging;
|
||||
using Avalonia.Utilities;
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
using AvaLogger = Avalonia.Logging.Logger;
|
||||
using AvaLogLevel = LogEventLevel;
|
||||
using RyuLogClass = LogClass;
|
||||
using RyuLogger = Ryujinx.Common.Logging.Logger;
|
||||
|
||||
internal class LoggerAdapter : 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(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();
|
||||
}
|
||||
}
|
||||
}
|
71
src/Ryujinx/UI/Helpers/MiniCommand.cs
Normal file
71
src/Ryujinx/UI/Helpers/MiniCommand.cs
Normal file
|
@ -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 readonly 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;
|
||||
}
|
||||
}
|
70
src/Ryujinx/UI/Helpers/NotificationHelper.cs
Normal file
70
src/Ryujinx/UI/Helpers/NotificationHelper.cs
Normal file
|
@ -0,0 +1,70 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Notifications;
|
||||
using Avalonia.Threading;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Common;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
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 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),
|
||||
};
|
||||
|
||||
var maybeAsyncWorkQueue = new Lazy<AsyncWorkQueue<Notification>>(
|
||||
() => new AsyncWorkQueue<Notification>(notification =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
_notificationManager.Show(notification);
|
||||
});
|
||||
},
|
||||
"UI.NotificationThread",
|
||||
_notifications),
|
||||
LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
|
||||
_notificationManager.TemplateApplied += (sender, args) =>
|
||||
{
|
||||
// NOTE: Force creation of the AsyncWorkQueue.
|
||||
_ = maybeAsyncWorkQueue.Value;
|
||||
};
|
||||
|
||||
host.Closing += (sender, args) =>
|
||||
{
|
||||
if (maybeAsyncWorkQueue.IsValueCreated)
|
||||
{
|
||||
maybeAsyncWorkQueue.Value.Dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
39
src/Ryujinx/UI/Helpers/OffscreenTextBox.cs
Normal file
39
src/Ryujinx/UI/Helpers/OffscreenTextBox.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public class OffscreenTextBox : TextBox
|
||||
{
|
||||
public static RoutedEvent<KeyEventArgs> GetKeyDownRoutedEvent()
|
||||
{
|
||||
return KeyDownEvent;
|
||||
}
|
||||
|
||||
public static 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,
|
||||
Source = this,
|
||||
RoutedEvent = TextInputEvent,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
28
src/Ryujinx/UI/Helpers/TimeZoneConverter.cs
Normal file
28
src/Ryujinx/UI/Helpers/TimeZoneConverter.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using Avalonia.Data.Converters;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using TimeZone = Ryujinx.Ava.UI.Models.TimeZone;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
internal class TimeZoneConverter : IValueConverter
|
||||
{
|
||||
public static TimeZoneConverter Instance = new();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var timeZone = (TimeZone)value;
|
||||
return string.Format("{0} {1} {2}", timeZone.UtcDifference, timeZone.Location, timeZone.Abbreviation);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
90
src/Ryujinx/UI/Helpers/UserErrorDialog.cs
Normal file
90
src/Ryujinx/UI/Helpers/UserErrorDialog.cs
Normal file
|
@ -0,0 +1,90 @@
|
|||
using Ryujinx.Ava.Common.Locale;
|
||||
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)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
src/Ryujinx/UI/Helpers/UserResult.cs
Normal file
12
src/Ryujinx/UI/Helpers/UserResult.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public enum UserResult
|
||||
{
|
||||
Ok,
|
||||
Yes,
|
||||
No,
|
||||
Abort,
|
||||
Cancel,
|
||||
None,
|
||||
}
|
||||
}
|
125
src/Ryujinx/UI/Helpers/Win32NativeInterop.cs
Normal file
125
src/Ryujinx/UI/Helpers/Win32NativeInterop.cs
Normal file
|
@ -0,0 +1,125 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
internal partial class Win32NativeInterop
|
||||
{
|
||||
[Flags]
|
||||
public enum ClassStyles : uint
|
||||
{
|
||||
CsClassdc = 0x40,
|
||||
CsOwndc = 0x20,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum WindowStyles : uint
|
||||
{
|
||||
WsChild = 0x40000000,
|
||||
}
|
||||
|
||||
public enum Cursors : uint
|
||||
{
|
||||
IdcArrow = 32512,
|
||||
}
|
||||
|
||||
[SuppressMessage("Design", "CA1069: Enums values should not be duplicated")]
|
||||
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.IdcArrow);
|
||||
}
|
||||
|
||||
[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, [In] byte[] pvAndPlane, [In] 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);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue