[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.
This commit is contained in:
Fabio Alessandrelli 2021-03-08 23:16:51 +01:00
parent eda5ae9d75
commit 3416f7b521
7 changed files with 202 additions and 4 deletions

View file

@ -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) {

View file

@ -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;

View file

@ -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 {

View file

@ -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

View file

@ -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.

View file

@ -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
*/

View file

@ -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;
},