Implement Software Keyboard GTK frontend (#1434)
* Implement SwKbd GUI * Relocate UI handler to Emu Context from Config Also create a common interface for UI handlers in the context and specialize for Gtk Add basic input length validation in InputDialog * Add Transfer Memory support to AppletCreator Read Initial Text for SwKbd using Transfer Memory * Improve InputDialog widget Improve length validation Has extra label to show validition info Handle potential errors and log them * Misc improvements * Improve string validation * Improve error handling * Remove tuple in struct * Address formatting nits * Add proper Cancel functionality Also handle GUI errors in UI handler * Address jD's comments * Fix _uiHandler init * Address AcK's comments
This commit is contained in:
parent
f0c91d9efb
commit
c11855565e
8 changed files with 245 additions and 15 deletions
|
@ -1,4 +1,5 @@
|
||||||
using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
|
||||||
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
|
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -9,9 +10,10 @@ namespace Ryujinx.HLE.HOS.Applets
|
||||||
{
|
{
|
||||||
internal class SoftwareKeyboardApplet : IApplet
|
internal class SoftwareKeyboardApplet : IApplet
|
||||||
{
|
{
|
||||||
private const string DefaultNumb = "1";
|
|
||||||
private const string DefaultText = "Ryujinx";
|
private const string DefaultText = "Ryujinx";
|
||||||
|
|
||||||
|
private readonly Switch _device;
|
||||||
|
|
||||||
private const int StandardBufferSize = 0x7D8;
|
private const int StandardBufferSize = 0x7D8;
|
||||||
private const int InteractiveBufferSize = 0x7D4;
|
private const int InteractiveBufferSize = 0x7D4;
|
||||||
|
|
||||||
|
@ -21,13 +23,18 @@ namespace Ryujinx.HLE.HOS.Applets
|
||||||
private AppletSession _interactiveSession;
|
private AppletSession _interactiveSession;
|
||||||
|
|
||||||
private SoftwareKeyboardConfig _keyboardConfig;
|
private SoftwareKeyboardConfig _keyboardConfig;
|
||||||
|
private byte[] _transferMemory;
|
||||||
|
|
||||||
private string _textValue = DefaultText;
|
private string _textValue = null;
|
||||||
|
private bool _okPressed = false;
|
||||||
private Encoding _encoding = Encoding.Unicode;
|
private Encoding _encoding = Encoding.Unicode;
|
||||||
|
|
||||||
public event EventHandler AppletStateChanged;
|
public event EventHandler AppletStateChanged;
|
||||||
|
|
||||||
public SoftwareKeyboardApplet(Horizon system) { }
|
public SoftwareKeyboardApplet(Horizon system)
|
||||||
|
{
|
||||||
|
_device = system.Device;
|
||||||
|
}
|
||||||
|
|
||||||
public ResultCode Start(AppletSession normalSession,
|
public ResultCode Start(AppletSession normalSession,
|
||||||
AppletSession interactiveSession)
|
AppletSession interactiveSession)
|
||||||
|
@ -39,9 +46,20 @@ namespace Ryujinx.HLE.HOS.Applets
|
||||||
|
|
||||||
var launchParams = _normalSession.Pop();
|
var launchParams = _normalSession.Pop();
|
||||||
var keyboardConfig = _normalSession.Pop();
|
var keyboardConfig = _normalSession.Pop();
|
||||||
var transferMemory = _normalSession.Pop();
|
|
||||||
|
|
||||||
|
if (keyboardConfig.Length < Marshal.SizeOf<SoftwareKeyboardConfig>())
|
||||||
|
{
|
||||||
|
Logger.PrintError(LogClass.ServiceAm, $"SoftwareKeyboardConfig size mismatch. Expected {Marshal.SizeOf<SoftwareKeyboardConfig>():x}. Got {keyboardConfig.Length:x}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
_keyboardConfig = ReadStruct<SoftwareKeyboardConfig>(keyboardConfig);
|
_keyboardConfig = ReadStruct<SoftwareKeyboardConfig>(keyboardConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_normalSession.TryPop(out _transferMemory))
|
||||||
|
{
|
||||||
|
Logger.PrintError(LogClass.ServiceAm, "SwKbd Transfer Memory is null");
|
||||||
|
}
|
||||||
|
|
||||||
if (_keyboardConfig.UseUtf8)
|
if (_keyboardConfig.UseUtf8)
|
||||||
{
|
{
|
||||||
|
@ -62,11 +80,13 @@ namespace Ryujinx.HLE.HOS.Applets
|
||||||
|
|
||||||
private void Execute()
|
private void Execute()
|
||||||
{
|
{
|
||||||
// If the keyboard type is numbers only, we swap to a default
|
string initialText = null;
|
||||||
// text that only contains numbers.
|
|
||||||
if (_keyboardConfig.Mode == KeyboardMode.NumbersOnly)
|
// Initial Text is always encoded as a UTF-16 string in the work buffer (passed as transfer memory)
|
||||||
|
// InitialStringOffset points to the memory offset and InitialStringLength is the number of UTF-16 characters
|
||||||
|
if (_transferMemory != null && _keyboardConfig.InitialStringLength > 0)
|
||||||
{
|
{
|
||||||
_textValue = DefaultNumb;
|
initialText = Encoding.Unicode.GetString(_transferMemory, _keyboardConfig.InitialStringOffset, 2 * _keyboardConfig.InitialStringLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the max string length is 0, we set it to a large default
|
// If the max string length is 0, we set it to a large default
|
||||||
|
@ -76,6 +96,30 @@ namespace Ryujinx.HLE.HOS.Applets
|
||||||
_keyboardConfig.StringLengthMax = 100;
|
_keyboardConfig.StringLengthMax = 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var args = new SoftwareKeyboardUiArgs
|
||||||
|
{
|
||||||
|
HeaderText = _keyboardConfig.HeaderText,
|
||||||
|
SubtitleText = _keyboardConfig.SubtitleText,
|
||||||
|
GuideText = _keyboardConfig.GuideText,
|
||||||
|
SubmitText = (!string.IsNullOrWhiteSpace(_keyboardConfig.SubmitText) ? _keyboardConfig.SubmitText : "OK"),
|
||||||
|
StringLengthMin = _keyboardConfig.StringLengthMin,
|
||||||
|
StringLengthMax = _keyboardConfig.StringLengthMax,
|
||||||
|
InitialText = initialText
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call the configured GUI handler to get user's input
|
||||||
|
if (_device.UiHandler == null)
|
||||||
|
{
|
||||||
|
Logger.PrintWarning(LogClass.Application, $"GUI Handler is not set. Falling back to default");
|
||||||
|
_okPressed = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_okPressed = _device.UiHandler.DisplayInputDialog(args, out _textValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
_textValue ??= initialText ?? DefaultText;
|
||||||
|
|
||||||
// If the game requests a string with a minimum length less
|
// If the game requests a string with a minimum length less
|
||||||
// than our default text, repeat our default text until we meet
|
// than our default text, repeat our default text until we meet
|
||||||
// the minimum length requirement.
|
// the minimum length requirement.
|
||||||
|
@ -162,7 +206,7 @@ namespace Ryujinx.HLE.HOS.Applets
|
||||||
if (!interactive)
|
if (!interactive)
|
||||||
{
|
{
|
||||||
// Result Code
|
// Result Code
|
||||||
writer.Write((uint)0);
|
writer.Write(_okPressed ? 0U : 1U);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
namespace Ryujinx.HLE.HOS.Applets
|
||||||
|
{
|
||||||
|
public struct SoftwareKeyboardUiArgs
|
||||||
|
{
|
||||||
|
public string HeaderText;
|
||||||
|
public string SubtitleText;
|
||||||
|
public string InitialText;
|
||||||
|
public string GuideText;
|
||||||
|
public string SubmitText;
|
||||||
|
public int StringLengthMin;
|
||||||
|
public int StringLengthMax;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
using Ryujinx.HLE.HOS.Applets;
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.HLE.HOS.Kernel.Memory;
|
||||||
using Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.LibraryAppletCreator;
|
using Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.LibraryAppletCreator;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.SystemAppletProxy
|
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.SystemAppletProxy
|
||||||
|
@ -36,10 +37,21 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
|
||||||
{
|
{
|
||||||
bool unknown = context.RequestData.ReadBoolean();
|
bool unknown = context.RequestData.ReadBoolean();
|
||||||
long size = context.RequestData.ReadInt64();
|
long size = context.RequestData.ReadInt64();
|
||||||
|
int handle = context.Request.HandleDesc.ToCopy[0];
|
||||||
|
|
||||||
// NOTE: We don't support TransferMemory for now.
|
KTransferMemory transferMem = context.Process.HandleTable.GetObject<KTransferMemory>(handle);
|
||||||
|
|
||||||
MakeObject(context, new IStorage(new byte[size]));
|
if (transferMem == null)
|
||||||
|
{
|
||||||
|
Logger.PrintWarning(LogClass.ServiceAm, $"Invalid TransferMemory Handle: {handle:X}");
|
||||||
|
|
||||||
|
return ResultCode.Success; // TODO: Find correct error code
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = new byte[transferMem.Size];
|
||||||
|
context.Memory.Read(transferMem.Address, data);
|
||||||
|
|
||||||
|
MakeObject(context, new IStorage(data));
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
|
14
Ryujinx.HLE/IHostUiHandler.cs
Normal file
14
Ryujinx.HLE/IHostUiHandler.cs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
using Ryujinx.HLE.HOS.Applets;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE
|
||||||
|
{
|
||||||
|
public interface IHostUiHandler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Displays an Input Dialog box to the user and blocks until text is entered.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userText">Text that the user entered. Set to `null` on internal errors</param>
|
||||||
|
/// <returns>True when OK is pressed, False otherwise. Also returns True on internal errors</returns>
|
||||||
|
bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText);
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,8 @@ namespace Ryujinx.HLE
|
||||||
|
|
||||||
public Hid Hid { get; private set; }
|
public Hid Hid { get; private set; }
|
||||||
|
|
||||||
|
public IHostUiHandler UiHandler { get; set; }
|
||||||
|
|
||||||
public bool EnableDeviceVsync { get; set; } = true;
|
public bool EnableDeviceVsync { get; set; } = true;
|
||||||
|
|
||||||
public Switch(VirtualFileSystem fileSystem, ContentManager contentManager, IRenderer renderer, IAalOutput audioOut)
|
public Switch(VirtualFileSystem fileSystem, ContentManager contentManager, IRenderer renderer, IAalOutput audioOut)
|
||||||
|
|
69
Ryujinx/Ui/GtkHostUiHandler.cs
Normal file
69
Ryujinx/Ui/GtkHostUiHandler.cs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
using Gtk;
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.HLE;
|
||||||
|
using Ryujinx.HLE.HOS.Applets;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ui
|
||||||
|
{
|
||||||
|
internal class GtkHostUiHandler : IHostUiHandler
|
||||||
|
{
|
||||||
|
private readonly Window _parent;
|
||||||
|
|
||||||
|
public GtkHostUiHandler(Window parent)
|
||||||
|
{
|
||||||
|
_parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText)
|
||||||
|
{
|
||||||
|
ManualResetEvent dialogCloseEvent = new ManualResetEvent(false);
|
||||||
|
bool okPressed = false;
|
||||||
|
bool error = false;
|
||||||
|
string inputText = args.InitialText ?? "";
|
||||||
|
|
||||||
|
Application.Invoke(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var swkbdDialog = new InputDialog(_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);
|
||||||
|
|
||||||
|
if (swkbdDialog.Run() == (int)ResponseType.Ok)
|
||||||
|
{
|
||||||
|
inputText = swkbdDialog.InputEntry.Text;
|
||||||
|
okPressed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
swkbdDialog.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
error = true;
|
||||||
|
Logger.PrintError(LogClass.Application, $"Error displaying Software Keyboard: {e}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
dialogCloseEvent.Set();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogCloseEvent.WaitOne();
|
||||||
|
|
||||||
|
userText = error ? null : inputText;
|
||||||
|
|
||||||
|
return error || okPressed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
69
Ryujinx/Ui/InputDialog.cs
Normal file
69
Ryujinx/Ui/InputDialog.cs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
using Gtk;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ui
|
||||||
|
{
|
||||||
|
public class InputDialog : MessageDialog
|
||||||
|
{
|
||||||
|
private int _inputMin, _inputMax;
|
||||||
|
private Predicate<int> _checkLength;
|
||||||
|
private Label _validationInfo;
|
||||||
|
|
||||||
|
public Entry InputEntry { get; }
|
||||||
|
public Button OkButton { get; }
|
||||||
|
public Button CancelButton { get; }
|
||||||
|
|
||||||
|
public InputDialog(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 += (object sender, EventArgs e) => { if (OkButton.IsSensitive) Respond(ResponseType.Ok); };
|
||||||
|
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);
|
||||||
|
|
||||||
|
SetInputLengthValidation(0, int.MaxValue); // disable by default
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = (length) => true;
|
||||||
|
}
|
||||||
|
else if (_inputMin > 0 && _inputMax == int.MaxValue)
|
||||||
|
{
|
||||||
|
_validationInfo.Visible = true;
|
||||||
|
_validationInfo.Markup = $"<i>Must be at least {_inputMin} characters long</i>";
|
||||||
|
_checkLength = (length) => _inputMin <= length;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_validationInfo.Visible = true;
|
||||||
|
_validationInfo.Markup = $"<i>Must be {_inputMin}-{_inputMax} characters long</i>";
|
||||||
|
_checkLength = (length) => _inputMin <= length && length <= _inputMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnInputChanged(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnInputChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
OkButton.Sensitive = _checkLength(InputEntry.Text.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ using LibHac.Ns;
|
||||||
using Ryujinx.Audio;
|
using Ryujinx.Audio;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Configuration;
|
using Ryujinx.Configuration;
|
||||||
|
using Ryujinx.Configuration.System;
|
||||||
using Ryujinx.Debugger.Profiler;
|
using Ryujinx.Debugger.Profiler;
|
||||||
using Ryujinx.Graphics.GAL;
|
using Ryujinx.Graphics.GAL;
|
||||||
using Ryujinx.Graphics.OpenGL;
|
using Ryujinx.Graphics.OpenGL;
|
||||||
|
@ -31,6 +32,7 @@ namespace Ryujinx.Ui
|
||||||
private static HLE.Switch _emulationContext;
|
private static HLE.Switch _emulationContext;
|
||||||
|
|
||||||
private static GlRenderer _glWidget;
|
private static GlRenderer _glWidget;
|
||||||
|
private static GtkHostUiHandler _uiHandler;
|
||||||
|
|
||||||
private static AutoResetEvent _deviceExitStatus = new AutoResetEvent(false);
|
private static AutoResetEvent _deviceExitStatus = new AutoResetEvent(false);
|
||||||
|
|
||||||
|
@ -191,6 +193,8 @@ namespace Ryujinx.Ui
|
||||||
Task.Run(RefreshFirmwareLabel);
|
Task.Run(RefreshFirmwareLabel);
|
||||||
|
|
||||||
_statusBar.Hide();
|
_statusBar.Hide();
|
||||||
|
|
||||||
|
_uiHandler = new GtkHostUiHandler(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MainWindow_WindowStateEvent(object o, WindowStateEventArgs args)
|
private void MainWindow_WindowStateEvent(object o, WindowStateEventArgs args)
|
||||||
|
@ -318,7 +322,10 @@ namespace Ryujinx.Ui
|
||||||
{
|
{
|
||||||
_virtualFileSystem.Reload();
|
_virtualFileSystem.Reload();
|
||||||
|
|
||||||
HLE.Switch instance = new HLE.Switch(_virtualFileSystem, _contentManager, InitializeRenderer(), InitializeAudioEngine());
|
HLE.Switch instance = new HLE.Switch(_virtualFileSystem, _contentManager, InitializeRenderer(), InitializeAudioEngine())
|
||||||
|
{
|
||||||
|
UiHandler = _uiHandler
|
||||||
|
};
|
||||||
|
|
||||||
instance.Initialize();
|
instance.Initialize();
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue