Browse Source

Merge pull request #43560 from naithar/fix/ios-keyboard

[3.2] [iOS] Keyboard input changes
Rémi Verschelde 4 years ago
parent
commit
574ca8a8cd

+ 1 - 0
platform/iphone/SCsub

@@ -19,6 +19,7 @@ iphone_lib = [
     "godot_view_renderer.mm",
     "godot_view_gesture_recognizer.mm",
     "device_metrics.m",
+    "keyboard_input_view.mm",
     "native_video_view.m",
 ]
 

+ 1 - 3
platform/iphone/godot_view.h

@@ -40,7 +40,7 @@ class String;
 @protocol DisplayLayer;
 @protocol GodotViewRendererProtocol;
 
-@interface GodotView : UIView <UIKeyInput>
+@interface GodotView : UIView
 
 @property(assign, nonatomic) id<GodotViewRendererProtocol> renderer;
 
@@ -55,8 +55,6 @@ class String;
 - (void)stopRendering;
 - (void)startRendering;
 
-- (BOOL)becomeFirstResponderWithString:(String)p_existing;
-
 @property(nonatomic, assign) BOOL useCADisplayLink;
 
 @end

+ 0 - 36
platform/iphone/godot_view.mm

@@ -30,7 +30,6 @@
 
 #import "godot_view.h"
 
-#include "core/os/keyboard.h"
 #include "core/project_settings.h"
 #include "os_iphone.h"
 #include "servers/audio_server.h"
@@ -48,7 +47,6 @@ static const int max_touches = 8;
 
 @interface GodotView () {
 	UITouch *godot_touches[max_touches];
-	String keyboard_text;
 }
 
 @property(assign, nonatomic) BOOL isActive;
@@ -275,40 +273,6 @@ static const int max_touches = 8;
 
 // MARK: - Input
 
-// MARK: Keyboard
-
-- (BOOL)canBecomeFirstResponder {
-	return YES;
-}
-
-- (BOOL)becomeFirstResponderWithString:(String)p_existing {
-	keyboard_text = p_existing;
-	return [self becomeFirstResponder];
-}
-
-- (BOOL)resignFirstResponder {
-	keyboard_text = String();
-	return [super resignFirstResponder];
-}
-
-- (void)deleteBackward {
-	if (keyboard_text.length()) {
-		keyboard_text.erase(keyboard_text.length() - 1, 1);
-	}
-	OSIPhone::get_singleton()->key(KEY_BACKSPACE, true);
-}
-
-- (BOOL)hasText {
-	return keyboard_text.length() > 0;
-}
-
-- (void)insertText:(NSString *)p_text {
-	String character;
-	character.parse_utf8([p_text UTF8String]);
-	keyboard_text = keyboard_text + character;
-	OSIPhone::get_singleton()->key(character[0] == 10 ? KEY_ENTER : character[0], true);
-}
-
 // MARK: Touches
 
 - (void)initTouches {

+ 37 - 0
platform/iphone/keyboard_input_view.h

@@ -0,0 +1,37 @@
+/*************************************************************************/
+/*  keyboard_input_view.h                                                */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#import <UIKit/UIKit.h>
+
+@interface GodotKeyboardInputView : UITextView
+
+- (BOOL)becomeFirstResponderWithString:(NSString *)existingString multiline:(BOOL)flag cursorStart:(NSInteger)start cursorEnd:(NSInteger)end;
+
+@end

+ 194 - 0
platform/iphone/keyboard_input_view.mm

@@ -0,0 +1,194 @@
+/*************************************************************************/
+/*  keyboard_input_view.mm                                               */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#import "keyboard_input_view.h"
+
+#include "core/os/keyboard.h"
+#include "os_iphone.h"
+
+@interface GodotKeyboardInputView () <UITextViewDelegate>
+
+@property(nonatomic, copy) NSString *previousText;
+@property(nonatomic, assign) NSRange previousSelectedRange;
+
+@end
+
+@implementation GodotKeyboardInputView
+
+- (instancetype)initWithCoder:(NSCoder *)coder {
+	self = [super initWithCoder:coder];
+
+	if (self) {
+		[self godot_commonInit];
+	}
+
+	return self;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame textContainer:(NSTextContainer *)textContainer {
+	self = [super initWithFrame:frame textContainer:textContainer];
+
+	if (self) {
+		[self godot_commonInit];
+	}
+
+	return self;
+}
+
+- (void)godot_commonInit {
+	self.hidden = YES;
+	self.delegate = self;
+
+	[[NSNotificationCenter defaultCenter] addObserver:self
+											 selector:@selector(observeTextChange:)
+												 name:UITextViewTextDidChangeNotification
+											   object:self];
+}
+
+- (void)dealloc {
+	self.delegate = nil;
+	[[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+// MARK: Keyboard
+
+- (BOOL)canBecomeFirstResponder {
+	return YES;
+}
+
+- (BOOL)becomeFirstResponderWithString:(NSString *)existingString multiline:(BOOL)flag cursorStart:(NSInteger)start cursorEnd:(NSInteger)end {
+	self.text = existingString;
+	self.previousText = existingString;
+
+	NSRange textRange;
+
+	// Either a simple cursor or a selection.
+	if (end > 0) {
+		textRange = NSMakeRange(start, end - start);
+	} else {
+		textRange = NSMakeRange(start, 0);
+	}
+
+	self.selectedRange = textRange;
+	self.previousSelectedRange = textRange;
+
+	return [self becomeFirstResponder];
+}
+
+- (BOOL)resignFirstResponder {
+	self.text = nil;
+	self.previousText = nil;
+	return [super resignFirstResponder];
+}
+
+// MARK: OS Messages
+
+- (void)deleteText:(NSInteger)charactersToDelete {
+	for (int i = 0; i < charactersToDelete; i++) {
+		OSIPhone::get_singleton()->key(KEY_BACKSPACE, true);
+		OSIPhone::get_singleton()->key(KEY_BACKSPACE, false);
+	}
+}
+
+- (void)enterText:(NSString *)substring {
+	String characters;
+	characters.parse_utf8([substring UTF8String]);
+
+	for (int i = 0; i < characters.size(); i++) {
+		int character = characters[i];
+
+		switch (character) {
+			case 10:
+				character = KEY_ENTER;
+				break;
+			case 8198:
+				character = KEY_SPACE;
+				break;
+			default:
+				break;
+		}
+
+		OSIPhone::get_singleton()->key(character, true);
+		OSIPhone::get_singleton()->key(character, false);
+	}
+}
+
+// MARK: Observer
+
+- (void)observeTextChange:(NSNotification *)notification {
+	if (notification.object != self) {
+		return;
+	}
+
+	if (self.previousSelectedRange.length == 0) {
+		// We are deleting all text before cursor if no range was selected.
+		// This way any inserted or changed text will be updated.
+		NSString *substringToDelete = [self.previousText substringToIndex:self.previousSelectedRange.location];
+		[self deleteText:substringToDelete.length];
+	} else {
+		// If text was previously selected
+		// we are sending only one `backspace`.
+		// It will remove all text from text input.
+		[self deleteText:1];
+	}
+
+	NSString *substringToEnter;
+
+	if (self.selectedRange.length == 0) {
+		// If previous cursor had a selection
+		// we have to calculate an inserted text.
+		if (self.previousSelectedRange.length != 0) {
+			NSInteger rangeEnd = self.selectedRange.location + self.selectedRange.length;
+			NSInteger rangeStart = MIN(self.previousSelectedRange.location, self.selectedRange.location);
+			NSInteger rangeLength = MAX(0, rangeEnd - rangeStart);
+
+			NSRange calculatedRange;
+
+			if (rangeLength >= 0) {
+				calculatedRange = NSMakeRange(rangeStart, rangeLength);
+			} else {
+				calculatedRange = NSMakeRange(rangeStart, 0);
+			}
+
+			substringToEnter = [self.text substringWithRange:calculatedRange];
+		} else {
+			substringToEnter = [self.text substringToIndex:self.selectedRange.location];
+		}
+	} else {
+		substringToEnter = [self.text substringWithRange:self.selectedRange];
+	}
+
+	[self enterText:substringToEnter];
+
+	self.previousText = self.text;
+	self.previousSelectedRange = self.selectedRange;
+}
+
+@end

+ 9 - 2
platform/iphone/os_iphone.mm

@@ -50,6 +50,7 @@
 #import "app_delegate.h"
 #import "device_metrics.h"
 #import "godot_view.h"
+#import "keyboard_input_view.h"
 #import "native_video_view.h"
 #import "view_controller.h"
 
@@ -453,11 +454,17 @@ bool OSIPhone::has_virtual_keyboard() const {
 };
 
 void OSIPhone::show_virtual_keyboard(const String &p_existing_text, const Rect2 &p_screen_rect, bool p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) {
-	[AppDelegate.viewController.godotView becomeFirstResponderWithString:p_existing_text];
+	NSString *existingString = [[NSString alloc] initWithUTF8String:p_existing_text.utf8().get_data()];
+
+	[AppDelegate.viewController.keyboardView
+			becomeFirstResponderWithString:existingString
+								 multiline:p_multiline
+							   cursorStart:p_cursor_start
+								 cursorEnd:p_cursor_end];
 };
 
 void OSIPhone::hide_virtual_keyboard() {
-	[AppDelegate.viewController.godotView resignFirstResponder];
+	[AppDelegate.viewController.keyboardView resignFirstResponder];
 }
 
 void OSIPhone::set_virtual_keyboard_height(int p_height) {

+ 2 - 0
platform/iphone/view_controller.h

@@ -33,11 +33,13 @@
 
 @class GodotView;
 @class GodotNativeVideoView;
+@class GodotKeyboardInputView;
 
 @interface ViewController : UIViewController <GKGameCenterControllerDelegate>
 
 @property(nonatomic, readonly, strong) GodotView *godotView;
 @property(nonatomic, readonly, strong) GodotNativeVideoView *videoView;
+@property(nonatomic, readonly, strong) GodotKeyboardInputView *keyboardView;
 
 // MARK: Native Video Player
 

+ 8 - 1
platform/iphone/view_controller.mm

@@ -33,6 +33,7 @@
 #include "core/project_settings.h"
 #import "godot_view.h"
 #import "godot_view_renderer.h"
+#import "keyboard_input_view.h"
 #import "native_video_view.h"
 #include "os_iphone.h"
 
@@ -40,6 +41,7 @@
 
 @property(strong, nonatomic) GodotViewRenderer *renderer;
 @property(strong, nonatomic) GodotNativeVideoView *videoView;
+@property(strong, nonatomic) GodotKeyboardInputView *keyboardView;
 
 @end
 
@@ -101,6 +103,10 @@
 }
 
 - (void)observeKeyboard {
+	printf("******** setting up keyboard input view\n");
+	self.keyboardView = [GodotKeyboardInputView new];
+	[self.view addSubview:self.keyboardView];
+
 	printf("******** adding observer for keyboard show/hide\n");
 	[[NSNotificationCenter defaultCenter]
 			addObserver:self
@@ -118,7 +124,8 @@
 	[self.videoView stopVideo];
 	self.videoView = nil;
 
-	self.videoView = nil;
+	self.keyboardView = nil;
+
 	self.renderer = nil;
 
 	[[NSNotificationCenter defaultCenter] removeObserver:self];

+ 18 - 2
scene/gui/text_edit.cpp

@@ -1755,8 +1755,24 @@ void TextEdit::_notification(int p_what) {
 			Point2 cursor_pos = Point2(cursor_get_column(), cursor_get_line()) * get_row_height();
 			OS::get_singleton()->set_ime_position(get_global_position() + cursor_pos);
 
-			if (OS::get_singleton()->has_virtual_keyboard() && virtual_keyboard_enabled)
-				OS::get_singleton()->show_virtual_keyboard(get_text(), get_global_rect(), true);
+			if (OS::get_singleton()->has_virtual_keyboard() && virtual_keyboard_enabled) {
+				int cursor_start = -1;
+				int cursor_end = -1;
+
+				if (!selection.active) {
+					String full_text = _base_get_text(0, 0, cursor.line, cursor.column);
+
+					cursor_start = full_text.length();
+				} else {
+					String pre_text = _base_get_text(0, 0, selection.from_line, selection.from_column);
+					String post_text = _base_get_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column);
+
+					cursor_start = pre_text.length();
+					cursor_end = cursor_start + post_text.length();
+				}
+
+				OS::get_singleton()->show_virtual_keyboard(get_text(), get_global_rect(), true, -1, cursor_start, cursor_end);
+			}
 		} break;
 		case NOTIFICATION_FOCUS_EXIT: {