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:
mageven 2020-08-03 07:00:58 +05:30 committed by GitHub
parent f0c91d9efb
commit c11855565e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 245 additions and 15 deletions

View file

@ -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();
_keyboardConfig = ReadStruct<SoftwareKeyboardConfig>(keyboardConfig); 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);
}
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
{ {

View file

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

View file

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

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

View file

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

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

View file

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