[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:
parent
9952a5039a
commit
4b38aefd33
@ -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;
|
||||
@ -355,6 +356,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 {
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -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<InputEventKey> 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;
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user