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<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)
             {
@@ -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<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;
         }
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
+    {
+        /// <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);
+    }
+}
\ 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<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);
+        }
+    }
+}
\ 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();