aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx/UI/Applet
diff options
context:
space:
mode:
Diffstat (limited to 'src/Ryujinx/UI/Applet')
-rw-r--r--src/Ryujinx/UI/Applet/ErrorAppletDialog.cs31
-rw-r--r--src/Ryujinx/UI/Applet/GtkDynamicTextInputHandler.cs108
-rw-r--r--src/Ryujinx/UI/Applet/GtkHostUIHandler.cs200
-rw-r--r--src/Ryujinx/UI/Applet/GtkHostUITheme.cs90
-rw-r--r--src/Ryujinx/UI/Applet/SwkbdAppletDialog.cs127
5 files changed, 556 insertions, 0 deletions
diff --git a/src/Ryujinx/UI/Applet/ErrorAppletDialog.cs b/src/Ryujinx/UI/Applet/ErrorAppletDialog.cs
new file mode 100644
index 00000000..7f8cc0e9
--- /dev/null
+++ b/src/Ryujinx/UI/Applet/ErrorAppletDialog.cs
@@ -0,0 +1,31 @@
+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();
+ }
+ }
+}
diff --git a/src/Ryujinx/UI/Applet/GtkDynamicTextInputHandler.cs b/src/Ryujinx/UI/Applet/GtkDynamicTextInputHandler.cs
new file mode 100644
index 00000000..0e560b78
--- /dev/null
+++ b/src/Ryujinx/UI/Applet/GtkDynamicTextInputHandler.cs
@@ -0,0 +1,108 @@
+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;
+ }
+ }
+}
diff --git a/src/Ryujinx/UI/Applet/GtkHostUIHandler.cs b/src/Ryujinx/UI/Applet/GtkHostUIHandler.cs
new file mode 100644
index 00000000..1d918d21
--- /dev/null
+++ b/src/Ryujinx/UI/Applet/GtkHostUIHandler.cs
@@ -0,0 +1,200 @@
+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);
+ }
+ }
+}
diff --git a/src/Ryujinx/UI/Applet/GtkHostUITheme.cs b/src/Ryujinx/UI/Applet/GtkHostUITheme.cs
new file mode 100644
index 00000000..52d1123b
--- /dev/null
+++ b/src/Ryujinx/UI/Applet/GtkHostUITheme.cs
@@ -0,0 +1,90 @@
+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);
+ }
+ }
+}
diff --git a/src/Ryujinx/UI/Applet/SwkbdAppletDialog.cs b/src/Ryujinx/UI/Applet/SwkbdAppletDialog.cs
new file mode 100644
index 00000000..8045da91
--- /dev/null
+++ b/src/Ryujinx/UI/Applet/SwkbdAppletDialog.cs
@@ -0,0 +1,127 @@
+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);
+ }
+ }
+}