diff options
Diffstat (limited to 'src/Ryujinx/UI/Applet')
-rw-r--r-- | src/Ryujinx/UI/Applet/ErrorAppletDialog.cs | 31 | ||||
-rw-r--r-- | src/Ryujinx/UI/Applet/GtkDynamicTextInputHandler.cs | 108 | ||||
-rw-r--r-- | src/Ryujinx/UI/Applet/GtkHostUIHandler.cs | 200 | ||||
-rw-r--r-- | src/Ryujinx/UI/Applet/GtkHostUITheme.cs | 90 | ||||
-rw-r--r-- | src/Ryujinx/UI/Applet/SwkbdAppletDialog.cs | 127 |
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); + } + } +} |