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:
parent
69093cf2d6
commit
380b95bc59
47 changed files with 2853 additions and 344 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
108
Ryujinx/Ui/Applet/GtkDynamicTextInputHandler.cs
Normal file
108
Ryujinx/Ui/Applet/GtkDynamicTextInputHandler.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
90
Ryujinx/Ui/Applet/GtkHostUiTheme.cs
Normal file
90
Ryujinx/Ui/Applet/GtkHostUiTheme.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
27
Ryujinx/Ui/Widgets/RawInputToTextEntry.cs
Normal file
27
Ryujinx/Ui/Widgets/RawInputToTextEntry.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue