diff --git a/platform/iphone/SCsub b/platform/iphone/SCsub index 6d7f37f137f..c5c459ab98f 100644 --- a/platform/iphone/SCsub +++ b/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", ] diff --git a/platform/iphone/godot_view.h b/platform/iphone/godot_view.h index bfc1484c1cc..cf502e74ddf 100644 --- a/platform/iphone/godot_view.h +++ b/platform/iphone/godot_view.h @@ -40,7 +40,7 @@ class String; @protocol DisplayLayer; @protocol GodotViewRendererProtocol; -@interface GodotView : UIView +@interface GodotView : UIView @property(assign, nonatomic) id renderer; @@ -55,8 +55,6 @@ class String; - (void)stopRendering; - (void)startRendering; -- (BOOL)becomeFirstResponderWithString:(String)p_existing; - @property(nonatomic, assign) BOOL useCADisplayLink; @end diff --git a/platform/iphone/godot_view.mm b/platform/iphone/godot_view.mm index d2f7bebdd19..a6c9a08e2ab 100644 --- a/platform/iphone/godot_view.mm +++ b/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 { diff --git a/platform/iphone/keyboard_input_view.h b/platform/iphone/keyboard_input_view.h new file mode 100644 index 00000000000..5382a2e9a94 --- /dev/null +++ b/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 + +@interface GodotKeyboardInputView : UITextView + +- (BOOL)becomeFirstResponderWithString:(NSString *)existingString multiline:(BOOL)flag cursorStart:(NSInteger)start cursorEnd:(NSInteger)end; + +@end diff --git a/platform/iphone/keyboard_input_view.mm b/platform/iphone/keyboard_input_view.mm new file mode 100644 index 00000000000..c3d3d542de1 --- /dev/null +++ b/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 () + +@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 diff --git a/platform/iphone/os_iphone.mm b/platform/iphone/os_iphone.mm index ecb52ba3a3b..082a02fbfcf 100644 --- a/platform/iphone/os_iphone.mm +++ b/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) { diff --git a/platform/iphone/view_controller.h b/platform/iphone/view_controller.h index b0b31ae377c..c246973e7ef 100644 --- a/platform/iphone/view_controller.h +++ b/platform/iphone/view_controller.h @@ -33,11 +33,13 @@ @class GodotView; @class GodotNativeVideoView; +@class GodotKeyboardInputView; @interface ViewController : UIViewController @property(nonatomic, readonly, strong) GodotView *godotView; @property(nonatomic, readonly, strong) GodotNativeVideoView *videoView; +@property(nonatomic, readonly, strong) GodotKeyboardInputView *keyboardView; // MARK: Native Video Player diff --git a/platform/iphone/view_controller.mm b/platform/iphone/view_controller.mm index 5b489692727..d6ed42ccb51 100644 --- a/platform/iphone/view_controller.mm +++ b/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]; diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 7c40694ebe1..4961dd8bebb 100644 --- a/scene/gui/text_edit.cpp +++ b/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: {