From c11855565e0ce2bac228610cbaa92c8c7f082c70 Mon Sep 17 00:00:00 2001 From: mageven <62494521+mageven@users.noreply.github.com> Date: Mon, 3 Aug 2020 07:00:58 +0530 Subject: [PATCH] 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 --- .../SoftwareKeyboardApplet.cs | 66 +++++++++++++++--- .../SoftwareKeyboardUiArgs.cs | 13 ++++ .../ILibraryAppletCreator.cs | 18 ++++- Ryujinx.HLE/IHostUiHandler.cs | 14 ++++ Ryujinx.HLE/Switch.cs | 2 + Ryujinx/Ui/GtkHostUiHandler.cs | 69 +++++++++++++++++++ Ryujinx/Ui/InputDialog.cs | 69 +++++++++++++++++++ Ryujinx/Ui/MainWindow.cs | 9 ++- 8 files changed, 245 insertions(+), 15 deletions(-) create mode 100644 Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiArgs.cs create mode 100644 Ryujinx.HLE/IHostUiHandler.cs create mode 100644 Ryujinx/Ui/GtkHostUiHandler.cs create mode 100644 Ryujinx/Ui/InputDialog.cs diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs index e142838cab..000d11930a 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs @@ -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 System; using System.IO; @@ -9,9 +10,10 @@ namespace Ryujinx.HLE.HOS.Applets { internal class SoftwareKeyboardApplet : IApplet { - private const string DefaultNumb = "1"; private const string DefaultText = "Ryujinx"; + private readonly Switch _device; + private const int StandardBufferSize = 0x7D8; private const int InteractiveBufferSize = 0x7D4; @@ -21,13 +23,18 @@ namespace Ryujinx.HLE.HOS.Applets private AppletSession _interactiveSession; private SoftwareKeyboardConfig _keyboardConfig; + private byte[] _transferMemory; - private string _textValue = DefaultText; + private string _textValue = null; + private bool _okPressed = false; private Encoding _encoding = Encoding.Unicode; public event EventHandler AppletStateChanged; - public SoftwareKeyboardApplet(Horizon system) { } + public SoftwareKeyboardApplet(Horizon system) + { + _device = system.Device; + } public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession) @@ -39,9 +46,20 @@ namespace Ryujinx.HLE.HOS.Applets var launchParams = _normalSession.Pop(); var keyboardConfig = _normalSession.Pop(); - var transferMemory = _normalSession.Pop(); - _keyboardConfig = ReadStruct(keyboardConfig); + if (keyboardConfig.Length < Marshal.SizeOf()) + { + Logger.PrintError(LogClass.ServiceAm, $"SoftwareKeyboardConfig size mismatch. Expected {Marshal.SizeOf():x}. Got {keyboardConfig.Length:x}"); + } + else + { + _keyboardConfig = ReadStruct(keyboardConfig); + } + + if (!_normalSession.TryPop(out _transferMemory)) + { + Logger.PrintError(LogClass.ServiceAm, "SwKbd Transfer Memory is null"); + } if (_keyboardConfig.UseUtf8) { @@ -62,11 +80,13 @@ namespace Ryujinx.HLE.HOS.Applets private void Execute() { - // If the keyboard type is numbers only, we swap to a default - // text that only contains numbers. - if (_keyboardConfig.Mode == KeyboardMode.NumbersOnly) + string initialText = null; + + // 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 @@ -76,6 +96,30 @@ namespace Ryujinx.HLE.HOS.Applets _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 // than our default text, repeat our default text until we meet // the minimum length requirement. @@ -162,7 +206,7 @@ namespace Ryujinx.HLE.HOS.Applets if (!interactive) { // Result Code - writer.Write((uint)0); + writer.Write(_okPressed ? 0U : 1U); } else { diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiArgs.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiArgs.cs new file mode 100644 index 0000000000..d24adec333 --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiArgs.cs @@ -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; + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/ILibraryAppletCreator.cs b/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/ILibraryAppletCreator.cs index 564bde0971..bc0e4d8a4a 100644 --- a/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/ILibraryAppletCreator.cs +++ b/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/ILibraryAppletCreator.cs @@ -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; 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(); 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(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; } diff --git a/Ryujinx.HLE/IHostUiHandler.cs b/Ryujinx.HLE/IHostUiHandler.cs new file mode 100644 index 0000000000..13b4b4c1c5 --- /dev/null +++ b/Ryujinx.HLE/IHostUiHandler.cs @@ -0,0 +1,14 @@ +using Ryujinx.HLE.HOS.Applets; + +namespace Ryujinx.HLE +{ + public interface IHostUiHandler + { + /// + /// Displays an Input Dialog box to the user and blocks until text is entered. + /// + /// Text that the user entered. Set to `null` on internal errors + /// True when OK is pressed, False otherwise. Also returns True on internal errors + bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText); + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/Switch.cs b/Ryujinx.HLE/Switch.cs index 2e1a4b66a9..0bdcdabd2c 100644 --- a/Ryujinx.HLE/Switch.cs +++ b/Ryujinx.HLE/Switch.cs @@ -37,6 +37,8 @@ namespace Ryujinx.HLE public Hid Hid { get; private set; } + public IHostUiHandler UiHandler { get; set; } + public bool EnableDeviceVsync { get; set; } = true; public Switch(VirtualFileSystem fileSystem, ContentManager contentManager, IRenderer renderer, IAalOutput audioOut) diff --git a/Ryujinx/Ui/GtkHostUiHandler.cs b/Ryujinx/Ui/GtkHostUiHandler.cs new file mode 100644 index 0000000000..7b7b364701 --- /dev/null +++ b/Ryujinx/Ui/GtkHostUiHandler.cs @@ -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; + } + } +} diff --git a/Ryujinx/Ui/InputDialog.cs b/Ryujinx/Ui/InputDialog.cs new file mode 100644 index 0000000000..a8dc80bf81 --- /dev/null +++ b/Ryujinx/Ui/InputDialog.cs @@ -0,0 +1,69 @@ +using Gtk; +using System; + +namespace Ryujinx.Ui +{ + public class InputDialog : MessageDialog + { + private int _inputMin, _inputMax; + private Predicate _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 = $"Must be at least {_inputMin} characters long"; + _checkLength = (length) => _inputMin <= length; + } + else + { + _validationInfo.Visible = true; + _validationInfo.Markup = $"Must be {_inputMin}-{_inputMax} characters long"; + _checkLength = (length) => _inputMin <= length && length <= _inputMax; + } + + OnInputChanged(this, EventArgs.Empty); + } + + private void OnInputChanged(object sender, EventArgs e) + { + OkButton.Sensitive = _checkLength(InputEntry.Text.Length); + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index 4287010784..2e288ac98b 100644 --- a/Ryujinx/Ui/MainWindow.cs +++ b/Ryujinx/Ui/MainWindow.cs @@ -5,6 +5,7 @@ using LibHac.Ns; using Ryujinx.Audio; using Ryujinx.Common.Logging; using Ryujinx.Configuration; +using Ryujinx.Configuration.System; using Ryujinx.Debugger.Profiler; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.OpenGL; @@ -31,6 +32,7 @@ namespace Ryujinx.Ui private static HLE.Switch _emulationContext; private static GlRenderer _glWidget; + private static GtkHostUiHandler _uiHandler; private static AutoResetEvent _deviceExitStatus = new AutoResetEvent(false); @@ -191,6 +193,8 @@ namespace Ryujinx.Ui Task.Run(RefreshFirmwareLabel); _statusBar.Hide(); + + _uiHandler = new GtkHostUiHandler(this); } private void MainWindow_WindowStateEvent(object o, WindowStateEventArgs args) @@ -318,7 +322,10 @@ namespace Ryujinx.Ui { _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();