From 4b38aefd338b3b37f09bebf2bab3e4df364b5694 Mon Sep 17 00:00:00 2001 From: Fabio Alessandrelli Date: Mon, 8 Mar 2021 23:16:51 +0100 Subject: [PATCH] [HTML5] Opt-in virtual keyboard support. Added as an export option "Experimental Virtual Keyboard". There is no zoom, so text/line edit must be in the top part of the screen, or it will get hidden by the virtual keyboard. UTF8/Latin-1 only (I think regular UTF-8 should work out of the box in 4.0 but I can't test it). It uses an hidden textarea or input, based on the multiline variable, and only gets activated if the device has a touchscreen. This could cause problems on devices with both touchscreen and a real keyboard (although input should still work in general with some minor focus issues). I'm thinking of a system to detect the first physical keystroke and disable it in case, but it might do more harm then good, so it must be well thought. --- platform/javascript/export/export.cpp | 2 + platform/javascript/godot_js.h | 7 + platform/javascript/js/engine/config.js | 10 ++ .../js/libs/library_godot_display.js | 136 +++++++++++++++++- .../javascript/js/libs/library_godot_os.js | 3 + platform/javascript/os_javascript.cpp | 37 ++++- platform/javascript/os_javascript.h | 8 +- 7 files changed, 195 insertions(+), 8 deletions(-) diff --git a/platform/javascript/export/export.cpp b/platform/javascript/export/export.cpp index 726f7caf4a2..540b286e317 100644 --- a/platform/javascript/export/export.cpp +++ b/platform/javascript/export/export.cpp @@ -297,6 +297,7 @@ void EditorExportPlatformJavaScript::_fix_html(Vector &p_html, const Re } Dictionary config; config["canvasResizePolicy"] = p_preset->get("html/canvas_resize_policy"); + config["experimentalVK"] = p_preset->get("html/experimental_virtual_keyboard"); config["gdnativeLibs"] = libs; config["executable"] = p_name; config["args"] = args; @@ -355,6 +356,7 @@ void EditorExportPlatformJavaScript::get_export_options(List *r_op r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "html/custom_html_shell", PROPERTY_HINT_FILE, "*.html"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "html/head_include", PROPERTY_HINT_MULTILINE_TEXT), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "html/canvas_resize_policy", PROPERTY_HINT_ENUM, "None,Project,Adaptive"), 2)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "html/experimental_virtual_keyboard"), false)); } String EditorExportPlatformJavaScript::get_name() const { diff --git a/platform/javascript/godot_js.h b/platform/javascript/godot_js.h index f86aadd2c29..4448a356702 100644 --- a/platform/javascript/godot_js.h +++ b/platform/javascript/godot_js.h @@ -93,6 +93,13 @@ extern void godot_js_display_notification_cb(void (*p_callback)(int p_notificati extern void godot_js_display_paste_cb(void (*p_callback)(const char *p_text)); extern void godot_js_display_drop_files_cb(void (*p_callback)(char **p_filev, int p_filec)); extern void godot_js_display_setup_canvas(int p_width, int p_height, int p_fullscreen, int p_hidpi); + +// Display Virtual Keyboard +extern int godot_js_display_vk_available(); +extern void godot_js_display_vk_cb(void (*p_input)(const char *p_text, int p_cursor)); +extern void godot_js_display_vk_show(const char *p_text, int p_multiline, int p_start, int p_end); +extern void godot_js_display_vk_hide(); + #ifdef __cplusplus } #endif diff --git a/platform/javascript/js/engine/config.js b/platform/javascript/js/engine/config.js index 82ff273ecf2..25d71d0905c 100644 --- a/platform/javascript/js/engine/config.js +++ b/platform/javascript/js/engine/config.js @@ -90,6 +90,14 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused- * @default */ args: [], + /** + * When enabled, this will turn on experimental virtual keyboard support on mobile. + * + * @memberof EngineConfig + * @type {boolean} + * @default + */ + experimentalVK: false, /** * @ignore * @type {Array.} @@ -223,6 +231,7 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused- this.locale = parse('locale', this.locale); this.canvasResizePolicy = parse('canvasResizePolicy', this.canvasResizePolicy); this.persistentPaths = parse('persistentPaths', this.persistentPaths); + this.experimentalVK = parse('experimentalVK', this.experimentalVK); this.gdnativeLibs = parse('gdnativeLibs', this.gdnativeLibs); this.fileSizes = parse('fileSizes', this.fileSizes); this.args = parse('args', this.args); @@ -307,6 +316,7 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused- 'canvas': this.canvas, 'canvasResizePolicy': this.canvasResizePolicy, 'locale': locale, + 'virtualKeyboard': this.experimentalVK, 'onExecute': this.onExecute, 'onExit': function (p_code) { cleanup(); // We always need to call the cleanup callback to free memory. diff --git a/platform/javascript/js/libs/library_godot_display.js b/platform/javascript/js/libs/library_godot_display.js index b4f1fee4ed1..781a202f89f 100644 --- a/platform/javascript/js/libs/library_godot_display.js +++ b/platform/javascript/js/libs/library_godot_display.js @@ -231,6 +231,105 @@ const GodotDisplayDragDrop = { }; mergeInto(LibraryManager.library, GodotDisplayDragDrop); +const GodotDisplayVK = { + + $GodotDisplayVK__deps: ['$GodotRuntime', '$GodotConfig', '$GodotDisplayListeners'], + $GodotDisplayVK__postset: 'GodotOS.atexit(function(resolve, reject) { GodotDisplayVK.clear(); resolve(); });', + $GodotDisplayVK: { + textinput: null, + textarea: null, + + available: function () { + return GodotConfig.virtual_keyboard && 'ontouchstart' in window; + }, + + init: function (input_cb) { + function create(what) { + const elem = document.createElement(what); + elem.style.display = 'none'; + elem.style.position = 'absolute'; + elem.style.zIndex = '-1'; + elem.style.background = 'transparent'; + elem.style.padding = '0px'; + elem.style.margin = '0px'; + elem.style.overflow = 'hidden'; + elem.style.width = '0px'; + elem.style.height = '0px'; + elem.style.border = '0px'; + elem.style.outline = 'none'; + elem.readonly = true; + elem.disabled = true; + GodotDisplayListeners.add(elem, 'input', function (evt) { + const c_str = GodotRuntime.allocString(elem.value); + input_cb(c_str, elem.selectionEnd); + GodotRuntime.free(c_str); + }, false); + GodotDisplayListeners.add(elem, 'blur', function (evt) { + elem.style.display = 'none'; + elem.readonly = true; + elem.disabled = true; + }, false); + GodotConfig.canvas.insertAdjacentElement('beforebegin', elem); + return elem; + } + GodotDisplayVK.textinput = create('input'); + GodotDisplayVK.textarea = create('textarea'); + GodotDisplayVK.updateSize(); + }, + show: function (text, multiline, start, end) { + if (!GodotDisplayVK.textinput || !GodotDisplayVK.textarea) { + return; + } + if (GodotDisplayVK.textinput.style.display !== '' || GodotDisplayVK.textarea.style.display !== '') { + GodotDisplayVK.hide(); + } + GodotDisplayVK.updateSize(); + const elem = multiline ? GodotDisplayVK.textarea : GodotDisplayVK.textinput; + elem.readonly = false; + elem.disabled = false; + elem.value = text; + elem.style.display = 'block'; + elem.focus(); + elem.setSelectionRange(start, end); + }, + hide: function () { + if (!GodotDisplayVK.textinput || !GodotDisplayVK.textarea) { + return; + } + [GodotDisplayVK.textinput, GodotDisplayVK.textarea].forEach(function (elem) { + elem.blur(); + elem.style.display = 'none'; + elem.value = ''; + }); + }, + updateSize: function () { + if (!GodotDisplayVK.textinput || !GodotDisplayVK.textarea) { + return; + } + const rect = GodotConfig.canvas.getBoundingClientRect(); + function update(elem) { + elem.style.left = `${rect.left}px`; + elem.style.top = `${rect.top}px`; + elem.style.width = `${rect.width}px`; + elem.style.height = `${rect.height}px`; + } + update(GodotDisplayVK.textinput); + update(GodotDisplayVK.textarea); + }, + clear: function () { + if (GodotDisplayVK.textinput) { + GodotDisplayVK.textinput.remove(); + GodotDisplayVK.textinput = null; + } + if (GodotDisplayVK.textarea) { + GodotDisplayVK.textarea.remove(); + GodotDisplayVK.textarea = null; + } + }, + }, +}; +mergeInto(LibraryManager.library, GodotDisplayVK); + /* * Display server cursor helper. * Keeps track of cursor status and custom shapes. @@ -511,7 +610,7 @@ mergeInto(LibraryManager.library, GodotDisplayScreen); * Exposes all the functions needed by DisplayServer implementation. */ const GodotDisplay = { - $GodotDisplay__deps: ['$GodotConfig', '$GodotRuntime', '$GodotDisplayCursor', '$GodotDisplayListeners', '$GodotDisplayDragDrop', '$GodotDisplayGamepads', '$GodotDisplayScreen'], + $GodotDisplay__deps: ['$GodotConfig', '$GodotRuntime', '$GodotDisplayCursor', '$GodotDisplayListeners', '$GodotDisplayDragDrop', '$GodotDisplayGamepads', '$GodotDisplayScreen', '$GodotDisplayVK'], $GodotDisplay: { window_icon: '', findDPI: function () { @@ -580,7 +679,11 @@ const GodotDisplay = { godot_js_display_size_update__sig: 'i', godot_js_display_size_update: function () { - return GodotDisplayScreen.updateSize(); + const updated = GodotDisplayScreen.updateSize(); + if (updated) { + GodotDisplayVK.updateSize(); + } + return updated; }, godot_js_display_screen_size_get__sig: 'vii', @@ -810,6 +913,35 @@ const GodotDisplay = { } }, + /* + * Virtual Keyboard + */ + godot_js_display_vk_show__sig: 'viiii', + godot_js_display_vk_show: function (p_text, p_multiline, p_start, p_end) { + const text = GodotRuntime.parseString(p_text); + const start = p_start > 0 ? p_start : 0; + const end = p_end > 0 ? p_end : start; + GodotDisplayVK.show(text, p_multiline, start, end); + }, + + godot_js_display_vk_hide__sig: 'v', + godot_js_display_vk_hide: function () { + GodotDisplayVK.hide(); + }, + + godot_js_display_vk_available__sig: 'i', + godot_js_display_vk_available: function () { + return GodotDisplayVK.available(); + }, + + godot_js_display_vk_cb__sig: 'vi', + godot_js_display_vk_cb: function (p_input_cb) { + const input_cb = GodotRuntime.get_func(p_input_cb); + if (GodotDisplayVK.available()) { + GodotDisplayVK.init(input_cb); + } + }, + /* * Gamepads */ diff --git a/platform/javascript/js/libs/library_godot_os.js b/platform/javascript/js/libs/library_godot_os.js index 0f189b013c7..775a822d881 100644 --- a/platform/javascript/js/libs/library_godot_os.js +++ b/platform/javascript/js/libs/library_godot_os.js @@ -59,6 +59,7 @@ const GodotConfig = { canvas: null, locale: 'en', canvas_resize_policy: 2, // Adaptive + virtual_keyboard: false, on_execute: null, on_exit: null, @@ -66,6 +67,7 @@ const GodotConfig = { GodotConfig.canvas_resize_policy = p_opts['canvasResizePolicy']; GodotConfig.canvas = p_opts['canvas']; GodotConfig.locale = p_opts['locale'] || GodotConfig.locale; + GodotConfig.virtual_keyboard = p_opts['virtualKeyboard']; GodotConfig.on_execute = p_opts['onExecute']; GodotConfig.on_exit = p_opts['onExit']; }, @@ -77,6 +79,7 @@ const GodotConfig = { GodotConfig.canvas = null; GodotConfig.locale = 'en'; GodotConfig.canvas_resize_policy = 2; + GodotConfig.virtual_keyboard = false; GodotConfig.on_execute = null; GodotConfig.on_exit = null; }, diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp index de86c4910c6..9c4ef4ffb49 100644 --- a/platform/javascript/os_javascript.cpp +++ b/platform/javascript/os_javascript.cpp @@ -897,12 +897,46 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, godot_js_display_paste_cb(&OS_JavaScript::update_clipboard_callback); godot_js_display_drop_files_cb(&OS_JavaScript::drop_files_callback); godot_js_display_gamepad_cb(&OS_JavaScript::gamepad_callback); + godot_js_display_vk_cb(&input_text_callback); visual_server->init(); return OK; } +void OS_JavaScript::input_text_callback(const char *p_text, int p_cursor) { + OS_JavaScript *os = OS_JavaScript::get_singleton(); + if (!os || !os->get_main_loop()) { + return; + } + os->get_main_loop()->input_text(String::utf8(p_text)); + Ref k; + for (int i = 0; i < p_cursor; i++) { + k.instance(); + k->set_pressed(true); + k->set_echo(false); + k->set_scancode(KEY_RIGHT); + os->input->parse_input_event(k); + k.instance(); + k->set_pressed(false); + k->set_echo(false); + k->set_scancode(KEY_RIGHT); + os->input->parse_input_event(k); + } +} + +bool OS_JavaScript::has_virtual_keyboard() const { + return godot_js_display_vk_available() != 0; +} + +void OS_JavaScript::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) { + godot_js_display_vk_show(p_existing_text.utf8().get_data(), p_multiline, p_cursor_start, p_cursor_end); +} + +void OS_JavaScript::hide_virtual_keyboard() { + godot_js_display_vk_hide(); +} + bool OS_JavaScript::get_swap_ok_cancel() { return swap_ok_cancel; } @@ -1192,9 +1226,6 @@ OS_JavaScript::OS_JavaScript() { last_click_ms = 0; last_click_pos = Point2(-100, -100); - last_width = 0; - last_height = 0; - window_maximized = false; entering_fullscreen = false; just_exited_fullscreen = false; diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index 31e7c2c7e32..58dd36e1ef5 100644 --- a/platform/javascript/os_javascript.h +++ b/platform/javascript/os_javascript.h @@ -60,9 +60,6 @@ private: double last_click_ms; int last_click_button_index; - int last_width; - int last_height; - MainLoop *main_loop; int video_driver_index; AudioDriverJavaScript *audio_driver_javascript; @@ -89,6 +86,7 @@ private: static EM_BOOL touchmove_callback(int p_event_type, const EmscriptenTouchEvent *p_event, void *p_user_data); static void gamepad_callback(int p_index, int p_connected, const char *p_id, const char *p_guid); + static void input_text_callback(const char *p_text, int p_cursor); void process_joypads(); static void file_access_close_callback(const String &p_file, int p_flags); @@ -120,6 +118,10 @@ public: // Override return type to make writing static callbacks less tedious. static OS_JavaScript *get_singleton(); + virtual bool has_virtual_keyboard() const; + virtual void show_virtual_keyboard(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), bool p_multiline = false, int p_max_input_length = -1, int p_cursor_start = -1, int p_cursor_end = -1); + virtual void hide_virtual_keyboard(); + virtual bool get_swap_ok_cancel(); virtual void swap_buffers(); virtual void set_video_mode(const VideoMode &p_video_mode, int p_screen = 0);