diff options
| author | Andre Weissflog <floooh@gmail.com> | 2019-12-02 17:56:31 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-12-02 17:56:31 +0100 |
| commit | caea66533e96dd85d46086e4c3455d01a698aad3 (patch) | |
| tree | 9fc672ecd3d8478dfd30e162aa0952fcce08d2a7 | |
| parent | c3020f9876a96044ad6c8ef43884c03e460159a8 (diff) | |
Initial clipboard support (#237)
* WIP clipboard support code dump.
HTML5 clipboard/permissions API is a mess, so that's not an option.
* sokol_app.h: clipboard on emscripten working
* sokol_imgui.h: init-flag to disable Ctrl-hotkeys
* sokol_app.h: new sapp_consume_event() function, rename clipboard event
* add some todos to sokol_app.h
* macOS: send CLIPBOARD_PASTED event
* sokol_app.h: add Win32 clipboard support
* sokol_app.h: documentation for clipboard support
* sokol_app.h: dummy clipboard support
* Start adding clipboard support to sokol_imgui.
Current problem: on HTML5, copy/paste must happen inside JS event
handler.
* mention new sokol_app.h clipboard support in README
* readme tweaks
* sokol_app.h: MSVC warnings fixed
* sokol_imgui.h: fix key debuffering
* sokol_app.h: documentation fixes
| -rw-r--r-- | README.md | 22 | ||||
| -rw-r--r-- | sokol_app.h | 321 | ||||
| -rw-r--r-- | util/sokol_imgui.h | 135 |
3 files changed, 457 insertions, 21 deletions
@@ -6,7 +6,7 @@ Simple [STB-style](https://github.com/nothings/stb/blob/master/docs/stb_howto.txt) cross-platform libraries for C and C++, written in C. -[See what's new](#updates) (**08-Sep-2019**: clamp-to-border texture sampling in sokol_gfx.h) +[See what's new](#updates) (**02-Dec-2019**: initial clipboard support in sokol_app.h) [Live Samples](https://floooh.github.io/sokol-html5/index.html) via WASM. @@ -465,6 +465,26 @@ Mainly some "missing features" for desktop apps: # Updates +- **02-Dec-2019**: Initial clipboard support in sokol_app.h for Windows, macOS + and HTML5. This allows to read and write UTF-8 encoded strings from and + to the target platform's shared clipboard. + + A 'real-world' example usage is in the [Visual6502 Remix project](https://github.com/floooh/v6502r). + + Unfortunately clipboard support on the HTML5 platform comes with a lot of + platform-specific caveats which can't be solved in sokol_app.h alone + because of the restrictions the web platform puts on clipboard access and + different behaviours and support levels of the various HTML5 clipboard + APIs. I'm not really happy with the current HTML5 clipboard + implementation. It sorta works, but it sure ain't pretty :) + + Maybe the situation will improve in a few years when all browsers agree + on and support the new [permission-based clipboard + API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API). + + For documention of the clipboard feature, search for CLIPBOARD SUPPORT + in sokol_app.h + - **08-Sep-2019**: sokol_gfx.h now supports clamp-to-border texture sampling: - the enum ```sg_wrap``` has a new member ```SG_WRAP_CLAMP_TO_BORDER``` - there's a new enum ```sg_border_color``` diff --git a/sokol_app.h b/sokol_app.h index ddcf2e7d..c91b7d52 100644 --- a/sokol_app.h +++ b/sokol_app.h @@ -101,8 +101,13 @@ screen keyboard | --- | --- | --- | YES | TODO | --- | YES swap interval | YES | YES | YES | YES | TODO | TODO | YES high-dpi | YES | YES | TODO | YES | YES | TODO | YES + clipboard | YES | YES | TODO | --- | --- | --- | YES - - what about bluetooth keyboard / mouse on mobile platforms? + TODO + ==== + - Linux clipboard support + - document sapp_consume_event() + - sapp_consume_event() on non-web platforms? STEP BY STEP ============ @@ -275,6 +280,58 @@ "Really Quit?" dialog box). Note that the cleanup-callback isn't called on the web and mobile platforms. + CLIPBOARD SUPPORT + ================= + Applications can send and receive UTF-8 encoded text data from and to the + system clipboard. By default, clipboard support is disabled and + must be enabled at startup via the following sapp_desc struct + members: + + sapp_desc.enable_clipboard - set to true to enable clipboard support + sapp_desc.clipboard_size - size of the internal clipboard buffer in bytes + + Enabling the clipboard will dynamically allocate a clipboard buffer + for UTF-8 encoded text data of the requested size in bytes, the default + size if 8 KBytes. Strings that don't fit into the clipboard buffer + (including the terminating zero) will be silently clipped, so it's + important that you provide a big enough clipboard size for your + use case. + + To send data to the clipboard, call sapp_set_clipboard_string() with + a pointer to an UTF-8 encoded, null-terminated C-string. + + NOTE that on the HTML5 platform, sapp_set_clipboard_string() must be + called from inside a 'short-lived event handler', and there are a few + other HTML5-specific caveats to workaround. You'll basically have to + tinker until it works in all browsers :/ (maybe the situation will + improve when all browsers agree on and implement the new + HTML5 navigator.clipboard API). + + To get data from the clipboard, check for the SAPP_EVENTTYPE_CLIPBOARD_PASTED + event in your event handler function, and then call sapp_get_clipboard_string() + to obtain the updated UTF-8 encoded text. + + NOTE that behaviour of sapp_get_clipboard_string() is slightly different + depending on platform: + + - on the HTML5 platform, the internal clipboard buffer will only be updated + right before the SAPP_EVENTTYPE_CLIPBOARD_PASTED event is sent, + and sapp_get_clipboard_string() will simply return the current content + of the clipboard buffer + - on 'native' platforms, the call to sapp_get_clipboard_string() will + update the internal clipboard buffer with the most recent data + from the system clipboard + + Portable code should check for the SAPP_EVENTTYPE_CLIPBOARD_PASTED event, + and then call sapp_get_clipboard_string() right in the event handler. + + The SAPP_EVENTTYPE_CLIPBOARD_PASTED event will be generated by sokol-app + as follows: + + - on macOS: when the Cmd+V key is pressed down + - on HTML5: when the browser sends a 'paste' event to the global 'window' object + - on all other platforms: when the Ctrl+V key is pressed down + HIGH-DPI RENDERING ================== You can set the sapp_desc.high_dpi flag during initialization to request @@ -526,6 +583,7 @@ typedef enum sapp_event_type { SAPP_EVENTTYPE_RESUMED, SAPP_EVENTTYPE_UPDATE_CURSOR, SAPP_EVENTTYPE_QUIT_REQUESTED, + SAPP_EVENTTYPE_CLIPBOARD_PASTED, _SAPP_EVENTTYPE_NUM, _SAPP_EVENTTYPE_FORCE_U32 = 0x7FFFFFFF } sapp_event_type; @@ -719,6 +777,8 @@ typedef struct sapp_desc { bool alpha; /* whether the framebuffer should have an alpha channel (ignored on some platforms) */ const char* window_title; /* the window title as UTF-8 encoded string */ bool user_cursor; /* if true, user is expected to manage cursor image in SAPP_EVENTTYPE_UPDATE_CURSOR */ + bool enable_clipboard; /* enable clipboard access, default is false */ + int clipboard_size; /* max size of clipboard content in bytes */ const char* html5_canvas_name; /* the name (id) of the HTML5 canvas element, default is "canvas" */ bool html5_canvas_resize; /* if true, the HTML5 canvas size is set to sapp_desc.width/height, otherwise canvas size is tracked */ @@ -760,8 +820,14 @@ SOKOL_API_DECL void sapp_request_quit(void); SOKOL_API_DECL void sapp_cancel_quit(void); /* intiate a "hard quit" (quit application without sending SAPP_EVENTTYPE_QUIT_REQUSTED) */ SOKOL_API_DECL void sapp_quit(void); +/* call from inside event callback to consume the current event (don't forward to platform) */ +SOKOL_API_DECL void sapp_consume_event(void); /* get the current frame counter (for comparison with sapp_event.frame_count) */ SOKOL_API_DECL uint64_t sapp_frame_count(void); +/* write string into clipboard */ +SOKOL_API_DECL void sapp_set_clipboard_string(const char* str); +/* read string from clipboard (usually during SAPP_EVENTTYPE_CLIPBOARD_PASTED) */ +SOKOL_API_DECL const char* sapp_get_clipboard_string(void); /* special run-function for SOKOL_NO_ENTRY (in standard mode this is an empty stub) */ SOKOL_API_DECL int sapp_run(const sapp_desc* desc); @@ -933,6 +999,7 @@ typedef struct { bool cleanup_called; bool quit_requested; bool quit_ordered; + bool event_consumed; const char* html5_canvas_name; bool html5_ask_leave_site; char window_title[_SAPP_MAX_TITLE_LENGTH]; /* UTF-8 */ @@ -945,6 +1012,9 @@ typedef struct { sapp_event event; sapp_desc desc; sapp_keycode keycodes[SAPP_MAX_KEYCODES]; + bool clipboard_enabled; + int clipboard_size; + char* clipboard; } _sapp_state; static _sapp_state _sapp; @@ -994,7 +1064,7 @@ _SOKOL_PRIVATE void _sapp_call_cleanup(void) { } } -_SOKOL_PRIVATE void _sapp_call_event(const sapp_event* e) { +_SOKOL_PRIVATE bool _sapp_call_event(const sapp_event* e) { if (!_sapp.cleanup_called) { if (_sapp.desc.event_cb) { _sapp.desc.event_cb(e); @@ -1003,6 +1073,13 @@ _SOKOL_PRIVATE void _sapp_call_event(const sapp_event* e) { _sapp.desc.event_userdata_cb(e, _sapp.desc.user_data); } } + if (_sapp.event_consumed) { + _sapp.event_consumed = false; + return true; + } + else { + return false; + } } _SOKOL_PRIVATE void _sapp_strcpy(const char* src, char* dst, int max_len) { @@ -1034,6 +1111,11 @@ _SOKOL_PRIVATE void _sapp_init_state(const sapp_desc* desc) { _sapp.swap_interval = _sapp_def(_sapp.desc.swap_interval, 1); _sapp.html5_canvas_name = _sapp_def(_sapp.desc.html5_canvas_name, "canvas"); _sapp.html5_ask_leave_site = _sapp.desc.html5_ask_leave_site; + _sapp.clipboard_enabled = _sapp.desc.enable_clipboard; + if (_sapp.clipboard_enabled) { + _sapp.clipboard_size = _sapp_def(_sapp.desc.clipboard_size, 8192); + _sapp.clipboard = (char*) SOKOL_CALLOC(1, _sapp.clipboard_size); + } if (_sapp.desc.window_title) { _sapp_strcpy(_sapp.desc.window_title, _sapp.window_title, sizeof(_sapp.window_title)); } @@ -1043,6 +1125,14 @@ _SOKOL_PRIVATE void _sapp_init_state(const sapp_desc* desc) { _sapp.dpi_scale = 1.0f; } +_SOKOL_PRIVATE void _sapp_discard_state(void) { + if (_sapp.clipboard_enabled) { + SOKOL_ASSERT(_sapp.clipboard); + SOKOL_FREE((void*)_sapp.clipboard); + } + memset(&_sapp, 0, sizeof(_sapp)); +} + _SOKOL_PRIVATE void _sapp_init_event(sapp_event_type type) { memset(&_sapp.event, 0, sizeof(_sapp.event)); _sapp.event.type = type; @@ -1254,6 +1344,7 @@ _SOKOL_PRIVATE void _sapp_run(const sapp_desc* desc) { NSApp.delegate = _sapp_macos_app_dlg_obj; [NSApp activateIgnoringOtherApps:YES]; [NSApp run]; + _sapp_discard_state(); } /* MacOS entry function */ @@ -1592,9 +1683,10 @@ _SOKOL_PRIVATE void _sapp_macos_app_event(sapp_event_type type) { as a workaround, to prevent key presses from sticking we'll send a keyup event following right after the keydown if SUPER is also pressed */ - _sapp_macos_key_event(SAPP_EVENTTYPE_KEY_DOWN, _sapp_translate_key(event.keyCode), event.isARepeat, mods); + const sapp_keycode key_code = _sapp_translate_key(event.keyCode); + _sapp_macos_key_event(SAPP_EVENTTYPE_KEY_DOWN, key_code, event.isARepeat, mods); if (0 != (mods & SAPP_MODIFIER_SUPER)) { - _sapp_macos_key_event(SAPP_EVENTTYPE_KEY_UP, _sapp_translate_key(event.keyCode), event.isARepeat, mods); + _sapp_macos_key_event(SAPP_EVENTTYPE_KEY_UP, key_code, event.isARepeat, mods); } const NSString* chars = event.characters; const NSUInteger len = chars.length; @@ -1611,6 +1703,11 @@ _SOKOL_PRIVATE void _sapp_macos_app_event(sapp_event_type type) { _sapp_call_event(&_sapp.event); } } + /* if this is a Cmd+V (paste), also send a CLIPBOARD_PASTE event */ + if (_sapp.clipboard_enabled && (mods == SAPP_MODIFIER_SUPER) && (key_code == SAPP_KEYCODE_V)) { + _sapp_init_event(SAPP_EVENTTYPE_CLIPBOARD_PASTED); + _sapp_call_event(&_sapp.event); + } } } - (void)keyUp:(NSEvent*)event { @@ -1655,6 +1752,31 @@ _SOKOL_PRIVATE void _sapp_macos_app_event(sapp_event_type type) { } @end +void _sapp_macos_set_clipboard_string(const char* str) { + @autoreleasepool { + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard declareTypes:@[NSPasteboardTypeString] owner:nil]; + [pasteboard setString:@(str) forType:NSPasteboardTypeString]; + } +} + +const char* _sapp_macos_get_clipboard_string(void) { + SOKOL_ASSERT(_sapp.clipboard); + @autoreleasepool { + _sapp.clipboard[0] = 0; + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + if (![[pasteboard types] containsObject:NSPasteboardTypeString]) { + return _sapp.clipboard; + } + NSString* str = [pasteboard stringForType:NSPasteboardTypeString]; + if (!str) { + return _sapp.clipboard; + } + _sapp_strcpy([str UTF8String], _sapp.clipboard, _sapp.clipboard_size); + } + return _sapp.clipboard; +} + #endif /* MacOS */ /*== iOS =====================================================================*/ @@ -1708,6 +1830,7 @@ _SOKOL_PRIVATE void _sapp_run(const sapp_desc* desc) { static int argc = 1; static char* argv[] = { (char*)"sokol_app" }; UIApplicationMain(argc, argv, nil, NSStringFromClass([_sapp_app_delegate class])); + _sapp_discard_state(); } /* iOS entry function */ @@ -2073,6 +2196,16 @@ EM_JS(void, sapp_js_unfocus_textfield, (void), { document.getElementById("_sokol_app_input_element").blur(); }); +EMSCRIPTEN_KEEPALIVE void _sapp_emsc_onpaste(const char* str) { + if (_sapp.clipboard_enabled) { + _sapp_strcpy(str, _sapp.clipboard, _sapp.clipboard_size); + if (_sapp_events_enabled()) { + _sapp_init_event(SAPP_EVENTTYPE_CLIPBOARD_PASTED); + _sapp_call_event(&_sapp.event); + } + } +} + /* https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload */ EMSCRIPTEN_KEEPALIVE int _sapp_html5_get_ask_leave_site(void) { return _sapp.html5_ask_leave_site ? 1 : 0; @@ -2087,6 +2220,35 @@ EM_JS(void, sapp_js_hook_beforeunload, (void), { }); }); +EM_JS(void, sapp_js_init_clipboard, (void), { + window.addEventListener('paste', function(event) { + var pasted_str = event.clipboardData.getData('text'); + ccall('_sapp_emsc_onpaste', 'void', ['string'], [pasted_str]); + }); +}); + +EM_JS(void, sapp_js_write_clipboard, (const char* c_str), { + var str = UTF8ToString(c_str); + var ta = document.createElement('textarea'); + ta.setAttribute('autocomplete', 'off'); + ta.setAttribute('autocorrect', 'off'); + ta.setAttribute('autocapitalize', 'off'); + ta.setAttribute('spellcheck', 'false'); + ta.style.left = -100 + 'px'; + ta.style.top = -100 + 'px'; + ta.style.height = 1; + ta.style.width = 1; + ta.value = str; + document.body.appendChild(ta); + ta.select(); + document.execCommand('copy'); + document.body.removeChild(ta); +}); + +_SOKOL_PRIVATE void _sapp_emsc_set_clipboard_string(const char* str) { + sapp_js_write_clipboard(str); +} + /* called from the emscripten event handler to update the keyboard visibility state, this must happen from an JS input event handler, otherwise the request will be ignored by the browser @@ -2317,6 +2479,10 @@ _SOKOL_PRIVATE EM_BOOL _sapp_emsc_key_cb(int emsc_type, const EmscriptenKeyboard } if (type == SAPP_EVENTTYPE_CHAR) { _sapp.event.char_code = emsc_event->charCode; + /* workaround to make Cmd+V work on Safari */ + if ((emsc_event->metaKey) && (emsc_event->charCode == 118)) { + retval = false; + } } else { _sapp.event.key_code = _sapp_translate_key(emsc_event->keyCode); @@ -2397,10 +2563,15 @@ _SOKOL_PRIVATE EM_BOOL _sapp_emsc_key_cb(int emsc_type, const EmscriptenKeyboard break; } } - _sapp_call_event(&_sapp.event); + if (_sapp_call_event(&_sapp.event)) { + /* consume event via sapp_consume_event() */ + retval = true; + } if (send_keyup_followup) { _sapp.event.type = SAPP_EVENTTYPE_KEY_UP; - _sapp_call_event(&_sapp.event); + if (_sapp_call_event(&_sapp.event)) { + retval = true; + } } } } @@ -2567,9 +2738,16 @@ _SOKOL_PRIVATE void _sapp_emsc_init_keytable(void) { _sapp.keycodes[224] = SAPP_KEYCODE_LEFT_SUPER; } +_SOKOL_PRIVATE void _sapp_emsc_init_clipboard(void) { + sapp_js_init_clipboard(); +} + _SOKOL_PRIVATE void _sapp_run(const sapp_desc* desc) { _sapp_init_state(desc); _sapp_emsc_init_keytable(); + if (_sapp.clipboard_enabled) { + _sapp_emsc_init_clipboard(); + } double w, h; if (_sapp.desc.html5_canvas_resize) { w = (double) _sapp.desc.width; @@ -2636,6 +2814,8 @@ _SOKOL_PRIVATE void _sapp_run(const sapp_desc* desc) { emscripten_request_animation_frame_loop(_sapp_emsc_frame, 0); sapp_js_hook_beforeunload(); + + // NOT A BUG: do not call _sapp_discard_state() } #if !defined(SOKOL_NO_ENTRY) @@ -3894,6 +4074,12 @@ _SOKOL_PRIVATE bool _sapp_win32_utf8_to_wide(const char* src, wchar_t* dst, int } } +_SOKOL_PRIVATE bool _sapp_win32_wide_to_utf8(const wchar_t* src, char* dst, int dst_num_bytes) { + SOKOL_ASSERT(src && dst && (dst_num_bytes > 1)); + memset(dst, 0, dst_num_bytes); + return 0 != WideCharToMultiByte(CP_UTF8, 0, src, -1, dst, dst_num_bytes, NULL, NULL); +} + _SOKOL_PRIVATE void _sapp_win32_show_mouse(bool shown) { ShowCursor((BOOL)shown); } @@ -4101,6 +4287,15 @@ _SOKOL_PRIVATE void _sapp_win32_key_event(sapp_event_type type, int vk, bool rep _sapp.event.key_code = _sapp.keycodes[vk]; _sapp.event.key_repeat = repeat; _sapp_call_event(&_sapp.event); + /* check if a CLIPBOARD_PASTED event must be sent too */ + if (_sapp.clipboard_enabled && + (type == SAPP_EVENTTYPE_KEY_DOWN) && + (_sapp.event.modifiers = SAPP_MODIFIER_CTRL) && + (_sapp.event.key_code == SAPP_KEYCODE_V)) + { + _sapp_init_event(SAPP_EVENTTYPE_CLIPBOARD_PASTED); + _sapp_call_event(&_sapp.event); + } } } @@ -4352,6 +4547,69 @@ _SOKOL_PRIVATE void _sapp_win32_init_dpi(void) { } } +_SOKOL_PRIVATE bool _sapp_win32_set_clipboard_string(const char* str) { + SOKOL_ASSERT(str); + SOKOL_ASSERT(_sapp_win32_hwnd); + SOKOL_ASSERT(_sapp.clipboard_enabled && (_sapp.clipboard_size > 0)); + + wchar_t* wchar_buf = 0; + const int wchar_buf_size = _sapp.clipboard_size * sizeof(wchar_t); + HANDLE object = GlobalAlloc(GMEM_MOVEABLE, wchar_buf_size); + if (!object) { + goto error; + } + wchar_buf = (wchar_t*) GlobalLock(object); + if (!wchar_buf) { + goto error; + } + if (!_sapp_win32_utf8_to_wide(str, wchar_buf, wchar_buf_size)) { + goto error; + } + GlobalUnlock(wchar_buf); + wchar_buf = 0; + if (!OpenClipboard(_sapp_win32_hwnd)) { + goto error; + } + EmptyClipboard(); + SetClipboardData(CF_UNICODETEXT, object); + CloseClipboard(); + return true; + +error: + if (wchar_buf) { + GlobalUnlock(object); + } + if (object) { + GlobalFree(object); + } + return false; +} + +_SOKOL_PRIVATE const char* _sapp_win32_get_clipboard_string(void) { + SOKOL_ASSERT(_sapp.clipboard_enabled && _sapp.clipboard); + SOKOL_ASSERT(_sapp_win32_hwnd); + if (!OpenClipboard(_sapp_win32_hwnd)) { + /* silently ignore any errors and just return the current + content of the local clipboard buffer + */ + return _sapp.clipboard; + } + HANDLE object = GetClipboardData(CF_UNICODETEXT); + if (!object) { + CloseClipboard(); + return _sapp.clipboard; + } + const wchar_t* wchar_buf = (const wchar_t*) GlobalLock(object); + if (!wchar_buf) { + CloseClipboard(); + return _sapp.clipboard; + } + _sapp_win32_wide_to_utf8(wchar_buf, _sapp.clipboard, _sapp.clipboard_size); + GlobalUnlock(object); + CloseClipboard(); + return _sapp.clipboard; +} + _SOKOL_PRIVATE void _sapp_run(const sapp_desc* desc) { _sapp_init_state(desc); _sapp_win32_init_keytable(); @@ -4416,6 +4674,7 @@ _SOKOL_PRIVATE void _sapp_run(const sapp_desc* desc) { _sapp_wgl_shutdown(); #endif _sapp_win32_destroy_window(); + _sapp_discard_state(); } static char** _sapp_win32_command_line_to_utf8_argv(LPWSTR w_command_line, int* o_argc) { @@ -5191,6 +5450,8 @@ void ANativeActivity_onCreate(ANativeActivity* activity, void* saved_state, size activity->callbacks->onLowMemory = _sapp_android_on_low_memory; SOKOL_LOG("NativeActivity successfully created"); + + /* NOT A BUG: do NOT call sapp_discard_state() */ } #endif /* Android */ @@ -6673,6 +6934,15 @@ _SOKOL_PRIVATE void _sapp_x11_key_event(sapp_event_type type, sapp_keycode key, _sapp.event.key_repeat = repeat; _sapp.event.modifiers = mods; _sapp_call_event(&_sapp.event); + /* check if a CLIPBOARD_PASTED event must be sent too */ + if (_sapp.clipboard_enabled && + (type == SAPP_EVENTTYPE_KEY_DOWN) && + (_sapp.event.modifiers = SAPP_MODIFIER_CTRL) && + (_sapp.event.key_code == SAPP_KEYCODE_V)) + { + _sapp_init_event(SAPP_EVENTTYPE_CLIPBOARD_PASTED); + _sapp_call_event(&_sapp.event); + } } } @@ -7019,6 +7289,7 @@ _SOKOL_PRIVATE void _sapp_run(const sapp_desc* desc) { _sapp_glx_destroy_context(); _sapp_x11_destroy_window(); XCloseDisplay(_sapp_x11_display); + _sapp_discard_state(); } #if !defined(SOKOL_NO_ENTRY) @@ -7134,6 +7405,44 @@ SOKOL_API_IMPL void sapp_quit(void) { _sapp.quit_ordered = true; } +SOKOL_API_IMPL void sapp_consume_event(void) { + _sapp.event_consumed = true; +} + +/* NOTE: on HTML5, sapp_set_clipboard_string() must be called from within event handler! */ +SOKOL_API_IMPL void sapp_set_clipboard_string(const char* str) { + if (!_sapp.clipboard_enabled) { + return; + } + SOKOL_ASSERT(str); + #if defined(__APPLE__) && defined(TARGET_OS_IPHONE) && !TARGET_OS_IPHONE + _sapp_macos_set_clipboard_string(str); + #elif defined(__EMSCRIPTEN__) + _sapp_emsc_set_clipboard_string(str); + #elif defined(_WIN32) + _sapp_win32_set_clipboard_string(str); + #else + /* not implemented */ + #endif + _sapp_strcpy(str, _sapp.clipboard, _sapp.clipboard_size); +} + +SOKOL_API_IMPL const char* sapp_get_clipboard_string(void) { + if (!_sapp.clipboard_enabled) { + return ""; + } + #if defined(__APPLE__) && defined(TARGET_OS_IPHONE) && !TARGET_OS_IPHONE + return _sapp_macos_get_clipboard_string(); + #elif defined(__EMSCRIPTEN__) + return _sapp.clipboard; + #elif defined(_WIN32) + return _sapp_win32_get_clipboard_string(); + #else + /* not implemented */ + return _sapp.clipboard; + #endif +} + SOKOL_API_IMPL const void* sapp_metal_get_device(void) { SOKOL_ASSERT(_sapp.valid); #if defined(SOKOL_METAL) diff --git a/util/sokol_imgui.h b/util/sokol_imgui.h index 7e796172..d8b3d8c7 100644 --- a/util/sokol_imgui.h +++ b/util/sokol_imgui.h @@ -219,6 +219,7 @@ typedef struct simgui_desc_t { float dpi_scale; const char* ini_filename; bool no_default_font; + bool disable_hotkeys; /* don't let ImGui handle Ctrl-A,C,V,X,Y,Z */ } simgui_desc_t; SOKOL_API_DECL void simgui_setup(const simgui_desc_t* desc); @@ -251,6 +252,10 @@ SOKOL_API_DECL void simgui_shutdown(void); #include <stddef.h> /* offsetof */ #include <string.h> /* memset */ +#if !defined(SOKOL_IMGUI_NO_SOKOL_APP) && defined(__EMSCRIPTEN__) +#include <emscripten.h> +#endif + #ifndef SOKOL_API_IMPL #define SOKOL_API_IMPL #endif @@ -278,6 +283,8 @@ typedef struct { ImVec2 disp_size; } _simgui_vs_params_t; +#define SIMGUI_MAX_KEY_VALUE (512) // same as ImGuis IO.KeysDown array + typedef struct { simgui_desc_t desc; sg_buffer vbuf; @@ -288,6 +295,9 @@ typedef struct { #if !defined(SOKOL_IMGUI_NO_SOKOL_APP) bool btn_down[SAPP_MAX_MOUSEBUTTONS]; bool btn_up[SAPP_MAX_MOUSEBUTTONS]; + uint8_t keys_down[SIMGUI_MAX_KEY_VALUE]; // bits 0..3 or modifiers, != 0 is key-down + uint8_t keys_up[SIMGUI_MAX_KEY_VALUE]; // same is keys_down + bool is_osx; // return true if running on OSX (or HTML5 OSX), needed for copy/paste #endif } _simgui_state_t; static _simgui_state_t _simgui; @@ -701,12 +711,46 @@ static const uint8_t _simgui_fs_bin[] = { #error "sokol_imgui.h: No sokol_gfx.h backend selected (SOKOL_GLCORE33, SOKOL_GLES2, SOKOL_GLES3, SOKOL_D3D11 or SOKOL_METAL)" #endif +#if !defined(SOKOL_IMGUI_NO_SOKOL_APP) +static void _simgui_set_clipboard(void* user_data, const char* text) { + sapp_set_clipboard_string(text); +} + +static const char* _simgui_get_clipboard(void* user_data) { + return sapp_get_clipboard_string(); +} + +#if defined(__EMSCRIPTEN__) +EM_JS(int, simgui_js_is_osx, (void), { + if (navigator.userAgent.includes('Macintosh')) { + return 1; + } + else { + return 0; + } +}); +#endif + +static bool _simgui_is_osx(void) { + #if defined(__EMSCRIPTEN__) + return simgui_js_is_osx(); + #elif defined(__APPLE__) + return true; + #else + return false; + #endif +} +#endif + SOKOL_API_IMPL void simgui_setup(const simgui_desc_t* desc) { SOKOL_ASSERT(desc); memset(&_simgui, 0, sizeof(_simgui)); _simgui.desc = *desc; _simgui.desc.max_vertices = _simgui_def(_simgui.desc.max_vertices, 65536); _simgui.desc.dpi_scale = _simgui_def(_simgui.desc.dpi_scale, 1.0f); + #if !defined(SOKOL_IMGUI_NO_SOKOL_APP) + _simgui.is_osx = _simgui_is_osx(); + #endif /* can keep color_format, depth_format and sample_count as is, since sokol_gfx.h will do its own default-value handling */ @@ -728,6 +772,7 @@ SOKOL_API_IMPL void simgui_setup(const simgui_desc_t* desc) { } #endif io->IniFilename = _simgui.desc.ini_filename; + io->ConfigMacOSXBehaviors = _simgui_is_osx(); io->BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; #if !defined(SOKOL_IMGUI_NO_SOKOL_APP) io->KeyMap[ImGuiKey_Tab] = SAPP_KEYCODE_TAB; @@ -744,12 +789,18 @@ SOKOL_API_IMPL void simgui_setup(const simgui_desc_t* desc) { io->KeyMap[ImGuiKey_Space] = SAPP_KEYCODE_SPACE; io->KeyMap[ImGuiKey_Enter] = SAPP_KEYCODE_ENTER; io->KeyMap[ImGuiKey_Escape] = SAPP_KEYCODE_ESCAPE; - io->KeyMap[ImGuiKey_A] = SAPP_KEYCODE_A; - io->KeyMap[ImGuiKey_C] = SAPP_KEYCODE_C; - io->KeyMap[ImGuiKey_V] = SAPP_KEYCODE_V; - io->KeyMap[ImGuiKey_X] = SAPP_KEYCODE_X; - io->KeyMap[ImGuiKey_Y] = SAPP_KEYCODE_Y; - io->KeyMap[ImGuiKey_Z] = SAPP_KEYCODE_Z; + if (!_simgui.desc.disable_hotkeys) { + io->KeyMap[ImGuiKey_A] = SAPP_KEYCODE_A; + io->KeyMap[ImGuiKey_C] = SAPP_KEYCODE_C; + io->KeyMap[ImGuiKey_V] = SAPP_KEYCODE_V; + io->KeyMap[ImGuiKey_X] = SAPP_KEYCODE_X; + io->KeyMap[ImGuiKey_Y] = SAPP_KEYCODE_Y; + io->KeyMap[ImGuiKey_Z] = SAPP_KEYCODE_Z; + } + #if !defined(SOKOL_IMGUI_NO_SOKOL_APP) + io->SetClipboardTextFn = _simgui_set_clipboard; + io->GetClipboardTextFn = _simgui_get_clipboard; + #endif #endif /* create sokol-gfx resources */ @@ -872,6 +923,13 @@ SOKOL_API_IMPL void simgui_shutdown(void) { sg_destroy_buffer(_simgui.vbuf); } +_SOKOL_PRIVATE void _simgui_set_imgui_modifiers(ImGuiIO* io, uint8_t mods) { + io->KeyAlt = (mods & SAPP_MODIFIER_ALT) != 0; + io->KeyCtrl = (mods & SAPP_MODIFIER_CTRL) != 0; + io->KeyShift = (mods & SAPP_MODIFIER_SHIFT) != 0; + io->KeySuper = (mods & SAPP_MODIFIER_SUPER) != 0; +} + SOKOL_API_IMPL void simgui_new_frame(int width, int height, double delta_time) { #if defined(__cplusplus) ImGuiIO* io = &ImGui::GetIO(); @@ -892,6 +950,18 @@ SOKOL_API_IMPL void simgui_new_frame(int width, int height, double delta_time) { io->MouseDown[i] = false; } } + for (int i = 0; i < SIMGUI_MAX_KEY_VALUE; i++) { + if (_simgui.keys_down[i]) { + io->KeysDown[i] = true; + _simgui_set_imgui_modifiers(io, _simgui.keys_down[i]); + _simgui.keys_down[i] = 0; + } + else if (_simgui.keys_up[i]) { + io->KeysDown[i] = false; + _simgui_set_imgui_modifiers(io, _simgui.keys_up[i]); + _simgui.keys_up[i] = 0; + } + } if (io->WantTextInput && !sapp_keyboard_shown()) { sapp_show_keyboard(true); } @@ -1009,6 +1079,15 @@ SOKOL_API_IMPL void simgui_render(void) { } #if !defined(SOKOL_IMGUI_NO_SOKOL_APP) +_SOKOL_PRIVATE bool _simgui_is_ctrl(uint8_t modifiers) { + if (_simgui.is_osx) { + return 0 != (modifiers & SAPP_MODIFIER_SUPER); + } + else { + return 0 != (modifiers & SAPP_MODIFIER_CTRL); + } +} + SOKOL_API_IMPL bool simgui_handle_event(const sapp_event* ev) { const float dpi_scale = _simgui.desc.dpi_scale; #if defined(__cplusplus) @@ -1016,10 +1095,7 @@ SOKOL_API_IMPL bool simgui_handle_event(const sapp_event* ev) { #else ImGuiIO* io = igGetIO(); #endif - io->KeyAlt = (ev->modifiers & SAPP_MODIFIER_ALT) != 0; - io->KeyCtrl = (ev->modifiers & SAPP_MODIFIER_CTRL) != 0; - io->KeyShift = (ev->modifiers & SAPP_MODIFIER_SHIFT) != 0; - io->KeySuper = (ev->modifiers & SAPP_MODIFIER_SUPER) != 0; + _simgui_set_imgui_modifiers(io, ev->modifiers); switch (ev->type) { case SAPP_EVENTTYPE_MOUSE_DOWN: io->MousePos.x = ev->mouse_x / dpi_scale; @@ -1069,17 +1145,43 @@ SOKOL_API_IMPL bool simgui_handle_event(const sapp_event* ev) { _simgui.btn_up[0] = _simgui.btn_down[0] = false; break; case SAPP_EVENTTYPE_KEY_DOWN: - io->KeysDown[ev->key_code] = true; + /* intercept Ctrl-V, this is handled via EVENTTYPE_CLIPBOARD_PASTED */ + if (_simgui_is_ctrl(ev->modifiers) && (ev->key_code == SAPP_KEYCODE_V)) { + break; + } + /* on web platform, don't forward Ctrl-X, Ctrl-V to the browser */ + if (_simgui_is_ctrl(ev->modifiers) && (ev->key_code == SAPP_KEYCODE_X)) { + sapp_consume_event(); + } + if (_simgui_is_ctrl(ev->modifiers) && (ev->key_code == SAPP_KEYCODE_C)) { + sapp_consume_event(); + } + _simgui.keys_down[ev->key_code] = 0x80 | ev->modifiers; break; case SAPP_EVENTTYPE_KEY_UP: - io->KeysDown[ev->key_code] = false; + /* intercept Ctrl-V, this is handled via EVENTTYPE_CLIPBOARD_PASTED */ + if (_simgui_is_ctrl(ev->modifiers) && (ev->key_code == SAPP_KEYCODE_V)) { + break; + } + /* on web platform, don't forward Ctrl-X, Ctrl-V to the browser */ + if (_simgui_is_ctrl(ev->modifiers) && (ev->key_code == SAPP_KEYCODE_X)) { + sapp_consume_event(); + } + if (_simgui_is_ctrl(ev->modifiers) && (ev->key_code == SAPP_KEYCODE_C)) { + sapp_consume_event(); + } + _simgui.keys_up[ev->key_code] = 0x80 | ev->modifiers; break; case SAPP_EVENTTYPE_CHAR: /* on some platforms, special keys may be reported as characters, which may confuse some ImGui widgets, - drop those + drop those, also don't forward characters if some + modifiers have been pressed */ - if ((ev->char_code >= 32) && (ev->char_code != 127)) { + if ((ev->char_code >= 32) && + (ev->char_code != 127) && + (0 == (ev->modifiers & (SAPP_MODIFIER_ALT|SAPP_MODIFIER_CTRL|SAPP_MODIFIER_SUPER)))) + { #if defined(__cplusplus) io->AddInputCharacter((ImWchar)ev->char_code); #else @@ -1087,6 +1189,11 @@ SOKOL_API_IMPL bool simgui_handle_event(const sapp_event* ev) { #endif } break; + case SAPP_EVENTTYPE_CLIPBOARD_PASTED: + /* simulate a Ctrl-V key down/up */ + _simgui.keys_down[SAPP_KEYCODE_V] = _simgui.keys_up[SAPP_KEYCODE_V] = + 0x80 | (_simgui.is_osx ? SAPP_MODIFIER_SUPER:SAPP_MODIFIER_CTRL); + break; default: break; } |