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
This commit is contained in:
Caian Benedicto 2021-10-12 16:54:21 -03:00 committed by GitHub
parent 69093cf2d6
commit 380b95bc59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 2853 additions and 344 deletions

View file

@ -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);
}
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}