diff options
author | Caian Benedicto <caianbene@gmail.com> | 2021-10-12 16:54:21 -0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-10-12 21:54:21 +0200 |
commit | 380b95bc59e7dc419f89df951cdc086e792cb0ff (patch) | |
tree | 59a636b48db991d8e13132d7d3f41464d9b04b24 /Ryujinx | |
parent | 69093cf2d69490862aff974f170cee63a0016fd0 (diff) |
Inline software keyboard without input pop up dialog (#2180)
* Initial implementation
* Refactor dynamic text input keys out to facilitate configuration via UI
* Fix code styling
* Add per applet indirect layer handles
* Remove static functions from SoftwareKeyboardRenderer
* Remove inline keyboard reset delay
* Remove inline keyboard V2 responses
* Add inline keyboard soft-lock recovering
* Add comments
* Forward accept and cancel key names to the keyboard and add soft-lock prevention line
* Add dummy window to handle paste events
* Rework inline keyboard state machine and graphics
* Implement IHostUiHandler interfaces on headless WindowBase class
* Add inline keyboard assets
* Fix coding style
* Fix coding style
* Change mode cycling shortcut to F6
* Fix invalid calc size error in games using extended calc
* Remove unnecessary namespaces
Diffstat (limited to 'Ryujinx')
-rw-r--r-- | Ryujinx/Input/GTK3/GTK3MappingHelper.cs | 32 | ||||
-rw-r--r-- | Ryujinx/Ui/Applet/GtkDynamicTextInputHandler.cs | 108 | ||||
-rw-r--r-- | Ryujinx/Ui/Applet/GtkHostUiHandler.cs | 25 | ||||
-rw-r--r-- | Ryujinx/Ui/Applet/GtkHostUiTheme.cs | 90 | ||||
-rw-r--r-- | Ryujinx/Ui/Widgets/RawInputToTextEntry.cs | 27 |
5 files changed, 280 insertions, 2 deletions
diff --git a/Ryujinx/Input/GTK3/GTK3MappingHelper.cs b/Ryujinx/Input/GTK3/GTK3MappingHelper.cs index 8bab0dc0..49ea0d15 100644 --- a/Ryujinx/Input/GTK3/GTK3MappingHelper.cs +++ b/Ryujinx/Input/GTK3/GTK3MappingHelper.cs @@ -1,4 +1,6 @@ -using System.Runtime.CompilerServices; +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; using GtkKey = Gdk.Key; namespace Ryujinx.Input.GTK3 @@ -144,11 +146,39 @@ namespace Ryujinx.Input.GTK3 GtkKey.blank, }; + private static readonly Dictionary<GtkKey, Key> _gtkKeyMapping; + + static GTK3MappingHelper() + { + var inputKeys = Enum.GetValues(typeof(Key)); + + // GtkKey is not contiguous and quite large, so use a dictionary instead of an array. + _gtkKeyMapping = new Dictionary<GtkKey, Key>(); + + foreach (var key in inputKeys) + { + try + { + var index = ToGtkKey((Key)key); + _gtkKeyMapping[index] = (Key)key; + } + catch + { + // Skip invalid mappings. + } + } + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static GtkKey ToGtkKey(Key key) { return _keyMapping[(int)key]; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Key ToInputKey(GtkKey key) + { + return _gtkKeyMapping.GetValueOrDefault(key, Key.Unknown); + } } } diff --git a/Ryujinx/Ui/Applet/GtkDynamicTextInputHandler.cs b/Ryujinx/Ui/Applet/GtkDynamicTextInputHandler.cs new file mode 100644 index 00000000..92e99385 --- /dev/null +++ b/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 OffscreenWindow(); + private readonly RawInputToTextEntry _inputToTextEntry = new RawInputToTextEntry(); + + 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 = (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 = (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; + } + } +}
\ No newline at end of file diff --git a/Ryujinx/Ui/Applet/GtkHostUiHandler.cs b/Ryujinx/Ui/Applet/GtkHostUiHandler.cs index c227ebd3..d81cbe3c 100644 --- a/Ryujinx/Ui/Applet/GtkHostUiHandler.cs +++ b/Ryujinx/Ui/Applet/GtkHostUiHandler.cs @@ -1,10 +1,11 @@ using Gtk; -using Ryujinx.HLE; 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; +using Action = System.Action; namespace Ryujinx.Ui.Applet { @@ -12,9 +13,13 @@ namespace Ryujinx.Ui.Applet { private readonly Window _parent; + public IHostUiTheme HostUiTheme { get; } + public GtkHostUiHandler(Window parent) { _parent = parent; + + HostUiTheme = new GtkHostUiTheme(parent); } public bool DisplayMessageDialog(ControllerAppletUiArgs args) @@ -186,5 +191,23 @@ namespace Ryujinx.Ui.Applet return showDetails; } + + private void SynchronousGtkInvoke(Action action) + { + var waitHandle = new ManualResetEventSlim(); + + Application.Invoke(delegate + { + action(); + waitHandle.Set(); + }); + + waitHandle.Wait(); + } + + public IDynamicTextInputHandler CreateDynamicTextInputHandler() + { + return new GtkDynamicTextInputHandler(_parent); + } } }
\ No newline at end of file diff --git a/Ryujinx/Ui/Applet/GtkHostUiTheme.cs b/Ryujinx/Ui/Applet/GtkHostUiTheme.cs new file mode 100644 index 00000000..f25da47c --- /dev/null +++ b/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(); + 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 ListBoxRow(); + 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 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); + } + } +}
\ No newline at end of file diff --git a/Ryujinx/Ui/Widgets/RawInputToTextEntry.cs b/Ryujinx/Ui/Widgets/RawInputToTextEntry.cs new file mode 100644 index 00000000..a0092f29 --- /dev/null +++ b/Ryujinx/Ui/Widgets/RawInputToTextEntry.cs @@ -0,0 +1,27 @@ +using Gtk; + +namespace Ryujinx.Ui.Widgets +{ + public class RawInputToTextEntry : Entry + { + public void SendKeyPressEvent(object o, KeyPressEventArgs args) + { + base.OnKeyPressEvent(args.Event); + } + + public void SendKeyReleaseEvent(object o, KeyReleaseEventArgs args) + { + base.OnKeyReleaseEvent(args.Event); + } + + public void SendButtonPressEvent(object o, ButtonPressEventArgs args) + { + base.OnButtonPressEvent(args.Event); + } + + public void SendButtonReleaseEvent(object o, ButtonReleaseEventArgs args) + { + base.OnButtonReleaseEvent(args.Event); + } + } +} |