Przeglądaj źródła

[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.
Fabio Alessandrelli 4 lat temu
rodzic
commit
3416f7b521

+ 41 - 2
platform/javascript/display_server_javascript.cpp

@@ -536,6 +536,43 @@ bool DisplayServerJavaScript::screen_is_touchscreen(int p_screen) const {
 	return godot_js_display_touchscreen_is_available();
 }
 
+// Virtual Keybaord
+void DisplayServerJavaScript::vk_input_text_callback(const char *p_text, int p_cursor) {
+	DisplayServerJavaScript *ds = DisplayServerJavaScript::get_singleton();
+	if (!ds || ds->input_text_callback.is_null()) {
+		return;
+	}
+	// Call input_text
+	Variant event = String(p_text);
+	Variant *eventp = &event;
+	Variant ret;
+	Callable::CallError ce;
+	ds->input_text_callback.call((const Variant **)&eventp, 1, ret, ce);
+	// Insert key right to reach position.
+	Input *input = Input::get_singleton();
+	Ref<InputEventKey> k;
+	for (int i = 0; i < p_cursor; i++) {
+		k.instance();
+		k->set_pressed(true);
+		k->set_echo(false);
+		k->set_keycode(KEY_RIGHT);
+		input->parse_input_event(k);
+		k.instance();
+		k->set_pressed(false);
+		k->set_echo(false);
+		k->set_keycode(KEY_RIGHT);
+		input->parse_input_event(k);
+	}
+}
+
+void DisplayServerJavaScript::virtual_keyboard_show(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 DisplayServerJavaScript::virtual_keyboard_hide() {
+	godot_js_display_vk_hide();
+}
+
 // Gamepad
 void DisplayServerJavaScript::gamepad_callback(int p_index, int p_connected, const char *p_id, const char *p_guid) {
 	Input *input = Input::get_singleton();
@@ -764,6 +801,7 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive
 	godot_js_display_paste_cb(update_clipboard_callback);
 	godot_js_display_drop_files_cb(drop_files_js_callback);
 	godot_js_display_gamepad_cb(&DisplayServerJavaScript::gamepad_callback);
+	godot_js_display_vk_cb(&vk_input_text_callback);
 
 	Input::get_singleton()->set_event_dispatch_function(_dispatch_input_event);
 }
@@ -793,7 +831,8 @@ bool DisplayServerJavaScript::has_feature(Feature p_feature) const {
 		//case FEATURE_WINDOW_TRANSPARENCY:
 		//case FEATURE_KEEP_SCREEN_ON:
 		//case FEATURE_ORIENTATION:
-		//case FEATURE_VIRTUAL_KEYBOARD:
+		case FEATURE_VIRTUAL_KEYBOARD:
+			return godot_js_display_vk_available() != 0;
 		default:
 			return false;
 	}
@@ -866,7 +905,7 @@ void DisplayServerJavaScript::window_set_input_event_callback(const Callable &p_
 }
 
 void DisplayServerJavaScript::window_set_input_text_callback(const Callable &p_callable, WindowID p_window) {
-	input_text_callback = p_callable; // TODO unused... do I need this?
+	input_text_callback = p_callable;
 }
 
 void DisplayServerJavaScript::window_set_drop_files_callback(const Callable &p_callable, WindowID p_window) {

+ 5 - 0
platform/javascript/display_server_javascript.h

@@ -75,6 +75,8 @@ private:
 	static EM_BOOL keypress_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data);
 	static EM_BOOL keyup_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data);
 
+	static void vk_input_text_callback(const char *p_text, int p_cursor);
+
 	static EM_BOOL mousemove_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data);
 	static EM_BOOL mouse_button_callback(int p_event_type, const EmscriptenMouseEvent *p_event, void *p_user_data);
 
@@ -135,6 +137,9 @@ public:
 	int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
 	float screen_get_scale(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
 
+	void virtual_keyboard_show(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) override;
+	void virtual_keyboard_hide() override;
+
 	// windows
 	Vector<DisplayServer::WindowID> get_window_list() const override;
 	WindowID get_window_at_screen_position(const Point2i &p_position) const override;

+ 2 - 0
platform/javascript/export/export.cpp

@@ -297,6 +297,7 @@ void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &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;
@@ -352,6 +353,7 @@ void EditorExportPlatformJavaScript::get_export_options(List<ExportOption> *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 {

+ 7 - 0
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

+ 10 - 0
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.<string>}
@@ -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.

+ 134 - 2
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',
@@ -811,6 +914,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
 	 */

+ 3 - 0
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;
 		},