Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor iOS text input activation to better work with hardware keyboards #11406

Merged
merged 3 commits into from
Dec 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/video/SDL_sysvideo.h
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ struct SDL_VideoDevice
bool (*HasScreenKeyboardSupport)(SDL_VideoDevice *_this);
void (*ShowScreenKeyboard)(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props);
void (*HideScreenKeyboard)(SDL_VideoDevice *_this, SDL_Window *window);
void (*SetTextInputProperties)(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props);
bool (*IsScreenKeyboardShown)(SDL_VideoDevice *_this, SDL_Window *window);

// Clipboard
Expand Down
4 changes: 4 additions & 0 deletions src/video/SDL_video.c
Original file line number Diff line number Diff line change
Expand Up @@ -5294,6 +5294,10 @@ bool SDL_StartTextInputWithProperties(SDL_Window *window, SDL_PropertiesID props
}
}

if (_this->SetTextInputProperties) {
_this->SetTextInputProperties(_this, window, props);
}

// Show the on-screen keyboard, if desired
if (AutoShowingScreenKeyboard() && !SDL_ScreenKeyboardShown(window)) {
if (_this->ShowScreenKeyboard) {
Expand Down
5 changes: 3 additions & 2 deletions src/video/uikit/SDL_uikitvideo.m
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,9 @@ 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->SetTextInputProperties = UIKit_SetTextInputProperties;
device->IsScreenKeyboardShown = UIKit_IsScreenKeyboardShown;
device->UpdateTextInputArea = UIKit_UpdateTextInputArea;
#endif
Expand Down
11 changes: 6 additions & 5 deletions src/video/uikit/SDL_uikitviewcontroller.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@
#endif

#ifdef SDL_IPHONE_KEYBOARD
- (void)showKeyboard;
- (void)hideKeyboard;
- (bool)startTextInput;
- (bool)stopTextInput;
- (void)initKeyboard;
- (void)deinitKeyboard;

Expand All @@ -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
Expand All @@ -88,8 +88,9 @@

#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);
void UIKit_SetTextInputProperties(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props);
bool UIKit_IsScreenKeyboardShown(SDL_VideoDevice *_this, SDL_Window *window);
bool UIKit_UpdateTextInputArea(SDL_VideoDevice *_this, SDL_Window *window);
#endif
128 changes: 67 additions & 61 deletions src/video/uikit/SDL_uikitviewcontroller.m
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@ @implementation SDL_uikitviewcontroller

#ifdef SDL_IPHONE_KEYBOARD
SDLUITextField *textField;
BOOL hardwareKeyboard;
BOOL showingKeyboard;
BOOL hidingKeyboard;
BOOL rotatingOrientation;
NSString *committedText;
Expand All @@ -98,8 +96,6 @@ - (instancetype)initWithSDLWindow:(SDL_Window *)_window

#ifdef SDL_IPHONE_KEYBOARD
[self initKeyboard];
hardwareKeyboard = NO;
showingKeyboard = NO;
hidingKeyboard = NO;
rotatingOrientation = NO;
#endif
Expand Down Expand Up @@ -266,7 +262,7 @@ - (BOOL)prefersPointerLocked

@synthesize textInputRect;
@synthesize keyboardHeight;
@synthesize keyboardVisible;
@synthesize textFieldFocused;

// Set ourselves up as a UITextFieldDelegate
- (void)initKeyboard
Expand All @@ -279,18 +275,14 @@ - (void)initKeyboard
committedText = textField.text;

textField.hidden = YES;
keyboardVisible = NO;
textFieldFocused = NO;

NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
#ifndef SDL_PLATFORM_TVOS
[center addObserver:self
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
Expand Down Expand Up @@ -345,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];
}
}

Expand All @@ -369,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];
Expand All @@ -384,7 +375,7 @@ - (void)deinitKeyboard
object:nil];
}

- (void)setKeyboardProperties:(SDL_PropertiesID) props
- (void)setTextFieldProperties:(SDL_PropertiesID) props
{
textField.secureTextEntry = NO;

Expand Down Expand Up @@ -479,45 +470,53 @@ - (void)setKeyboardProperties:(SDL_PropertiesID) props
} else {
textField.enablesReturnKeyAutomatically = NO;
}
}

// reveal onscreen virtual keyboard
- (void)showKeyboard
{
if (keyboardVisible) {
if (!textField.window) {
/* textField has not been added to the view yet,
we don't have to do anything. */
return;
}

keyboardVisible = YES;
if (textField.window) {
showingKeyboard = YES;
// the text field needs to be re-added to the view in order to update correctly.
UIView *superview = textField.superview;
[textField removeFromSuperview];
[superview addSubview:textField];
Comment on lines +480 to +483
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can technically be split to a separate PR but I figured I could include it in this PR since I'm touching the entire file anyway.


if (SDL_TextInputActive(window)) {
[textField becomeFirstResponder];
}
}

// hide onscreen virtual keyboard
- (void)hideKeyboard
/* 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 = NO;
if (textField.window) {
hidingKeyboard = YES;
[textField resignFirstResponder];
}
return [textField becomeFirstResponder];
}

- (void)keyboardWillShow:(NSNotification *)notification
/* 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
{
BOOL shouldStartTextInput = NO;

if (!SDL_TextInputActive(window) && !hidingKeyboard && !rotatingOrientation) {
shouldStartTextInput = YES;
textFieldFocused = NO;
if (!textField.window) {
/* textField has not been added to the view yet,
* we will try again when that happens. */
return true;
}

showingKeyboard = YES;
return [textField resignFirstResponder];
}

- (void)keyboardWillShow:(NSNotification *)notification
{
#ifndef SDL_PLATFORM_TVOS
CGRect kbrect = [[notification userInfo][UIKeyboardFrameEndUserInfoKey] CGRectValue];

Expand All @@ -528,28 +527,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);
}
Comment on lines +543 to 554
Copy link
Contributor Author

@frenzibyte frenzibyte Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upon further testing, having this also breaks password manager integrations (when setting SDL_PROP_TEXTINPUT_TYPE_NUMBER to SDL_TEXTINPUT_TYPE_TEXT_USERNAME/SDL_TEXTINPUT_TYPE_TEXT_PASSWORD_HIDDEN)

RPReplay_Final1730711349.MP4

Notice how pressing the "Passwords" button in the keyboard hides the keyboard, which triggers the SDL application to stop text input. This is problematic because when the user selects their password from the keychain, the input is ignored since text input is not active.

I think it would be a better option to keep text input state completely manual rather than tied to the state of the keyboard, and do away with all these conditionals that are set in place here. @slouken thoughts? do other platforms (aka Android) stop text input when the user hides the keyboard?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the expectation is that text input state is tied to the visibility of the on-screen keyboard, if there is no hardware keyboard attached.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is SDL 3.0, we can revisit that assumption, but that's the way it is currently. Bringing up the on-screen keyboard on Android, for example, will enable text input so it can be used to input text into the application.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the situation above, there is no possible way to enter the selected password without breaking that assumption, unless SDL can somehow bypass the "text input active" check when sending text events and do that on textFieldTextDidChange events, but I think that's too much of a hacky solution IMO.

Assuming that each application that starts text input has a path for stopping text input, it would be much easier to keep text input state manual and detached from the visibility of the on-screen keyboard, to allow integration with password managers etc.

}
Expand Down Expand Up @@ -632,7 +632,6 @@ - (void)updateKeyboard

- (void)setKeyboardHeight:(int)height
{
keyboardVisible = height > 0;
keyboardHeight = height;
[self updateKeyboard];
}
Expand All @@ -653,7 +652,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);
}
Expand Down Expand Up @@ -684,20 +683,27 @@ 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);
return [vc startTextInput];
}
}

bool UIKit_StopTextInput(SDL_VideoDevice *_this, SDL_Window *window)
{
@autoreleasepool {
SDL_uikitviewcontroller *vc = GetWindowViewController(window);
[vc setKeyboardProperties:props];
[vc showKeyboard];
return [vc stopTextInput];
}
}

void UIKit_HideScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window)
void UIKit_SetTextInputProperties(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props)
{
@autoreleasepool {
SDL_uikitviewcontroller *vc = GetWindowViewController(window);
[vc hideKeyboard];
[vc setTextFieldProperties:props];
}
}

Expand All @@ -706,7 +712,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;
}
Expand All @@ -719,7 +725,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];
}
}
Expand Down
Loading