SDL: Refactor iOS text input activation to better work with hardware keyboards

From 5d09656afa3b6d10de522835887b51f2b09485ed Mon Sep 17 00:00:00 2001
From: Salman Alshamrani <[EMAIL REDACTED]>
Date: Sun, 3 Nov 2024 23:12:27 -0500
Subject: [PATCH] Refactor iOS text input activation to better work with
 hardware keyboards

---
 src/video/uikit/SDL_uikitvideo.m          |   4 +-
 src/video/uikit/SDL_uikitviewcontroller.h |  10 +-
 src/video/uikit/SDL_uikitviewcontroller.m | 108 ++++++++++------------
 3 files changed, 54 insertions(+), 68 deletions(-)

diff --git a/src/video/uikit/SDL_uikitvideo.m b/src/video/uikit/SDL_uikitvideo.m
index 4866de9785637..d492178b8151f 100644
--- a/src/video/uikit/SDL_uikitvideo.m
+++ b/src/video/uikit/SDL_uikitvideo.m
@@ -97,8 +97,8 @@ static void UIKit_DeleteDevice(SDL_VideoDevice *device)
 
 #ifdef SDL_IPHONE_KEYBOARD
         device->HasScreenKeyboardSupport = UIKit_HasScreenKeyboardSupport;
-        device->ShowScreenKeyboard = UIKit_ShowScreenKeyboard;
-        device->HideScreenKeyboard = UIKit_HideScreenKeyboard;
+        device->StartTextInput = UIKit_StartTextInput;
+        device->StopTextInput = UIKit_StopTextInput;
         device->IsScreenKeyboardShown = UIKit_IsScreenKeyboardShown;
         device->UpdateTextInputArea = UIKit_UpdateTextInputArea;
 #endif
diff --git a/src/video/uikit/SDL_uikitviewcontroller.h b/src/video/uikit/SDL_uikitviewcontroller.h
index b66258c1f0fe1..dd22e780c7a54 100644
--- a/src/video/uikit/SDL_uikitviewcontroller.h
+++ b/src/video/uikit/SDL_uikitviewcontroller.h
@@ -69,8 +69,8 @@
 #endif
 
 #ifdef SDL_IPHONE_KEYBOARD
-- (void)showKeyboard;
-- (void)hideKeyboard;
+- (bool)startTextInput;
+- (bool)stopTextInput;
 - (void)initKeyboard;
 - (void)deinitKeyboard;
 
@@ -79,7 +79,7 @@
 
 - (void)updateKeyboard;
 
-@property(nonatomic, assign, getter=isKeyboardVisible) BOOL keyboardVisible;
+@property(nonatomic, assign, getter=isTextFieldFocused) BOOL textFieldFocused;
 @property(nonatomic, assign) SDL_Rect textInputRect;
 @property(nonatomic, assign) int keyboardHeight;
 #endif
@@ -88,8 +88,8 @@
 
 #ifdef SDL_IPHONE_KEYBOARD
 bool UIKit_HasScreenKeyboardSupport(SDL_VideoDevice *_this);
-void UIKit_ShowScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props);
-void UIKit_HideScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window);
+bool UIKit_StartTextInput(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props);
+bool UIKit_StopTextInput(SDL_VideoDevice *_this, SDL_Window *window);
 bool UIKit_IsScreenKeyboardShown(SDL_VideoDevice *_this, SDL_Window *window);
 bool UIKit_UpdateTextInputArea(SDL_VideoDevice *_this, SDL_Window *window);
 #endif
diff --git a/src/video/uikit/SDL_uikitviewcontroller.m b/src/video/uikit/SDL_uikitviewcontroller.m
index 3c3cae8555463..832ee33ecbdcb 100644
--- a/src/video/uikit/SDL_uikitviewcontroller.m
+++ b/src/video/uikit/SDL_uikitviewcontroller.m
@@ -80,7 +80,6 @@ @implementation SDL_uikitviewcontroller
 
 #ifdef SDL_IPHONE_KEYBOARD
     SDLUITextField *textField;
-    BOOL showingKeyboard;
     BOOL hidingKeyboard;
     BOOL rotatingOrientation;
     NSString *committedText;
@@ -97,7 +96,6 @@ - (instancetype)initWithSDLWindow:(SDL_Window *)_window
 
 #ifdef SDL_IPHONE_KEYBOARD
         [self initKeyboard];
-        showingKeyboard = NO;
         hidingKeyboard = NO;
         rotatingOrientation = NO;
 #endif
@@ -264,7 +262,7 @@ - (BOOL)prefersPointerLocked
 
 @synthesize textInputRect;
 @synthesize keyboardHeight;
-@synthesize keyboardVisible;
+@synthesize textFieldFocused;
 
 // Set ourselves up as a UITextFieldDelegate
 - (void)initKeyboard
@@ -277,7 +275,7 @@ - (void)initKeyboard
     committedText = textField.text;
 
     textField.hidden = YES;
-    keyboardVisible = NO;
+    textFieldFocused = NO;
 
     NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
 #ifndef SDL_PLATFORM_TVOS
@@ -285,10 +283,6 @@ - (void)initKeyboard
                selector:@selector(keyboardWillShow:)
                    name:UIKeyboardWillShowNotification
                  object:nil];
-    [center addObserver:self
-               selector:@selector(keyboardDidShow:)
-                   name:UIKeyboardDidShowNotification
-                 object:nil];
     [center addObserver:self
                selector:@selector(keyboardWillHide:)
                    name:UIKeyboardWillHideNotification
@@ -343,8 +337,10 @@ - (void)setView:(UIView *)view
 
     [view addSubview:textField];
 
-    if (keyboardVisible) {
-        [self showKeyboard];
+    if (textFieldFocused) {
+        /* startTextInput has been called before the text field was added to the view,
+         * call it again for the text field to actually become first responder. */
+        [self startTextInput];
     }
 }
 
@@ -367,9 +363,6 @@ - (void)deinitKeyboard
     [center removeObserver:self
                       name:UIKeyboardWillShowNotification
                     object:nil];
-    [center removeObserver:self
-                      name:UIKeyboardDidShowNotification
-                    object:nil];
     [center removeObserver:self
                       name:UIKeyboardWillHideNotification
                     object:nil];
@@ -382,7 +375,7 @@ - (void)deinitKeyboard
                     object:nil];
 }
 
-- (void)setKeyboardProperties:(SDL_PropertiesID) props
+- (void)setTextFieldProperties:(SDL_PropertiesID) props
 {
     textField.secureTextEntry = NO;
 
@@ -479,43 +472,36 @@ - (void)setKeyboardProperties:(SDL_PropertiesID) props
     }
 }
 
-// reveal onscreen virtual keyboard
-- (void)showKeyboard
+/* requests the SDL text field to become focused and accept text input.
+ * also shows the onscreen virtual keyboard if no hardware keyboard is attached. */
+- (bool)startTextInput
 {
-    if (keyboardVisible) {
-        return;
+    textFieldFocused = YES;
+    if (!textField.window) {
+        /* textField has not been added to the view yet,
+         * we will try again when that happens. */
+        return true;
     }
 
-    keyboardVisible = YES;
-    if (textField.window) {
-        showingKeyboard = YES;
-        [textField becomeFirstResponder];
-    }
+    return [textField becomeFirstResponder];
 }
 
-// hide onscreen virtual keyboard
-- (void)hideKeyboard
+/* requests the SDL text field to lose focus and stop accepting text input.
+ * also hides the onscreen virtual keyboard if no hardware keyboard is attached. */
+- (bool)stopTextInput
 {
-    if (!keyboardVisible) {
-        return;
+    textFieldFocused = NO;
+    if (!textField.window) {
+        /* textField has not been added to the view yet,
+         * we will try again when that happens. */
+        return true;
     }
 
-    keyboardVisible = NO;
-    if (textField.window) {
-        hidingKeyboard = YES;
-        [textField resignFirstResponder];
-    }
+    return [textField resignFirstResponder];
 }
 
 - (void)keyboardWillShow:(NSNotification *)notification
 {
-    BOOL shouldStartTextInput = NO;
-
-    if (!SDL_TextInputActive(window) && !hidingKeyboard && !rotatingOrientation) {
-        shouldStartTextInput = YES;
-    }
-
-    showingKeyboard = YES;
 #ifndef SDL_PLATFORM_TVOS
     CGRect kbrect = [[notification userInfo][UIKeyboardFrameEndUserInfoKey] CGRectValue];
 
@@ -526,28 +512,29 @@ - (void)keyboardWillShow:(NSNotification *)notification
     [self setKeyboardHeight:(int)kbrect.size.height];
 #endif
 
-    if (shouldStartTextInput) {
+    /* A keyboard hide transition has been interrupted with a show (keyboardWillHide has been called but keyboardDidHide didn't).
+     * since text input was stopped by the hide, we have to start it again. */
+    if (hidingKeyboard) {
         SDL_StartTextInput(window);
+        hidingKeyboard = NO;
     }
 }
 
-- (void)keyboardDidShow:(NSNotification *)notification
-{
-    showingKeyboard = NO;
-}
-
 - (void)keyboardWillHide:(NSNotification *)notification
 {
-    BOOL shouldStopTextInput = NO;
-
-    if (SDL_TextInputActive(window) && !showingKeyboard && !rotatingOrientation) {
-        shouldStopTextInput = YES;
-    }
-
     hidingKeyboard = YES;
     [self setKeyboardHeight:0];
 
-    if (shouldStopTextInput) {
+    /* When the user dismisses the software keyboard by the "hide" button in the bottom right corner,
+     * we want to reflect that on SDL_TextInputActive by calling SDL_StopTextInput...on certain conditions */
+    if (SDL_TextInputActive(window)
+        /* keyboardWillHide gets called when a hardware keyboard is attached,
+         * keep text input state active if hiding while there is a hardware keyboard.
+         * if the hardware keyboard gets detached, the software keyboard will appear anyway. */
+        && !SDL_HasKeyboard()
+        /* When the device changes orientation, a sequence of hide and show transitions are triggered.
+         * keep text input state active in this case. */
+        && !rotatingOrientation) {
         SDL_StopTextInput(window);
     }
 }
@@ -630,7 +617,6 @@ - (void)updateKeyboard
 
 - (void)setKeyboardHeight:(int)height
 {
-    keyboardVisible = height > 0;
     keyboardHeight = height;
     [self updateKeyboard];
 }
@@ -651,7 +637,7 @@ - (BOOL)textField:(UITextField *)_textField shouldChangeCharactersInRange:(NSRan
 - (BOOL)textFieldShouldReturn:(UITextField *)_textField
 {
     SDL_SendKeyboardKeyAutoRelease(0, SDL_SCANCODE_RETURN);
-    if (keyboardVisible &&
+    if (textFieldFocused &&
         SDL_GetHintBoolean(SDL_HINT_RETURN_KEY_HIDES_IME, false)) {
         SDL_StopTextInput(window);
     }
@@ -682,20 +668,20 @@ bool UIKit_HasScreenKeyboardSupport(SDL_VideoDevice *_this)
     return true;
 }
 
-void UIKit_ShowScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props)
+bool UIKit_StartTextInput(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props)
 {
     @autoreleasepool {
         SDL_uikitviewcontroller *vc = GetWindowViewController(window);
-        [vc setKeyboardProperties:props];
-        [vc showKeyboard];
+        [vc setTextFieldProperties:props];
+        return [vc startTextInput];
     }
 }
 
-void UIKit_HideScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window)
+bool UIKit_StopTextInput(SDL_VideoDevice *_this, SDL_Window *window)
 {
     @autoreleasepool {
         SDL_uikitviewcontroller *vc = GetWindowViewController(window);
-        [vc hideKeyboard];
+        return [vc stopTextInput];
     }
 }
 
@@ -704,7 +690,7 @@ bool UIKit_IsScreenKeyboardShown(SDL_VideoDevice *_this, SDL_Window *window)
     @autoreleasepool {
         SDL_uikitviewcontroller *vc = GetWindowViewController(window);
         if (vc != nil) {
-            return vc.keyboardVisible;
+            return vc.textFieldFocused;
         }
         return false;
     }
@@ -717,7 +703,7 @@ bool UIKit_UpdateTextInputArea(SDL_VideoDevice *_this, SDL_Window *window)
         if (vc != nil) {
             vc.textInputRect = window->text_input_rect;
 
-            if (vc.keyboardVisible) {
+            if (vc.textFieldFocused) {
                 [vc updateKeyboard];
             }
         }