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:
204
src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
Normal file
204
src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.HLE;
|
||||
using Ryujinx.HLE.HOS.Applets;
|
||||
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
|
||||
using Ryujinx.HLE.UI;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Applet
|
||||
{
|
||||
internal class AvaHostUIHandler : IHostUIHandler
|
||||
{
|
||||
private readonly MainWindow _parent;
|
||||
|
||||
public IHostUITheme HostUITheme { get; }
|
||||
|
||||
public AvaHostUIHandler(MainWindow parent)
|
||||
{
|
||||
_parent = parent;
|
||||
|
||||
HostUITheme = new AvaloniaHostUITheme(parent);
|
||||
}
|
||||
|
||||
public bool DisplayMessageDialog(ControllerAppletUIArgs args)
|
||||
{
|
||||
ManualResetEvent dialogCloseEvent = new(false);
|
||||
|
||||
bool okPressed = false;
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
var response = await ControllerAppletDialog.ShowControllerAppletDialog(_parent, args);
|
||||
if (response == UserResult.Ok)
|
||||
{
|
||||
okPressed = true;
|
||||
}
|
||||
|
||||
dialogCloseEvent.Set();
|
||||
});
|
||||
|
||||
dialogCloseEvent.WaitOne();
|
||||
|
||||
return okPressed;
|
||||
}
|
||||
|
||||
public bool DisplayMessageDialog(string title, string message)
|
||||
{
|
||||
ManualResetEvent dialogCloseEvent = new(false);
|
||||
|
||||
bool okPressed = false;
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ManualResetEvent deferEvent = new(false);
|
||||
|
||||
bool opened = false;
|
||||
|
||||
UserResult response = await ContentDialogHelper.ShowDeferredContentDialog(_parent,
|
||||
title,
|
||||
message,
|
||||
"",
|
||||
LocaleManager.Instance[LocaleKeys.DialogOpenSettingsWindowLabel],
|
||||
"",
|
||||
LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
|
||||
(int)Symbol.Important,
|
||||
deferEvent,
|
||||
async window =>
|
||||
{
|
||||
if (opened)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
opened = true;
|
||||
|
||||
_parent.SettingsWindow = new SettingsWindow(_parent.VirtualFileSystem, _parent.ContentManager);
|
||||
|
||||
await _parent.SettingsWindow.ShowDialog(window);
|
||||
|
||||
_parent.SettingsWindow = null;
|
||||
|
||||
opened = false;
|
||||
});
|
||||
|
||||
if (response == UserResult.Ok)
|
||||
{
|
||||
okPressed = true;
|
||||
}
|
||||
|
||||
dialogCloseEvent.Set();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogMessageDialogErrorExceptionMessage, ex));
|
||||
|
||||
dialogCloseEvent.Set();
|
||||
}
|
||||
});
|
||||
|
||||
dialogCloseEvent.WaitOne();
|
||||
|
||||
return okPressed;
|
||||
}
|
||||
|
||||
public bool DisplayInputDialog(SoftwareKeyboardUIArgs args, out string userText)
|
||||
{
|
||||
ManualResetEvent dialogCloseEvent = new(false);
|
||||
|
||||
bool okPressed = false;
|
||||
bool error = false;
|
||||
string inputText = args.InitialText ?? "";
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await SwkbdAppletDialog.ShowInputDialog(LocaleManager.Instance[LocaleKeys.SoftwareKeyboard], args);
|
||||
|
||||
if (response.Result == UserResult.Ok)
|
||||
{
|
||||
inputText = response.Input;
|
||||
okPressed = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = true;
|
||||
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogSoftwareKeyboardErrorExceptionMessage, ex));
|
||||
}
|
||||
finally
|
||||
{
|
||||
dialogCloseEvent.Set();
|
||||
}
|
||||
});
|
||||
|
||||
dialogCloseEvent.WaitOne();
|
||||
|
||||
userText = error ? null : inputText;
|
||||
|
||||
return error || okPressed;
|
||||
}
|
||||
|
||||
public void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value)
|
||||
{
|
||||
device.Configuration.UserChannelPersistence.ExecuteProgram(kind, value);
|
||||
_parent.ViewModel.AppHost?.Stop();
|
||||
}
|
||||
|
||||
public bool DisplayErrorAppletDialog(string title, string message, string[] buttons)
|
||||
{
|
||||
ManualResetEvent dialogCloseEvent = new(false);
|
||||
|
||||
bool showDetails = false;
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ErrorAppletWindow msgDialog = new(_parent, buttons, message)
|
||||
{
|
||||
Title = title,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen,
|
||||
Width = 400,
|
||||
};
|
||||
|
||||
object response = await msgDialog.Run();
|
||||
|
||||
if (response != null && buttons != null && buttons.Length > 1 && (int)response != buttons.Length - 1)
|
||||
{
|
||||
showDetails = true;
|
||||
}
|
||||
|
||||
dialogCloseEvent.Set();
|
||||
|
||||
msgDialog.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
dialogCloseEvent.Set();
|
||||
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogErrorAppletErrorExceptionMessage, ex));
|
||||
}
|
||||
});
|
||||
|
||||
dialogCloseEvent.WaitOne();
|
||||
|
||||
return showDetails;
|
||||
}
|
||||
|
||||
public IDynamicTextInputHandler CreateDynamicTextInputHandler()
|
||||
{
|
||||
return new AvaloniaDynamicTextInputHandler(_parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
162
src/Ryujinx/UI/Applet/AvaloniaDynamicTextInputHandler.cs
Normal file
162
src/Ryujinx/UI/Applet/AvaloniaDynamicTextInputHandler.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Threading;
|
||||
using Ryujinx.Ava.Input;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.HLE.UI;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using HidKey = Ryujinx.Common.Configuration.Hid.Key;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Applet
|
||||
{
|
||||
class AvaloniaDynamicTextInputHandler : IDynamicTextInputHandler
|
||||
{
|
||||
private MainWindow _parent;
|
||||
private readonly OffscreenTextBox _hiddenTextBox;
|
||||
private bool _canProcessInput;
|
||||
private IDisposable _textChangedSubscription;
|
||||
private IDisposable _selectionStartChangedSubscription;
|
||||
private IDisposable _selectionEndtextChangedSubscription;
|
||||
|
||||
public AvaloniaDynamicTextInputHandler(MainWindow parent)
|
||||
{
|
||||
_parent = parent;
|
||||
|
||||
(_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).KeyPressed += AvaloniaDynamicTextInputHandler_KeyPressed;
|
||||
(_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).KeyRelease += AvaloniaDynamicTextInputHandler_KeyRelease;
|
||||
(_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).TextInput += AvaloniaDynamicTextInputHandler_TextInput;
|
||||
|
||||
_hiddenTextBox = _parent.HiddenTextBox;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
_textChangedSubscription = _hiddenTextBox.GetObservable(TextBox.TextProperty).Subscribe(TextChanged);
|
||||
_selectionStartChangedSubscription = _hiddenTextBox.GetObservable(TextBox.SelectionStartProperty).Subscribe(SelectionChanged);
|
||||
_selectionEndtextChangedSubscription = _hiddenTextBox.GetObservable(TextBox.SelectionEndProperty).Subscribe(SelectionChanged);
|
||||
});
|
||||
}
|
||||
|
||||
private void TextChanged(string text)
|
||||
{
|
||||
TextChangedEvent?.Invoke(text ?? string.Empty, _hiddenTextBox.SelectionStart, _hiddenTextBox.SelectionEnd, true);
|
||||
}
|
||||
|
||||
private void SelectionChanged(int selection)
|
||||
{
|
||||
if (_hiddenTextBox.SelectionEnd < _hiddenTextBox.SelectionStart)
|
||||
{
|
||||
_hiddenTextBox.SelectionStart = _hiddenTextBox.SelectionEnd;
|
||||
}
|
||||
|
||||
TextChangedEvent?.Invoke(_hiddenTextBox.Text ?? string.Empty, _hiddenTextBox.SelectionStart, _hiddenTextBox.SelectionEnd, true);
|
||||
}
|
||||
|
||||
private void AvaloniaDynamicTextInputHandler_TextInput(object sender, string text)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (_canProcessInput)
|
||||
{
|
||||
_hiddenTextBox.SendText(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void AvaloniaDynamicTextInputHandler_KeyRelease(object sender, KeyEventArgs e)
|
||||
{
|
||||
var key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.Key);
|
||||
|
||||
if (!(KeyReleasedEvent?.Invoke(key)).GetValueOrDefault(true))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
e.RoutedEvent = OffscreenTextBox.GetKeyUpRoutedEvent();
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (_canProcessInput)
|
||||
{
|
||||
_hiddenTextBox.SendKeyUpEvent(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void AvaloniaDynamicTextInputHandler_KeyPressed(object sender, KeyEventArgs e)
|
||||
{
|
||||
var key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.Key);
|
||||
|
||||
if (!(KeyPressedEvent?.Invoke(key)).GetValueOrDefault(true))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
e.RoutedEvent = OffscreenTextBox.GetKeyUpRoutedEvent();
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (_canProcessInput)
|
||||
{
|
||||
_hiddenTextBox.SendKeyDownEvent(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public bool TextProcessingEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
return Volatile.Read(ref _canProcessInput);
|
||||
}
|
||||
set
|
||||
{
|
||||
Volatile.Write(ref _canProcessInput, value);
|
||||
}
|
||||
}
|
||||
|
||||
public event DynamicTextChangedHandler TextChangedEvent;
|
||||
public event KeyPressedHandler KeyPressedEvent;
|
||||
public event KeyReleasedHandler KeyReleasedEvent;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
(_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).KeyPressed -= AvaloniaDynamicTextInputHandler_KeyPressed;
|
||||
(_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).KeyRelease -= AvaloniaDynamicTextInputHandler_KeyRelease;
|
||||
(_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).TextInput -= AvaloniaDynamicTextInputHandler_TextInput;
|
||||
|
||||
_textChangedSubscription?.Dispose();
|
||||
_selectionStartChangedSubscription?.Dispose();
|
||||
_selectionEndtextChangedSubscription?.Dispose();
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
_hiddenTextBox.Clear();
|
||||
_parent.ViewModel.RendererHostControl.Focus();
|
||||
|
||||
_parent = null;
|
||||
});
|
||||
}
|
||||
|
||||
public void SetText(string text, int cursorBegin)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
_hiddenTextBox.Text = text;
|
||||
_hiddenTextBox.CaretIndex = cursorBegin;
|
||||
});
|
||||
}
|
||||
|
||||
public void SetText(string text, int cursorBegin, int cursorEnd)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
_hiddenTextBox.Text = text;
|
||||
_hiddenTextBox.SelectionStart = cursorBegin;
|
||||
_hiddenTextBox.SelectionEnd = cursorEnd;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/Ryujinx/UI/Applet/AvaloniaHostUITheme.cs
Normal file
41
src/Ryujinx/UI/Applet/AvaloniaHostUITheme.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Avalonia.Media;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.HLE.UI;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Applet
|
||||
{
|
||||
class AvaloniaHostUITheme : IHostUITheme
|
||||
{
|
||||
public AvaloniaHostUITheme(MainWindow parent)
|
||||
{
|
||||
FontFamily = OperatingSystem.IsWindows() && OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000) ? "Segoe UI Variable" : parent.FontFamily.Name;
|
||||
DefaultBackgroundColor = BrushToThemeColor(parent.Background);
|
||||
DefaultForegroundColor = BrushToThemeColor(parent.Foreground);
|
||||
DefaultBorderColor = BrushToThemeColor(parent.BorderBrush);
|
||||
SelectionBackgroundColor = BrushToThemeColor(parent.ViewControls.SearchBox.SelectionBrush);
|
||||
SelectionForegroundColor = BrushToThemeColor(parent.ViewControls.SearchBox.SelectionForegroundBrush);
|
||||
}
|
||||
|
||||
public string FontFamily { get; }
|
||||
|
||||
public ThemeColor DefaultBackgroundColor { get; }
|
||||
public ThemeColor DefaultForegroundColor { get; }
|
||||
public ThemeColor DefaultBorderColor { get; }
|
||||
public ThemeColor SelectionBackgroundColor { get; }
|
||||
public ThemeColor SelectionForegroundColor { get; }
|
||||
|
||||
private static ThemeColor BrushToThemeColor(IBrush brush)
|
||||
{
|
||||
if (brush is SolidColorBrush solidColor)
|
||||
{
|
||||
return new ThemeColor((float)solidColor.Color.A / 255,
|
||||
(float)solidColor.Color.R / 255,
|
||||
(float)solidColor.Color.G / 255,
|
||||
(float)solidColor.Color.B / 255);
|
||||
}
|
||||
|
||||
return new ThemeColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
145
src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml
Normal file
145
src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml
Normal file
@@ -0,0 +1,145 @@
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Applet.ControllerAppletDialog"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:applet="using:Ryujinx.Ava.UI.Applet"
|
||||
mc:Ignorable="d"
|
||||
Width="400"
|
||||
Focusable="True"
|
||||
x:DataType="applet:ControllerAppletDialog">
|
||||
<Grid
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="0 0 0 10"
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5">
|
||||
<StackPanel
|
||||
Spacing="10"
|
||||
Margin="10">
|
||||
<TextBlock
|
||||
Text="{locale:Locale ControllerAppletDescription}" />
|
||||
<TextBlock
|
||||
IsVisible="{Binding IsDocked}"
|
||||
FontWeight="Bold"
|
||||
Text="{locale:Locale ControllerAppletDocked}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Margin="0 0 10 0">
|
||||
<StackPanel
|
||||
Margin="10"
|
||||
Spacing="10"
|
||||
Orientation="Vertical">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
FontWeight="Bold"
|
||||
Text="{locale:Locale ControllerAppletControllers}" />
|
||||
<StackPanel
|
||||
Spacing="10"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<Image
|
||||
Height="50"
|
||||
Width="50"
|
||||
Stretch="Uniform"
|
||||
Source="{Binding ProControllerImage}"
|
||||
IsVisible="{Binding SupportsProController}" />
|
||||
<Image
|
||||
Height="50"
|
||||
Width="50"
|
||||
Stretch="Uniform"
|
||||
Source="{Binding JoyconPairImage}"
|
||||
IsVisible="{Binding SupportsJoyconPair}" />
|
||||
<Image
|
||||
Height="50"
|
||||
Width="50"
|
||||
Stretch="Uniform"
|
||||
Source="{Binding JoyconLeftImage}"
|
||||
IsVisible="{Binding SupportsLeftJoycon}" />
|
||||
<Image
|
||||
Height="50"
|
||||
Width="50"
|
||||
Stretch="Uniform"
|
||||
Source="{Binding JoyconRightImage}"
|
||||
IsVisible="{Binding SupportsRightJoycon}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5">
|
||||
<StackPanel
|
||||
Margin="10"
|
||||
Spacing="10"
|
||||
Orientation="Vertical">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
FontWeight="Bold"
|
||||
Text="{locale:Locale ControllerAppletPlayers}" />
|
||||
<Border Height="50">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
FontSize="40"
|
||||
FontWeight="Thin"
|
||||
Text="{Binding PlayerCount}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Panel
|
||||
Margin="0 24 0 0"
|
||||
Grid.Column="0"
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="2">
|
||||
<StackPanel
|
||||
Orientation="Horizontal"
|
||||
Spacing="10"
|
||||
HorizontalAlignment="Right">
|
||||
<Button
|
||||
Name="SaveButton"
|
||||
MinWidth="90"
|
||||
Command="{Binding OpenSettingsWindow}">
|
||||
<TextBlock Text="{locale:Locale DialogOpenSettingsWindowLabel}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="CancelButton"
|
||||
MinWidth="90"
|
||||
Command="{Binding Close}">
|
||||
<TextBlock Text="{locale:Locale SettingsButtonClose}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
140
src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs
Normal file
140
src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Svg.Skia;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.HLE.HOS.Applets;
|
||||
using Ryujinx.HLE.HOS.Services.Hid;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Applet
|
||||
{
|
||||
internal partial class ControllerAppletDialog : UserControl
|
||||
{
|
||||
private const string ProControllerResource = "Ryujinx/Assets/Icons/Controller_ProCon.svg";
|
||||
private const string JoyConPairResource = "Ryujinx/Assets/Icons/Controller_JoyConPair.svg";
|
||||
private const string JoyConLeftResource = "Ryujinx/Assets/Icons/Controller_JoyConLeft.svg";
|
||||
private const string JoyConRightResource = "Ryujinx/Assets/Icons/Controller_JoyConRight.svg";
|
||||
|
||||
public static SvgImage ProControllerImage => GetResource(ProControllerResource);
|
||||
public static SvgImage JoyconPairImage => GetResource(JoyConPairResource);
|
||||
public static SvgImage JoyconLeftImage => GetResource(JoyConLeftResource);
|
||||
public static SvgImage JoyconRightImage => GetResource(JoyConRightResource);
|
||||
|
||||
public string PlayerCount { get; set; } = "";
|
||||
public bool SupportsProController { get; set; }
|
||||
public bool SupportsLeftJoycon { get; set; }
|
||||
public bool SupportsRightJoycon { get; set; }
|
||||
public bool SupportsJoyconPair { get; set; }
|
||||
public bool IsDocked { get; set; }
|
||||
|
||||
private readonly MainWindow _mainWindow;
|
||||
|
||||
public ControllerAppletDialog(MainWindow mainWindow, ControllerAppletUIArgs args)
|
||||
{
|
||||
if (args.PlayerCountMin == args.PlayerCountMax)
|
||||
{
|
||||
PlayerCount = args.PlayerCountMin.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
PlayerCount = $"{args.PlayerCountMin} - {args.PlayerCountMax}";
|
||||
}
|
||||
|
||||
SupportsProController = (args.SupportedStyles & ControllerType.ProController) != 0;
|
||||
SupportsLeftJoycon = (args.SupportedStyles & ControllerType.JoyconLeft) != 0;
|
||||
SupportsRightJoycon = (args.SupportedStyles & ControllerType.JoyconRight) != 0;
|
||||
SupportsJoyconPair = (args.SupportedStyles & ControllerType.JoyconPair) != 0;
|
||||
|
||||
IsDocked = args.IsDocked;
|
||||
|
||||
_mainWindow = mainWindow;
|
||||
|
||||
DataContext = this;
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public ControllerAppletDialog(MainWindow mainWindow)
|
||||
{
|
||||
_mainWindow = mainWindow;
|
||||
DataContext = this;
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public static async Task<UserResult> ShowControllerAppletDialog(MainWindow window, ControllerAppletUIArgs args)
|
||||
{
|
||||
ContentDialog contentDialog = new();
|
||||
UserResult result = UserResult.Cancel;
|
||||
ControllerAppletDialog content = new(window, args);
|
||||
|
||||
contentDialog.Title = LocaleManager.Instance[LocaleKeys.DialogControllerAppletTitle];
|
||||
contentDialog.Content = content;
|
||||
|
||||
void Handler(ContentDialog sender, ContentDialogClosedEventArgs eventArgs)
|
||||
{
|
||||
if (eventArgs.Result == ContentDialogResult.Primary)
|
||||
{
|
||||
result = UserResult.Ok;
|
||||
}
|
||||
}
|
||||
|
||||
contentDialog.Closed += Handler;
|
||||
|
||||
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
||||
bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false));
|
||||
|
||||
contentDialog.Styles.Add(bottomBorder);
|
||||
|
||||
await ContentDialogHelper.ShowAsync(contentDialog);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static SvgImage GetResource(string path)
|
||||
{
|
||||
SvgImage image = new();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
SvgSource source = new(default(Uri));
|
||||
|
||||
source.Load(EmbeddedResources.GetStream(path));
|
||||
|
||||
image.Source = source;
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
public void OpenSettingsWindow()
|
||||
{
|
||||
if (_mainWindow.SettingsWindow == null)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
_mainWindow.SettingsWindow = new SettingsWindow(_mainWindow.VirtualFileSystem, _mainWindow.ContentManager);
|
||||
_mainWindow.SettingsWindow.NavPanel.Content = _mainWindow.SettingsWindow.InputPage;
|
||||
_mainWindow.SettingsWindow.NavPanel.SelectedItem = _mainWindow.SettingsWindow.NavPanel.MenuItems.ElementAt(1);
|
||||
|
||||
await ContentDialogHelper.ShowWindowAsync(_mainWindow.SettingsWindow, _mainWindow);
|
||||
_mainWindow.SettingsWindow = null;
|
||||
this.Close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
((ContentDialog)Parent)?.Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
using Gtk;
|
||||
using Ryujinx.UI.Common.Configuration;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Ryujinx.UI.Applet
|
||||
{
|
||||
internal class ErrorAppletDialog : MessageDialog
|
||||
{
|
||||
public ErrorAppletDialog(Window parentWindow, DialogFlags dialogFlags, MessageType messageType, string[] buttons) : base(parentWindow, dialogFlags, messageType, ButtonsType.None, null)
|
||||
{
|
||||
Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Logo_Ryujinx.png");
|
||||
|
||||
int responseId = 0;
|
||||
|
||||
if (buttons != null)
|
||||
{
|
||||
foreach (string buttonText in buttons)
|
||||
{
|
||||
AddButton(buttonText, responseId);
|
||||
responseId++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddButton("OK", 0);
|
||||
}
|
||||
|
||||
ShowAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/Ryujinx/UI/Applet/ErrorAppletWindow.axaml
Normal file
54
src/Ryujinx/UI/Applet/ErrorAppletWindow.axaml
Normal file
@@ -0,0 +1,54 @@
|
||||
<Window
|
||||
x:Class="Ryujinx.Ava.UI.Applet.ErrorAppletWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Title="{locale:Locale ErrorWindowTitle}"
|
||||
xmlns:views="using:Ryujinx.Ava.UI.Applet"
|
||||
Width="450"
|
||||
Height="340"
|
||||
CanResize="False"
|
||||
x:DataType="views:ErrorAppletWindow"
|
||||
SizeToContent="Height"
|
||||
mc:Ignorable="d"
|
||||
Focusable="True">
|
||||
<Grid
|
||||
Margin="20"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="2"
|
||||
Grid.Column="0"
|
||||
Height="80"
|
||||
MinWidth="50"
|
||||
Margin="5,10,20,10"
|
||||
Source="resm:Ryujinx.UI.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.UI.Common" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="10"
|
||||
VerticalAlignment="Stretch"
|
||||
Text="{Binding Message}"
|
||||
TextWrapping="Wrap" />
|
||||
<StackPanel
|
||||
Name="ButtonStack"
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal"
|
||||
Spacing="10" />
|
||||
</Grid>
|
||||
</Window>
|
||||
74
src/Ryujinx/UI/Applet/ErrorAppletWindow.axaml.cs
Normal file
74
src/Ryujinx/UI/Applet/ErrorAppletWindow.axaml.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Threading;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Applet
|
||||
{
|
||||
internal partial class ErrorAppletWindow : StyleableWindow
|
||||
{
|
||||
private readonly Window _owner;
|
||||
private object _buttonResponse;
|
||||
|
||||
public ErrorAppletWindow(Window owner, string[] buttons, string message)
|
||||
{
|
||||
_owner = owner;
|
||||
Message = message;
|
||||
DataContext = this;
|
||||
InitializeComponent();
|
||||
|
||||
int responseId = 0;
|
||||
|
||||
if (buttons != null)
|
||||
{
|
||||
foreach (string buttonText in buttons)
|
||||
{
|
||||
AddButton(buttonText, responseId);
|
||||
responseId++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddButton(LocaleManager.Instance[LocaleKeys.InputDialogOk], 0);
|
||||
}
|
||||
}
|
||||
|
||||
public ErrorAppletWindow()
|
||||
{
|
||||
DataContext = this;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public string Message { get; set; }
|
||||
|
||||
private void AddButton(string label, object tag)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
Button button = new() { Content = label, Tag = tag };
|
||||
|
||||
button.Click += Button_Click;
|
||||
ButtonStack.Children.Add(button);
|
||||
});
|
||||
}
|
||||
|
||||
private void Button_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button)
|
||||
{
|
||||
_buttonResponse = button.Tag;
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
public async Task<object> Run()
|
||||
{
|
||||
await ShowDialog(_owner);
|
||||
|
||||
return _buttonResponse;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
using Gtk;
|
||||
using Ryujinx.HLE.UI;
|
||||
using Ryujinx.Input.GTK3;
|
||||
using Ryujinx.UI.Widgets;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.UI.Applet
|
||||
{
|
||||
/// <summary>
|
||||
/// Class that forwards key events to a GTK Entry so they can be processed into text.
|
||||
/// </summary>
|
||||
internal class GtkDynamicTextInputHandler : IDynamicTextInputHandler
|
||||
{
|
||||
private readonly Window _parent;
|
||||
private readonly OffscreenWindow _inputToTextWindow = new();
|
||||
private readonly RawInputToTextEntry _inputToTextEntry = new();
|
||||
|
||||
private bool _canProcessInput;
|
||||
|
||||
public event DynamicTextChangedHandler TextChangedEvent;
|
||||
public event KeyPressedHandler KeyPressedEvent;
|
||||
public event KeyReleasedHandler KeyReleasedEvent;
|
||||
|
||||
public bool TextProcessingEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
return Volatile.Read(ref _canProcessInput);
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
Volatile.Write(ref _canProcessInput, value);
|
||||
}
|
||||
}
|
||||
|
||||
public GtkDynamicTextInputHandler(Window parent)
|
||||
{
|
||||
_parent = parent;
|
||||
_parent.KeyPressEvent += HandleKeyPressEvent;
|
||||
_parent.KeyReleaseEvent += HandleKeyReleaseEvent;
|
||||
|
||||
_inputToTextWindow.Add(_inputToTextEntry);
|
||||
|
||||
_inputToTextEntry.TruncateMultiline = true;
|
||||
|
||||
// Start with input processing turned off so the text box won't accumulate text
|
||||
// if the user is playing on the keyboard.
|
||||
_canProcessInput = false;
|
||||
}
|
||||
|
||||
[GLib.ConnectBefore()]
|
||||
private void HandleKeyPressEvent(object o, KeyPressEventArgs args)
|
||||
{
|
||||
var key = (Ryujinx.Common.Configuration.Hid.Key)GTK3MappingHelper.ToInputKey(args.Event.Key);
|
||||
|
||||
if (!(KeyPressedEvent?.Invoke(key)).GetValueOrDefault(true))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_canProcessInput)
|
||||
{
|
||||
_inputToTextEntry.SendKeyPressEvent(o, args);
|
||||
_inputToTextEntry.GetSelectionBounds(out int selectionStart, out int selectionEnd);
|
||||
TextChangedEvent?.Invoke(_inputToTextEntry.Text, selectionStart, selectionEnd, _inputToTextEntry.OverwriteMode);
|
||||
}
|
||||
}
|
||||
|
||||
[GLib.ConnectBefore()]
|
||||
private void HandleKeyReleaseEvent(object o, KeyReleaseEventArgs args)
|
||||
{
|
||||
var key = (Ryujinx.Common.Configuration.Hid.Key)GTK3MappingHelper.ToInputKey(args.Event.Key);
|
||||
|
||||
if (!(KeyReleasedEvent?.Invoke(key)).GetValueOrDefault(true))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_canProcessInput)
|
||||
{
|
||||
// TODO (caian): This solution may have problems if the pause is sent after a key press
|
||||
// and before a key release. But for now GTK Entry does not seem to use release events.
|
||||
_inputToTextEntry.SendKeyReleaseEvent(o, args);
|
||||
_inputToTextEntry.GetSelectionBounds(out int selectionStart, out int selectionEnd);
|
||||
TextChangedEvent?.Invoke(_inputToTextEntry.Text, selectionStart, selectionEnd, _inputToTextEntry.OverwriteMode);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetText(string text, int cursorBegin)
|
||||
{
|
||||
_inputToTextEntry.Text = text;
|
||||
_inputToTextEntry.Position = cursorBegin;
|
||||
}
|
||||
|
||||
public void SetText(string text, int cursorBegin, int cursorEnd)
|
||||
{
|
||||
_inputToTextEntry.Text = text;
|
||||
_inputToTextEntry.SelectRegion(cursorBegin, cursorEnd);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_parent.KeyPressEvent -= HandleKeyPressEvent;
|
||||
_parent.KeyReleaseEvent -= HandleKeyReleaseEvent;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
using Gtk;
|
||||
using Ryujinx.HLE.HOS.Applets;
|
||||
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
|
||||
using Ryujinx.HLE.UI;
|
||||
using Ryujinx.UI.Widgets;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.UI.Applet
|
||||
{
|
||||
internal class GtkHostUIHandler : IHostUIHandler
|
||||
{
|
||||
private readonly Window _parent;
|
||||
|
||||
public IHostUITheme HostUITheme { get; }
|
||||
|
||||
public GtkHostUIHandler(Window parent)
|
||||
{
|
||||
_parent = parent;
|
||||
|
||||
HostUITheme = new GtkHostUITheme(parent);
|
||||
}
|
||||
|
||||
public bool DisplayMessageDialog(ControllerAppletUIArgs args)
|
||||
{
|
||||
string playerCount = args.PlayerCountMin == args.PlayerCountMax ? $"exactly {args.PlayerCountMin}" : $"{args.PlayerCountMin}-{args.PlayerCountMax}";
|
||||
|
||||
string message = $"Application requests <b>{playerCount}</b> player(s) with:\n\n"
|
||||
+ $"<tt><b>TYPES:</b> {args.SupportedStyles}</tt>\n\n"
|
||||
+ $"<tt><b>PLAYERS:</b> {string.Join(", ", args.SupportedPlayers)}</tt>\n\n"
|
||||
+ (args.IsDocked ? "Docked mode set. <tt>Handheld</tt> is also invalid.\n\n" : "")
|
||||
+ "<i>Please reconfigure Input now and then press OK.</i>";
|
||||
|
||||
return DisplayMessageDialog("Controller Applet", message);
|
||||
}
|
||||
|
||||
public bool DisplayMessageDialog(string title, string message)
|
||||
{
|
||||
ManualResetEvent dialogCloseEvent = new(false);
|
||||
|
||||
bool okPressed = false;
|
||||
|
||||
Application.Invoke(delegate
|
||||
{
|
||||
MessageDialog msgDialog = null;
|
||||
|
||||
try
|
||||
{
|
||||
msgDialog = new MessageDialog(_parent, DialogFlags.DestroyWithParent, MessageType.Info, ButtonsType.Ok, null)
|
||||
{
|
||||
Title = title,
|
||||
Text = message,
|
||||
UseMarkup = true,
|
||||
};
|
||||
|
||||
msgDialog.SetDefaultSize(400, 0);
|
||||
|
||||
msgDialog.Response += (object o, ResponseArgs args) =>
|
||||
{
|
||||
if (args.ResponseId == ResponseType.Ok)
|
||||
{
|
||||
okPressed = true;
|
||||
}
|
||||
|
||||
dialogCloseEvent.Set();
|
||||
msgDialog?.Dispose();
|
||||
};
|
||||
|
||||
msgDialog.Show();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GtkDialog.CreateErrorDialog($"Error displaying Message Dialog: {ex}");
|
||||
|
||||
dialogCloseEvent.Set();
|
||||
}
|
||||
});
|
||||
|
||||
dialogCloseEvent.WaitOne();
|
||||
|
||||
return okPressed;
|
||||
}
|
||||
|
||||
public bool DisplayInputDialog(SoftwareKeyboardUIArgs args, out string userText)
|
||||
{
|
||||
ManualResetEvent dialogCloseEvent = new(false);
|
||||
|
||||
bool okPressed = false;
|
||||
bool error = false;
|
||||
string inputText = args.InitialText ?? "";
|
||||
|
||||
Application.Invoke(delegate
|
||||
{
|
||||
try
|
||||
{
|
||||
var swkbdDialog = new SwkbdAppletDialog(_parent)
|
||||
{
|
||||
Title = "Software Keyboard",
|
||||
Text = args.HeaderText,
|
||||
SecondaryText = args.SubtitleText,
|
||||
};
|
||||
|
||||
swkbdDialog.InputEntry.Text = inputText;
|
||||
swkbdDialog.InputEntry.PlaceholderText = args.GuideText;
|
||||
swkbdDialog.OkButton.Label = args.SubmitText;
|
||||
|
||||
swkbdDialog.SetInputLengthValidation(args.StringLengthMin, args.StringLengthMax);
|
||||
swkbdDialog.SetInputValidation(args.KeyboardMode);
|
||||
|
||||
if (swkbdDialog.Run() == (int)ResponseType.Ok)
|
||||
{
|
||||
inputText = swkbdDialog.InputEntry.Text;
|
||||
okPressed = true;
|
||||
}
|
||||
|
||||
swkbdDialog.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = true;
|
||||
|
||||
GtkDialog.CreateErrorDialog($"Error displaying Software Keyboard: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
dialogCloseEvent.Set();
|
||||
}
|
||||
});
|
||||
|
||||
dialogCloseEvent.WaitOne();
|
||||
|
||||
userText = error ? null : inputText;
|
||||
|
||||
return error || okPressed;
|
||||
}
|
||||
|
||||
public void ExecuteProgram(HLE.Switch device, ProgramSpecifyKind kind, ulong value)
|
||||
{
|
||||
device.Configuration.UserChannelPersistence.ExecuteProgram(kind, value);
|
||||
((MainWindow)_parent).RendererWidget?.Exit();
|
||||
}
|
||||
|
||||
public bool DisplayErrorAppletDialog(string title, string message, string[] buttons)
|
||||
{
|
||||
ManualResetEvent dialogCloseEvent = new(false);
|
||||
|
||||
bool showDetails = false;
|
||||
|
||||
Application.Invoke(delegate
|
||||
{
|
||||
try
|
||||
{
|
||||
ErrorAppletDialog msgDialog = new(_parent, DialogFlags.DestroyWithParent, MessageType.Error, buttons)
|
||||
{
|
||||
Title = title,
|
||||
Text = message,
|
||||
UseMarkup = true,
|
||||
WindowPosition = WindowPosition.CenterAlways,
|
||||
};
|
||||
|
||||
msgDialog.SetDefaultSize(400, 0);
|
||||
|
||||
msgDialog.Response += (object o, ResponseArgs args) =>
|
||||
{
|
||||
if (buttons != null)
|
||||
{
|
||||
if (buttons.Length > 1)
|
||||
{
|
||||
if (args.ResponseId != (ResponseType)(buttons.Length - 1))
|
||||
{
|
||||
showDetails = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialogCloseEvent.Set();
|
||||
msgDialog?.Dispose();
|
||||
};
|
||||
|
||||
msgDialog.Show();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GtkDialog.CreateErrorDialog($"Error displaying ErrorApplet Dialog: {ex}");
|
||||
|
||||
dialogCloseEvent.Set();
|
||||
}
|
||||
});
|
||||
|
||||
dialogCloseEvent.WaitOne();
|
||||
|
||||
return showDetails;
|
||||
}
|
||||
|
||||
public IDynamicTextInputHandler CreateDynamicTextInputHandler()
|
||||
{
|
||||
return new GtkDynamicTextInputHandler(_parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
using Gtk;
|
||||
using Ryujinx.HLE.UI;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Ryujinx.UI.Applet
|
||||
{
|
||||
internal class GtkHostUITheme : IHostUITheme
|
||||
{
|
||||
private const int RenderSurfaceWidth = 32;
|
||||
private const int RenderSurfaceHeight = 32;
|
||||
|
||||
public string FontFamily { get; private set; }
|
||||
|
||||
public ThemeColor DefaultBackgroundColor { get; }
|
||||
public ThemeColor DefaultForegroundColor { get; }
|
||||
public ThemeColor DefaultBorderColor { get; }
|
||||
public ThemeColor SelectionBackgroundColor { get; }
|
||||
public ThemeColor SelectionForegroundColor { get; }
|
||||
|
||||
public GtkHostUITheme(Window parent)
|
||||
{
|
||||
Entry entry = new();
|
||||
entry.SetStateFlags(StateFlags.Selected, true);
|
||||
|
||||
// Get the font and some colors directly from GTK.
|
||||
FontFamily = entry.PangoContext.FontDescription.Family;
|
||||
|
||||
// Get foreground colors from the style context.
|
||||
|
||||
var defaultForegroundColor = entry.StyleContext.GetColor(StateFlags.Normal);
|
||||
var selectedForegroundColor = entry.StyleContext.GetColor(StateFlags.Selected);
|
||||
|
||||
DefaultForegroundColor = new ThemeColor((float)defaultForegroundColor.Alpha, (float)defaultForegroundColor.Red, (float)defaultForegroundColor.Green, (float)defaultForegroundColor.Blue);
|
||||
SelectionForegroundColor = new ThemeColor((float)selectedForegroundColor.Alpha, (float)selectedForegroundColor.Red, (float)selectedForegroundColor.Green, (float)selectedForegroundColor.Blue);
|
||||
|
||||
ListBoxRow row = new();
|
||||
row.SetStateFlags(StateFlags.Selected, true);
|
||||
|
||||
// Request the main thread to render some UI elements to an image to get an approximation for the color.
|
||||
// NOTE (caian): This will only take the color of the top-left corner of the background, which may be incorrect
|
||||
// if someone provides a custom style with a gradient or image.
|
||||
|
||||
using (var surface = new Cairo.ImageSurface(Cairo.Format.Argb32, RenderSurfaceWidth, RenderSurfaceHeight))
|
||||
using (var context = new Cairo.Context(surface))
|
||||
{
|
||||
context.SetSourceRGBA(1, 1, 1, 1);
|
||||
context.Rectangle(0, 0, RenderSurfaceWidth, RenderSurfaceHeight);
|
||||
context.Fill();
|
||||
|
||||
// The background color must be from the main Window because entry uses a different color.
|
||||
parent.StyleContext.RenderBackground(context, 0, 0, RenderSurfaceWidth, RenderSurfaceHeight);
|
||||
|
||||
DefaultBackgroundColor = ToThemeColor(surface.Data);
|
||||
|
||||
context.SetSourceRGBA(1, 1, 1, 1);
|
||||
context.Rectangle(0, 0, RenderSurfaceWidth, RenderSurfaceHeight);
|
||||
context.Fill();
|
||||
|
||||
// Use the background color of the list box row when selected as the text box frame color because they are the
|
||||
// same in the default theme.
|
||||
row.StyleContext.RenderBackground(context, 0, 0, RenderSurfaceWidth, RenderSurfaceHeight);
|
||||
|
||||
DefaultBorderColor = ToThemeColor(surface.Data);
|
||||
}
|
||||
|
||||
// Use the border color as the text selection color.
|
||||
SelectionBackgroundColor = DefaultBorderColor;
|
||||
}
|
||||
|
||||
private static ThemeColor ToThemeColor(byte[] data)
|
||||
{
|
||||
Debug.Assert(data.Length == 4 * RenderSurfaceWidth * RenderSurfaceHeight);
|
||||
|
||||
// Take the center-bottom pixel of the surface.
|
||||
int position = 4 * (RenderSurfaceWidth * (RenderSurfaceHeight - 1) + RenderSurfaceWidth / 2);
|
||||
|
||||
if (position + 4 > data.Length)
|
||||
{
|
||||
return new ThemeColor(1, 0, 0, 0);
|
||||
}
|
||||
|
||||
float a = data[position + 3] / 255.0f;
|
||||
float r = data[position + 2] / 255.0f;
|
||||
float g = data[position + 1] / 255.0f;
|
||||
float b = data[position + 0] / 255.0f;
|
||||
|
||||
return new ThemeColor(a, r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml
Normal file
67
src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml
Normal file
@@ -0,0 +1,67 @@
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Controls.SwkbdAppletDialog"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="using:Ryujinx.Ava.UI.Controls"
|
||||
Width="400"
|
||||
x:DataType="views:SwkbdAppletDialog"
|
||||
mc:Ignorable="d"
|
||||
Focusable="True">
|
||||
<Grid
|
||||
Margin="20"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="5"
|
||||
Height="80"
|
||||
MinWidth="50"
|
||||
Margin="5,10,20,10"
|
||||
VerticalAlignment="Center"
|
||||
Source="resm:Ryujinx.UI.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.UI.Common" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="5"
|
||||
Text="{Binding MainText}"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Margin="5"
|
||||
Text="{Binding SecondaryText}"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBox
|
||||
Name="Input"
|
||||
Grid.Row="3"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
Focusable="True"
|
||||
KeyUp="Message_KeyUp"
|
||||
Text="{Binding Message}"
|
||||
TextInput="Message_TextInput"
|
||||
TextWrapping="Wrap"
|
||||
UseFloatingWatermark="True" />
|
||||
<TextBlock
|
||||
Name="Error"
|
||||
Grid.Row="4"
|
||||
Grid.Column="1"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
183
src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml.cs
Normal file
183
src/Ryujinx/UI/Applet/SwkbdAppletDialog.axaml.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.HLE.HOS.Applets;
|
||||
using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
internal partial class SwkbdAppletDialog : UserControl
|
||||
{
|
||||
private Predicate<int> _checkLength = _ => true;
|
||||
private Predicate<string> _checkInput = _ => true;
|
||||
private int _inputMax;
|
||||
private int _inputMin;
|
||||
private readonly string _placeholder;
|
||||
|
||||
private ContentDialog _host;
|
||||
|
||||
public SwkbdAppletDialog(string mainText, string secondaryText, string placeholder, string message)
|
||||
{
|
||||
MainText = mainText;
|
||||
SecondaryText = secondaryText;
|
||||
Message = message ?? "";
|
||||
DataContext = this;
|
||||
_placeholder = placeholder;
|
||||
InitializeComponent();
|
||||
|
||||
Input.Watermark = _placeholder;
|
||||
|
||||
Input.AddHandler(TextInputEvent, Message_TextInput, RoutingStrategies.Tunnel, true);
|
||||
}
|
||||
|
||||
public SwkbdAppletDialog()
|
||||
{
|
||||
DataContext = this;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnGotFocus(GotFocusEventArgs e)
|
||||
{
|
||||
// FIXME: This does not work. Might be a bug in Avalonia with DialogHost
|
||||
// Currently focus will be redirected to the overlay window instead.
|
||||
Input.Focus();
|
||||
}
|
||||
|
||||
public string Message { get; set; } = "";
|
||||
public string MainText { get; set; } = "";
|
||||
public string SecondaryText { get; set; } = "";
|
||||
|
||||
public static async Task<(UserResult Result, string Input)> ShowInputDialog(string title, SoftwareKeyboardUIArgs args)
|
||||
{
|
||||
ContentDialog contentDialog = new();
|
||||
|
||||
UserResult result = UserResult.Cancel;
|
||||
|
||||
SwkbdAppletDialog content = new(args.HeaderText, args.SubtitleText, args.GuideText, args.InitialText);
|
||||
|
||||
string input = string.Empty;
|
||||
|
||||
content.SetInputLengthValidation(args.StringLengthMin, args.StringLengthMax);
|
||||
content.SetInputValidation(args.KeyboardMode);
|
||||
|
||||
content._host = contentDialog;
|
||||
contentDialog.Title = title;
|
||||
contentDialog.PrimaryButtonText = args.SubmitText;
|
||||
contentDialog.IsPrimaryButtonEnabled = content._checkLength(content.Message.Length);
|
||||
contentDialog.SecondaryButtonText = "";
|
||||
contentDialog.CloseButtonText = LocaleManager.Instance[LocaleKeys.InputDialogCancel];
|
||||
contentDialog.Content = content;
|
||||
|
||||
void Handler(ContentDialog sender, ContentDialogClosedEventArgs eventArgs)
|
||||
{
|
||||
if (eventArgs.Result == ContentDialogResult.Primary)
|
||||
{
|
||||
result = UserResult.Ok;
|
||||
input = content.Input.Text;
|
||||
}
|
||||
}
|
||||
|
||||
contentDialog.Closed += Handler;
|
||||
|
||||
await ContentDialogHelper.ShowAsync(contentDialog);
|
||||
|
||||
return (result, input);
|
||||
}
|
||||
|
||||
private void ApplyValidationInfo(string text)
|
||||
{
|
||||
Error.IsVisible = !string.IsNullOrEmpty(text);
|
||||
Error.Text = text;
|
||||
}
|
||||
|
||||
public void SetInputLengthValidation(int min, int max)
|
||||
{
|
||||
_inputMin = Math.Min(min, max);
|
||||
_inputMax = Math.Max(min, max);
|
||||
|
||||
Error.IsVisible = false;
|
||||
Error.FontStyle = FontStyle.Italic;
|
||||
|
||||
string validationInfoText = "";
|
||||
|
||||
if (_inputMin <= 0 && _inputMax == int.MaxValue) // Disable.
|
||||
{
|
||||
Error.IsVisible = false;
|
||||
|
||||
_checkLength = length => true;
|
||||
}
|
||||
else if (_inputMin > 0 && _inputMax == int.MaxValue)
|
||||
{
|
||||
validationInfoText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SwkbdMinCharacters, _inputMin);
|
||||
|
||||
_checkLength = length => _inputMin <= length;
|
||||
}
|
||||
else
|
||||
{
|
||||
validationInfoText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SwkbdMinRangeCharacters, _inputMin, _inputMax);
|
||||
|
||||
_checkLength = length => _inputMin <= length && length <= _inputMax;
|
||||
}
|
||||
|
||||
ApplyValidationInfo(validationInfoText);
|
||||
Message_TextInput(this, new TextInputEventArgs());
|
||||
}
|
||||
|
||||
private void SetInputValidation(KeyboardMode mode)
|
||||
{
|
||||
string validationInfoText = Error.Text;
|
||||
string localeText;
|
||||
switch (mode)
|
||||
{
|
||||
case KeyboardMode.Numeric:
|
||||
localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeNumeric);
|
||||
validationInfoText = string.IsNullOrEmpty(validationInfoText) ? localeText : string.Join("\n", validationInfoText, localeText);
|
||||
_checkInput = text => text.All(NumericCharacterValidation.IsNumeric);
|
||||
break;
|
||||
case KeyboardMode.Alphabet:
|
||||
localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeAlphabet);
|
||||
validationInfoText = string.IsNullOrEmpty(validationInfoText) ? localeText : string.Join("\n", validationInfoText, localeText);
|
||||
_checkInput = text => text.All(value => !CJKCharacterValidation.IsCJK(value));
|
||||
break;
|
||||
case KeyboardMode.ASCII:
|
||||
localeText = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SoftwareKeyboardModeASCII);
|
||||
validationInfoText = string.IsNullOrEmpty(validationInfoText) ? localeText : string.Join("\n", validationInfoText, localeText);
|
||||
_checkInput = text => text.All(char.IsAscii);
|
||||
break;
|
||||
default:
|
||||
_checkInput = _ => true;
|
||||
break;
|
||||
}
|
||||
|
||||
ApplyValidationInfo(validationInfoText);
|
||||
Message_TextInput(this, new TextInputEventArgs());
|
||||
}
|
||||
|
||||
private void Message_TextInput(object sender, TextInputEventArgs e)
|
||||
{
|
||||
if (_host != null)
|
||||
{
|
||||
_host.IsPrimaryButtonEnabled = _checkLength(Message.Length) && _checkInput(Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void Message_KeyUp(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter && _host.IsPrimaryButtonEnabled)
|
||||
{
|
||||
_host.Hide(ContentDialogResult.Primary);
|
||||
}
|
||||
else
|
||||
{
|
||||
_host.IsPrimaryButtonEnabled = _checkLength(Message.Length) && _checkInput(Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
using Gtk;
|
||||
using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.UI.Applet
|
||||
{
|
||||
public class SwkbdAppletDialog : MessageDialog
|
||||
{
|
||||
private int _inputMin;
|
||||
private int _inputMax;
|
||||
#pragma warning disable IDE0052 // Remove unread private member
|
||||
private KeyboardMode _mode;
|
||||
#pragma warning restore IDE0052
|
||||
|
||||
private string _validationInfoText = "";
|
||||
|
||||
private Predicate<int> _checkLength = _ => true;
|
||||
private Predicate<string> _checkInput = _ => true;
|
||||
|
||||
private readonly Label _validationInfo;
|
||||
|
||||
public Entry InputEntry { get; }
|
||||
public Button OkButton { get; }
|
||||
public Button CancelButton { get; }
|
||||
|
||||
public SwkbdAppletDialog(Window parent) : base(parent, DialogFlags.Modal | DialogFlags.DestroyWithParent, MessageType.Question, ButtonsType.None, null)
|
||||
{
|
||||
SetDefaultSize(300, 0);
|
||||
|
||||
_validationInfo = new Label()
|
||||
{
|
||||
Visible = false,
|
||||
};
|
||||
|
||||
InputEntry = new Entry()
|
||||
{
|
||||
Visible = true,
|
||||
};
|
||||
|
||||
InputEntry.Activated += OnInputActivated;
|
||||
InputEntry.Changed += OnInputChanged;
|
||||
|
||||
OkButton = (Button)AddButton("OK", ResponseType.Ok);
|
||||
CancelButton = (Button)AddButton("Cancel", ResponseType.Cancel);
|
||||
|
||||
((Box)MessageArea).PackEnd(_validationInfo, true, true, 0);
|
||||
((Box)MessageArea).PackEnd(InputEntry, true, true, 4);
|
||||
}
|
||||
|
||||
private void ApplyValidationInfo()
|
||||
{
|
||||
_validationInfo.Visible = !string.IsNullOrEmpty(_validationInfoText);
|
||||
_validationInfo.Markup = _validationInfoText;
|
||||
}
|
||||
|
||||
public void SetInputLengthValidation(int min, int max)
|
||||
{
|
||||
_inputMin = Math.Min(min, max);
|
||||
_inputMax = Math.Max(min, max);
|
||||
|
||||
_validationInfo.Visible = false;
|
||||
|
||||
if (_inputMin <= 0 && _inputMax == int.MaxValue) // Disable.
|
||||
{
|
||||
_validationInfo.Visible = false;
|
||||
|
||||
_checkLength = _ => true;
|
||||
}
|
||||
else if (_inputMin > 0 && _inputMax == int.MaxValue)
|
||||
{
|
||||
_validationInfoText = $"<i>Must be at least {_inputMin} characters long.</i> ";
|
||||
|
||||
_checkLength = length => _inputMin <= length;
|
||||
}
|
||||
else
|
||||
{
|
||||
_validationInfoText = $"<i>Must be {_inputMin}-{_inputMax} characters long.</i> ";
|
||||
|
||||
_checkLength = length => _inputMin <= length && length <= _inputMax;
|
||||
}
|
||||
|
||||
ApplyValidationInfo();
|
||||
OnInputChanged(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void SetInputValidation(KeyboardMode mode)
|
||||
{
|
||||
_mode = mode;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case KeyboardMode.Numeric:
|
||||
_validationInfoText += "<i>Must be 0-9 or '.' only.</i>";
|
||||
_checkInput = text => text.All(NumericCharacterValidation.IsNumeric);
|
||||
break;
|
||||
case KeyboardMode.Alphabet:
|
||||
_validationInfoText += "<i>Must be non CJK-characters only.</i>";
|
||||
_checkInput = text => text.All(value => !CJKCharacterValidation.IsCJK(value));
|
||||
break;
|
||||
case KeyboardMode.ASCII:
|
||||
_validationInfoText += "<i>Must be ASCII text only.</i>";
|
||||
_checkInput = text => text.All(char.IsAscii);
|
||||
break;
|
||||
default:
|
||||
_checkInput = _ => true;
|
||||
break;
|
||||
}
|
||||
|
||||
ApplyValidationInfo();
|
||||
OnInputChanged(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void OnInputActivated(object sender, EventArgs e)
|
||||
{
|
||||
if (OkButton.IsSensitive)
|
||||
{
|
||||
Respond(ResponseType.Ok);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnInputChanged(object sender, EventArgs e)
|
||||
{
|
||||
OkButton.Sensitive = _checkLength(InputEntry.Text.Length) && _checkInput(InputEntry.Text);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user