diff --git a/CMakeLists.txt b/CMakeLists.txt index 063533c4ed..450ed036f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,8 @@ option(GLFW_BUILD_EXAMPLES "Build the GLFW example programs" ${GLFW_STANDALONE}) option(GLFW_BUILD_TESTS "Build the GLFW test programs" ${GLFW_STANDALONE}) option(GLFW_BUILD_DOCS "Build the GLFW documentation" ON) option(GLFW_INSTALL "Generate installation target" ON) +option(GLFW_IME_MODE_AS_TEXT_INPUT_FOCUS + "Map GLFW_IME input mode to text input focus by default" OFF) include(GNUInstallDirs) include(CMakeDependentOption) @@ -28,6 +30,9 @@ cmake_dependent_option(GLFW_BUILD_WIN32 "Build support for Win32" ON "WIN32" OFF cmake_dependent_option(GLFW_BUILD_COCOA "Build support for Cocoa" ON "APPLE" OFF) cmake_dependent_option(GLFW_BUILD_X11 "Build support for X11" ON "UNIX;NOT APPLE" OFF) cmake_dependent_option(GLFW_BUILD_WAYLAND "Build support for Wayland" ON "UNIX;NOT APPLE" OFF) +cmake_dependent_option(GLFW_EMBED_IBUS_MODULE + "Embed the experimental X11 IBus IME module into GLFW" + OFF "UNIX;NOT APPLE;GLFW_BUILD_X11" OFF) cmake_dependent_option(GLFW_USE_HYBRID_HPG "Force use of high-performance GPU on hybrid systems" OFF "WIN32" OFF) @@ -133,4 +138,3 @@ if (GLFW_INSTALL) set_target_properties(uninstall PROPERTIES FOLDER "GLFW3") endif() endif() - diff --git a/docs/ime-ibus-prototype.md b/docs/ime-ibus-prototype.md new file mode 100644 index 0000000000..331f5bf13b --- /dev/null +++ b/docs/ime-ibus-prototype.md @@ -0,0 +1,363 @@ +# X11 IBus IME Prototype + +## Purpose + +This document describes an experimental X11 IME backend for GLFW based on IBus. +The goal of the prototype is to determine whether IBus, and Fcitx5 through its +IBus compatibility layer, can be used from GLFW without changing the GLFW event +loop architecture. + +This is a research prototype. It is not intended to be production quality or +ready for upstream submission. + +The prototype is meant to answer these questions: + +- Can an IME backend be loaded dynamically instead of linked into GLFW? +- Can all D-Bus communication happen outside the GLFW event loop? +- Can X11 key handling synchronously ask the IME whether a key was handled? +- Do IBus replies or text signals arrive late enough to cause duplicated text? +- Is candidate window positioning practical with the existing cursor rectangle + API? +- Can the existing GLFW preedit model represent useful IBus preedit state, + including caret position and focused text blocks? + +## Architecture + +The prototype adds a private X11-only IME module ABI. It is not part of the +public GLFW API. + +At runtime, GLFW checks `GLFW_IM_MODULE`. If it is set, GLFW loads the named +shared object with `dlopen()` and looks up the `glfwGetX11IMEBackend` symbol with +`dlsym()`. + +For example: + +```sh +GLFW_IM_MODULE=/path/to/glfw-ibus.so ./application +``` + +For experimentation with prebuilt GLFW binaries, the same IBus backend can also +be embedded into GLFW with the `GLFW_EMBED_IBUS_MODULE` CMake option. In that +mode, GLFW uses the embedded backend when `GLFW_IM_MODULE` is not set, while +still allowing `GLFW_IM_MODULE` to override it. + +### GLFW Core + +GLFW core remains unaware of IBus, Fcitx5 and D-Bus. The X11 backend only knows +about the private plugin ABI in `src/x11_ime_module.h`. + +When a module is active, the X11 backend: + +- skips XIM setup for that run +- forwards X11 key events to the module +- forwards focus changes to the module +- translates preedit cursor rectangles from client-area coordinates to X11 root + coordinates +- drains queued module events on the main thread + +The public IME API semantics are unchanged. Applications still use +`glfwSetPreeditCursorRectangle` with GLFW window/client-area coordinates. + +### Plugin ABI + +The plugin ABI consists of: + +- a host callback table provided by GLFW +- a backend function table provided by the module +- opaque window tokens passed back to GLFW by the module + +The module must not dereference GLFW window pointers. They are only handles for +callbacks into GLFW. + +The module can request that `glfwWaitEvents` wake up by calling the host +`post_empty_event` callback. This uses GLFW's existing X11 empty-event pipe; it +does not add D-Bus file descriptors to the GLFW event loop. + +### Dynamically Loaded Module + +The prototype module is built as `glfw-ibus.so` when the `dbus-1` development +package is available. + +If `GLFW_EMBED_IBUS_MODULE` is enabled, the same module source is also compiled +into the GLFW library and `dbus-1` becomes a GLFW build dependency. This is +only an experiment to remove setup friction for local or redistributed test +builds. + +The module owns: + +- IBus address discovery +- libdbus connection setup +- IBus input context creation +- D-Bus method calls +- D-Bus signal handling +- worker thread lifetime +- request and event queues +- timing instrumentation + +### Worker Thread + +The module creates a worker thread. The worker thread owns all D-Bus traffic. + +GLFW's X11 thread sends commands to the worker through an explicit queue. The +worker sends IME events back through a second queue. The worker never calls GLFW +IME callbacks directly. + +Queued events are drained from GLFW's normal X11 event functions on the main +thread. This preserves the existing `glfwPollEvents`, `glfwWaitEvents` and +`glfwWaitEventsTimeout` architecture. + +### D-Bus Communication + +The worker uses libdbus directly. The prototype intentionally does not integrate +libdbus watches or timeouts with GLFW. + +`ProcessKeyEvent` is currently sent with a blocking D-Bus call on the worker +thread. The GLFW/X11 thread waits for the worker to report the result, with a +prototype timeout controlled by `GLFW_IBUS_TIMEOUT_MS`. + +The default timeout is 100 ms. + +### IBus Integration + +The module uses these IBus input context methods and signals: + +- `CreateInputContext` +- `SetCapabilities` +- `ProcessKeyEvent` +- `CommitText` +- `UpdatePreeditText` +- `HidePreeditText` +- `FocusIn` +- `FocusOut` +- `SetCursorLocation` +- `Reset` +- `Enabled` +- `Disabled` + +The module currently uses `SetCursorLocation`, not +`SetCursorLocationRelative`. + +For X11 candidate positioning, GLFW translates the application-provided cursor +rectangle from client-area coordinates to root-window coordinates with +`XTranslateCoordinates()` before sending it to the module. + +## Relationship to PR #2130 + +This prototype builds on the IME architecture introduced by PR #2130. + +It reuses: + +- the public `GLFW_IME` input mode +- preedit callbacks +- IME status callbacks +- preedit cursor rectangle APIs +- shared preedit state in `_GLFWpreedit` +- X11 platform IME hooks for focus, key handling, cursor rectangle updates and + reset + +It does not redesign the application-facing IME API. + +The prototype adds an alternative X11 IME backend path behind a dynamically +loaded module. When no module is loaded, the existing XIM behavior remains the +fallback. + +## Why A Plugin Architecture Was Chosen + +IBus support brings Linux/X11-specific complexity into an otherwise portable +library. A plugin boundary keeps that complexity separate from GLFW core. + +The plugin design was chosen to: + +- avoid a hard D-Bus dependency in GLFW core +- avoid libdbus watch and timeout integration in the GLFW event loop +- avoid changes to `glfwPollEvents`, `glfwWaitEvents` and + `glfwWaitEventsTimeout` +- isolate IBus/Fcitx5 behavior and failure modes +- make the experiment opt-in with `GLFW_IM_MODULE` +- allow the prototype to be removed or replaced without affecting the public API + +This matches the goal of evaluating feasibility without committing GLFW to a +production IBus backend design. + +## Current Status + +### Preedit Support + +Preedit updates from IBus are received through `UpdatePreeditText`, queued by the +worker and drained on the GLFW main thread. + +The prototype maps IBus preedit text, caret position and attribute ranges to +GLFW preedit text, block sizes, focused block and caret index. The block +mapping is intentionally conservative because IBus engines differ in which +attributes they use to mark the active segment. + +This has been tested with normal IBus preedit flow and now behaves well enough +for practical application-side preedit drawing in the prototype. It is still a +mapping from IBus attributes to GLFW's simpler block model, not a lossless +exposure of every IBus text attribute. + +### Commit Support + +Committed text from `CommitText` is queued by the worker and emitted through +GLFW's normal character input path on the main thread. + +### Candidate Window Positioning + +Candidate window positioning is supported through `SetCursorLocation`. + +Applications still provide cursor rectangles in GLFW window/client-area +coordinates. The X11 plugin bridge translates those coordinates to root-window +coordinates before sending them to IBus. + +The bridge tracks whether a valid cursor rectangle has been translated. It does +not send `SetCursorLocation` until a valid rectangle exists. It resends the last +valid rectangle on focus and before key processing. + +### Text Input Focus + +`glfwSetTextInputFocus(window, GLFW_TRUE)` maps to IBus `FocusIn` when the X11 +window is focused. If the X11 window is not focused, the request is reflected +by GLFW state and the next X11 `FocusIn` event activates the module. + +`glfwSetTextInputFocus(window, GLFW_FALSE)` maps to IBus `Reset` followed by +`FocusOut`. This disables text input routing for the window while keeping the +window focus state separate from the text input focus abstraction. + +The X11 key path also checks GLFW's text input focus state before sending +`ProcessKeyEvent` to the module. This is required because IBus `FocusOut` is +asynchronous and does not by itself prevent GLFW from continuing to route key +events through IBus. + +For application compatibility experiments, `GLFW_IME` can be remapped to this +text input focus model. Set `GLFW_IME_MODE_AS_TEXT_INPUT_FOCUS` to a non-zero +value, or build GLFW with `GLFW_IME_MODE_AS_TEXT_INPUT_FOCUS` enabled. In this +mode, `glfwSetInputMode(window, GLFW_IME, value)` calls the text input focus path +instead of the native platform IME-status path. + +`glfwGetInputMode(window, GLFW_IME)` still returns the native platform IME +status until text input focus has been explicitly set for the window. After +that, it returns the explicit text input focus state. This preserves the old +query behavior for applications that never use the remapping path. + +### IME Enable And Disable Behavior + +The prototype has minimal IME status support. + +IBus `Enabled` and `Disabled` signals update module state and trigger GLFW IME +status callbacks. `glfwSetInputMode(window, GLFW_IME, value)` maps to +`FocusIn` or `FocusOut` in the prototype. + +This does not have the same semantics as Win32 `ImmGetOpenStatus` and +`ImmSetOpenStatus`. It is sufficient for experimentation, but not a final API +mapping. + +## Instrumentation + +The prototype intentionally keeps timing and late-event instrumentation. It is +emitted only when `GLFW_IME_DEBUG` is set to a non-zero value. + +Each `ProcessKeyEvent` request logs: + +- request id +- X11 key serial +- timestamp +- keyval +- keycode +- IBus state + +Each reply logs: + +- request id +- latency +- handled status +- timeout status +- failure status + +Each timeout logs: + +- request id +- elapsed time +- X11 key serial + +Each queued and drained IME event logs: + +- event type +- attributed request id +- whether the attributed request had timed out +- timestamp +- caret index, block count and focused block for preedit events +- text, when present + +IBus signals do not include the originating `ProcessKeyEvent` request. The +module attributes signals to the active request when possible, otherwise to the +most recent request. This attribution is for observation only. + +## Known Risks + +### Timeout Semantics + +The GLFW/X11 thread may stop waiting before the worker receives a +`ProcessKeyEvent` reply. The key is then treated as not handled and GLFW falls +back to the normal X11 key path. + +IBus may still later process the key. + +### Possible Duplicated Text After Timeout + +If GLFW falls back to normal text input after a timeout and IBus later emits +`CommitText` for the same key, the application may receive duplicated text. + +The prototype is instrumented to observe this. It does not fully solve it. + +### Late ProcessKeyEvent Replies + +Late replies are logged with their original request id and timeout state. + +The prototype keeps a small list of recent requests so late replies and related +signals can be identified. + +### Late CommitText Events + +Late `CommitText` events are queued and logged with the best available request +attribution. Because IBus does not identify the originating request, this +attribution is not guaranteed to be exact. + +### Candidate, Attribute And Surrounding Text Completeness + +The prototype does not implement lookup-table/candidate-list parsing or +surrounding text. + +IBus preedit attributes are mapped to GLFW block sizes and a focused block, but +the mapping is intentionally lossy. It is enough for useful preedit display, +but it does not expose underline style, foreground/background color or every +IBus text attribute to applications. + +### Restart And Recovery + +The prototype does not attempt production-grade daemon restart handling, +reconnection or failure recovery. + +## Current Recommendation + +The prototype demonstrates that the architecture is technically feasible: + +- an IME backend can be dynamically loaded +- D-Bus communication can be isolated from the GLFW event loop +- worker-thread communication can be kept behind explicit queues +- IBus/Fcitx5 preedit and commit paths can be integrated with the existing IME + architecture +- IBus preedit text, caret position and focused block information can be mapped + to GLFW's existing preedit callback model +- candidate window positioning can be handled without changing the public IME API +- explicit text input focus can be made to stop routing X11 keys through IBus + +The current prototype is no longer only a proof of concept for drawing basic +preedit text. It is close to usable for application testing on X11 with IBus or +Fcitx5's IBus compatibility layer. + +However, this is still an experimental backend. It is not currently recommended +for upstream submission as-is. + +The remaining decision point is semantic reliability: late replies and late text +signals after key-processing timeouts need more real-world measurement before an +upstream-quality design can be justified. diff --git a/docs/input.md b/docs/input.md index a502ffa9f6..1880f3f8f4 100644 --- a/docs/input.md +++ b/docs/input.md @@ -276,6 +276,47 @@ In this case, the preedit callback also works on X11. However, on-the-spot styl X11 is unstable, so it is not recommended. +@subsection input_text_focus Text input focus + +Text input focus describes whether the application is currently in a text input +context. Examples include a chat box, text field, search box, rename dialog or +editor. This is distinct from native window focus. + +This is useful for applications that render their own user interface inside a +single native window, such as games and browsers. In these applications, the +native window may remain focused while the application switches between +gameplay, menus, chat input, search fields, editors and other UI elements. The +application is the only component that reliably knows when text input is +expected. + +Use @ref glfwSetTextInputFocus to tell GLFW when the application enters or +leaves a text input context: + +@code +glfwSetTextInputFocus(window, GLFW_TRUE); // Text field became active +glfwSetTextInputFocus(window, GLFW_FALSE); // Text field lost focus +@endcode + +This function does not turn the IME on or off. It expresses whether GLFW should +route text input through the platform text input or IME path for this window. +It does not request an input language change, force a specific IME state or +force a specific input source. + +For compatibility, GLFW preserves the previous platform text input behavior for +applications that never call @ref glfwSetTextInputFocus. Once an application +calls it for a window, that window enters explicit text input focus management. +The application is then responsible for calling it with `GLFW_TRUE` when text +input begins and with `GLFW_FALSE` when text input ends. + +Applications that opt into explicit text input focus management should set the +initial state explicitly, usually to `GLFW_FALSE`, after window creation. They +should then set it to `GLFW_TRUE` only while a text input widget is active. + +Individual platforms map this abstraction to their native text input +mechanisms. Some platforms may also cancel or clear active preedit text when +text input focus is set to `GLFW_FALSE`. + + @subsection input_preedit Preedit input When inputting text with IME, the text is temporarily inputted, then conversion @@ -407,6 +448,16 @@ glfwSetInputMode(window, GLFW_IME, GLFW_TRUE); glfwSetInputMode(window, GLFW_IME, GLFW_FALSE); @endcode +This is related to but distinct from @ref glfwSetTextInputFocus. Text input +focus describes application intent: the application has entered or left a text +input context. `GLFW_IME` controls platform-specific IME state. Applications +should normally prefer @ref glfwSetTextInputFocus unless they specifically need +platform-dependent IME state control. + +As a rule of thumb, if you think you need to enable or disable IME because a +chat box, text field, search field, rename dialog or editor gained or lost +focus, you probably want text input focus instead. + You can use the following function to clear the current preedit. @code @@ -1239,4 +1290,3 @@ void drop_callback(GLFWwindow* window, int count, const char** paths) The path array and its strings are only valid until the file drop callback returns, as they may have been generated specifically for that event. You need to make a deep copy of the array if you want to keep the paths. - diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h index cb75ad99cd..ed36feb93b 100644 --- a/include/GLFW/glfw3.h +++ b/include/GLFW/glfw3.h @@ -5310,6 +5310,56 @@ GLFWAPI void glfwSetPreeditCursorRectangle(GLFWwindow* window, int x, int y, int */ GLFWAPI void glfwResetPreeditText(GLFWwindow* window); +/*! @brief Sets whether the application has text input focus. + * + * This function informs GLFW that the specified window has entered or left + * a text input context, such as a chat box, text field, search box, rename + * dialog or editor. Text input focus is separate from native window focus. + * A window may remain focused while the application switches between + * gameplay, menus, chat input, search fields, editors and other UI elements. + * The application is the only component that reliably knows when text input + * is expected. + * + * Pass `GLFW_TRUE` when the application enters a text input context and + * `GLFW_FALSE` when it leaves that context. + * + * This function does not turn the IME on or off, switch input languages or + * force a specific platform input source. It expresses whether GLFW should + * route key and text input through the platform text input or IME path for + * this window. Individual platforms may map this abstraction differently. + * + * For compatibility, applications that never call this function keep the same + * platform text input behavior as before this API was introduced. Once this + * function is called for a window, that window enters explicit text input + * focus management and the application is responsible for notifying GLFW when + * text input begins and ends. + * Applications that opt into explicit text input focus management should set + * the initial state explicitly after window creation. + * + * This function is related to but distinct from + * `glfwSetInputMode(window, GLFW_IME, value)`. Text input focus describes + * application intent, while `GLFW_IME` controls platform-specific IME state. + * Applications should normally prefer this function unless they specifically + * need platform-dependent IME state control. + * + * @param[in] window The window whose text input focus state to set. + * @param[in] focused `GLFW_TRUE` to enter text input focus, or `GLFW_FALSE` + * to leave it. + * + * @errors Possible errors include @ref GLFW_NOT_INITIALIZED and @ref + * GLFW_PLATFORM_ERROR. + * + * @thread_safety This function must only be called from the main thread. + * + * @sa @ref ime_support + * @sa @ref glfwSetInputMode + * + * @since Added in GLFW 3.X. + * + * @ingroup input + */ +GLFWAPI void glfwSetTextInputFocus(GLFWwindow* window, int focused); + /*! @brief Returns the preedit candidate. * * This function returns the text and the text-count of the preedit candidate. @@ -6862,4 +6912,3 @@ GLFWAPI VkResult glfwCreateWindowSurface(VkInstance instance, GLFWwindow* window #endif #endif /* _glfw3_h_ */ - diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bcd3871d6d..8feea278e1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -46,7 +46,8 @@ endif() if (GLFW_BUILD_X11) target_compile_definitions(glfw PRIVATE _GLFW_X11) - target_sources(glfw PRIVATE x11_platform.h x11_init.c + target_sources(glfw PRIVATE x11_platform.h x11_ime_module.h + x11_ime_module.c x11_init.c x11_monitor.c x11_window.c xkb_unicode.c glx_context.c) endif() @@ -140,6 +141,10 @@ target_include_directories(glfw PRIVATE "${GLFW_BINARY_DIR}/src") target_link_libraries(glfw PRIVATE Threads::Threads) +if (GLFW_IME_MODE_AS_TEXT_INPUT_FOCUS) + target_compile_definitions(glfw PRIVATE _GLFW_IME_MODE_AS_TEXT_INPUT_FOCUS) +endif() + if (GLFW_BUILD_WIN32) list(APPEND glfw_PKG_LIBS "-lgdi32") endif() @@ -213,6 +218,40 @@ if (GLFW_BUILD_X11) message(FATAL_ERROR "X Shape headers not found; install libxext development package") endif() target_include_directories(glfw PRIVATE "${X11_Xshape_INCLUDE_PATH}") + + if (CMAKE_SYSTEM_NAME STREQUAL "Linux") + include(FindPkgConfig) + pkg_check_modules(DBUS1 dbus-1) + if (GLFW_EMBED_IBUS_MODULE AND NOT DBUS1_FOUND) + message(FATAL_ERROR "D-Bus development package is required for embedded glfw-ibus") + endif() + if (DBUS1_FOUND) + if (GLFW_EMBED_IBUS_MODULE) + target_sources(glfw PRIVATE x11_ibus_module.c) + target_compile_definitions(glfw PRIVATE _GLFW_EMBED_IBUS_MODULE) + target_include_directories(glfw PRIVATE ${DBUS1_INCLUDE_DIRS}) + target_link_libraries(glfw PRIVATE ${DBUS1_LIBRARIES}) + target_compile_options(glfw PRIVATE ${DBUS1_CFLAGS_OTHER}) + target_link_directories(glfw PRIVATE ${DBUS1_LIBRARY_DIRS}) + endif() + + add_library(glfw-ibus MODULE x11_ibus_module.c x11_ime_module.h) + set_target_properties(glfw-ibus PROPERTIES + PREFIX "" + C_STANDARD 99 + C_EXTENSIONS OFF + FOLDER "GLFW3") + target_include_directories(glfw-ibus PRIVATE + "${GLFW_SOURCE_DIR}/src" + "${GLFW_SOURCE_DIR}/include" + ${DBUS1_INCLUDE_DIRS}) + target_link_libraries(glfw-ibus PRIVATE Threads::Threads ${DBUS1_LIBRARIES}) + target_compile_options(glfw-ibus PRIVATE ${DBUS1_CFLAGS_OTHER}) + target_link_directories(glfw-ibus PRIVATE ${DBUS1_LIBRARY_DIRS}) + else() + message(STATUS "D-Bus development package not found; skipping glfw-ibus prototype module") + endif() + endif() endif() if (UNIX AND NOT APPLE) @@ -345,4 +384,3 @@ if (GLFW_INSTALL) ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}") endif() - diff --git a/src/cocoa_init.m b/src/cocoa_init.m index c982027047..0674ebb401 100644 --- a/src/cocoa_init.m +++ b/src/cocoa_init.m @@ -536,6 +536,7 @@ GLFWbool _glfwConnectCocoa(int platformID, _GLFWplatform* platform) .getClipboardString = _glfwGetClipboardStringCocoa, .updatePreeditCursorRectangle = _glfwUpdatePreeditCursorRectangleCocoa, .resetPreeditText = _glfwResetPreeditTextCocoa, + .setTextInputFocus = _glfwSetTextInputFocusCocoa, .setIMEStatus = _glfwSetIMEStatusCocoa, .getIMEStatus = _glfwGetIMEStatusCocoa, .initJoysticks = _glfwInitJoysticksCocoa, @@ -723,4 +724,3 @@ void _glfwTerminateCocoa(void) } #endif // _GLFW_COCOA - diff --git a/src/cocoa_platform.h b/src/cocoa_platform.h index fff4178b15..44a620bd72 100644 --- a/src/cocoa_platform.h +++ b/src/cocoa_platform.h @@ -300,6 +300,7 @@ const char* _glfwGetClipboardStringCocoa(void); void _glfwUpdatePreeditCursorRectangleCocoa(_GLFWwindow* window); void _glfwResetPreeditTextCocoa(_GLFWwindow* window); +void _glfwSetTextInputFocusCocoa(_GLFWwindow* window, GLFWbool focused); void _glfwSetIMEStatusCocoa(_GLFWwindow* window, int active); int _glfwGetIMEStatusCocoa(_GLFWwindow* window); @@ -332,4 +333,3 @@ GLFWbool _glfwCreateContextNSGL(_GLFWwindow* window, const _GLFWctxconfig* ctxconfig, const _GLFWfbconfig* fbconfig); void _glfwDestroyContextNSGL(_GLFWwindow* window); - diff --git a/src/cocoa_window.m b/src/cocoa_window.m index d894b4a4e9..1caa41bfb4 100644 --- a/src/cocoa_window.m +++ b/src/cocoa_window.m @@ -573,7 +573,14 @@ - (void)keyDown:(NSEvent *)event if (![self hasMarkedText]) _glfwInputKey(window, key, [event keyCode], GLFW_PRESS, mods); - [self interpretKeyEvents:@[event]]; + if (!window->textInputFocusExplicit || window->textInputFocus) + [self interpretKeyEvents:@[event]]; + else + { + NSString* characters = [event characters]; + if (characters) + [self insertText:characters replacementRange:[self selectedRange]]; + } } - (void)flagsChanged:(NSEvent *)event @@ -2037,6 +2044,12 @@ void _glfwResetPreeditTextCocoa(_GLFWwindow* window) } // autoreleasepool } +void _glfwSetTextInputFocusCocoa(_GLFWwindow* window, GLFWbool focused) +{ + if (!focused) + _glfwResetPreeditTextCocoa(window); +} + void _glfwSetIMEStatusCocoa(_GLFWwindow* window, int active) { @autoreleasepool { @@ -2311,4 +2324,3 @@ GLFWAPI id glfwGetCocoaView(GLFWwindow* handle) } #endif // _GLFW_COCOA - diff --git a/src/input.c b/src/input.c index 4507f22e97..cac4590902 100644 --- a/src/input.c +++ b/src/input.c @@ -593,6 +593,20 @@ void _glfwCenterCursorInContentArea(_GLFWwindow* window) _glfw.platform.setCursorPos(window, width / 2.0, height / 2.0); } +static GLFWbool imeModeControlsTextInputFocus(void) +{ + const char* value = getenv("GLFW_IME_MODE_AS_TEXT_INPUT_FOCUS"); + + if (value && *value) + return strcmp(value, "0") != 0; + +#if defined(_GLFW_IME_MODE_AS_TEXT_INPUT_FOCUS) + return GLFW_TRUE; +#else + return GLFW_FALSE; +#endif +} + ////////////////////////////////////////////////////////////////////////// ////// GLFW public API ////// @@ -620,6 +634,12 @@ GLFWAPI int glfwGetInputMode(GLFWwindow* handle, int mode) case GLFW_UNLIMITED_MOUSE_BUTTONS: return window->disableMouseButtonLimit; case GLFW_IME: + if (imeModeControlsTextInputFocus() && + window->textInputFocusExplicit) + { + return window->textInputFocus; + } + return _glfw.platform.getIMEStatus(window); } @@ -737,6 +757,15 @@ GLFWAPI void glfwSetInputMode(GLFWwindow* handle, int mode, int value) case GLFW_IME: { + if (imeModeControlsTextInputFocus()) + { + value = value ? GLFW_TRUE : GLFW_FALSE; + window->textInputFocusExplicit = GLFW_TRUE; + window->textInputFocus = value; + _glfw.platform.setTextInputFocus(window, value); + return; + } + _glfw.platform.setIMEStatus(window, value ? GLFW_TRUE : GLFW_FALSE); return; } @@ -1040,6 +1069,19 @@ GLFWAPI void glfwResetPreeditText(GLFWwindow* handle) _glfw.platform.resetPreeditText(window); } +GLFWAPI void glfwSetTextInputFocus(GLFWwindow* handle, int focused) +{ + _GLFW_REQUIRE_INIT(); + + _GLFWwindow* window = (_GLFWwindow*) handle; + assert(window != NULL); + + focused = focused ? GLFW_TRUE : GLFW_FALSE; + window->textInputFocusExplicit = GLFW_TRUE; + window->textInputFocus = focused; + _glfw.platform.setTextInputFocus(window, focused); +} + GLFWAPI unsigned int* glfwGetPreeditCandidate(GLFWwindow* handle, int index, int* textCount) { _GLFWwindow* window = (_GLFWwindow*) handle; @@ -1647,4 +1689,3 @@ GLFWAPI uint64_t glfwGetTimerFrequency(void) _GLFW_REQUIRE_INIT_OR_RETURN(0); return _glfwPlatformGetTimerFrequency(); } - diff --git a/src/internal.h b/src/internal.h index ca5c280b99..378fccb266 100644 --- a/src/internal.h +++ b/src/internal.h @@ -592,6 +592,18 @@ struct _GLFWwindow GLFWbool stickyMouseButtons; GLFWbool lockKeyMods; GLFWbool disableMouseButtonLimit; + + // Requested application text input focus state. This is meaningful only + // after textInputFocusExplicit has been set by glfwSetTextInputFocus. + // It describes whether an application-drawn text input context, such as a + // chat box or text field, currently has focus inside this native window. + GLFWbool textInputFocus; + // Whether the application has opted into explicit text input focus + // management for this window. This exists so that the zero-initialized + // textInputFocus value does not disable or bypass existing platform text + // input behavior for applications that never call glfwSetTextInputFocus. + GLFWbool textInputFocusExplicit; + int cursorMode; char mouseButtons[GLFW_MOUSE_BUTTON_LAST + 1]; char keys[GLFW_KEY_LAST + 1]; @@ -744,6 +756,7 @@ struct _GLFWplatform const char* (*getClipboardString)(void); void (*updatePreeditCursorRectangle)(_GLFWwindow*); void (*resetPreeditText)(_GLFWwindow*); + void (*setTextInputFocus)(_GLFWwindow*,GLFWbool); void (*setIMEStatus)(_GLFWwindow*,int); int (*getIMEStatus)(_GLFWwindow*); GLFWbool (*initJoysticks)(void); @@ -1070,4 +1083,3 @@ int _glfw_max(int a, int b); void* _glfw_calloc(size_t count, size_t size); void* _glfw_realloc(void* pointer, size_t size); void _glfw_free(void* pointer); - diff --git a/src/null_init.c b/src/null_init.c index 1cf0eccdf5..05377d7170 100644 --- a/src/null_init.c +++ b/src/null_init.c @@ -57,6 +57,7 @@ GLFWbool _glfwConnectNull(int platformID, _GLFWplatform* platform) .getClipboardString = _glfwGetClipboardStringNull, .updatePreeditCursorRectangle = _glfwUpdatePreeditCursorRectangleNull, .resetPreeditText = _glfwResetPreeditTextNull, + .setTextInputFocus = _glfwSetTextInputFocusNull, .setIMEStatus = _glfwSetIMEStatusNull, .getIMEStatus = _glfwGetIMEStatusNull, .initJoysticks = _glfwInitJoysticksNull, @@ -266,4 +267,3 @@ void _glfwTerminateNull(void) _glfwTerminateEGL(); memset(&_glfw.null, 0, sizeof(_glfw.null)); } - diff --git a/src/null_platform.h b/src/null_platform.h index bbd3ee42e4..4b22634152 100644 --- a/src/null_platform.h +++ b/src/null_platform.h @@ -272,6 +272,7 @@ int _glfwGetKeyScancodeNull(int key); void _glfwUpdatePreeditCursorRectangleNull(_GLFWwindow* window); void _glfwResetPreeditTextNull(_GLFWwindow* window); +void _glfwSetTextInputFocusNull(_GLFWwindow* window, GLFWbool focused); void _glfwSetIMEStatusNull(_GLFWwindow* window, int active); int _glfwGetIMEStatusNull(_GLFWwindow* window); @@ -284,4 +285,3 @@ GLFWbool _glfwGetPhysicalDevicePresentationSupportNull(VkInstance instance, VkPh VkResult _glfwCreateWindowSurfaceNull(VkInstance instance, _GLFWwindow* window, const VkAllocationCallbacks* allocator, VkSurfaceKHR* surface); void _glfwPollMonitorsNull(void); - diff --git a/src/null_window.c b/src/null_window.c index efadbe18fa..108b564ed4 100644 --- a/src/null_window.c +++ b/src/null_window.c @@ -559,6 +559,10 @@ void _glfwResetPreeditTextNull(_GLFWwindow* window) { } +void _glfwSetTextInputFocusNull(_GLFWwindow* window, GLFWbool focused) +{ +} + void _glfwSetIMEStatusNull(_GLFWwindow* window, int active) { } @@ -763,4 +767,3 @@ VkResult _glfwCreateWindowSurfaceNull(VkInstance instance, return err; } - diff --git a/src/win32_init.c b/src/win32_init.c index b809cfe214..dafd3d751a 100644 --- a/src/win32_init.c +++ b/src/win32_init.c @@ -172,6 +172,8 @@ static GLFWbool loadLibraries(void) _glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmGetCompositionStringW"); _glfw.win32.imm32.ImmGetContext_ = (PFN_ImmGetContext) _glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmGetContext"); + _glfw.win32.imm32.ImmAssociateContext_ = (PFN_ImmAssociateContext) + _glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmAssociateContext"); _glfw.win32.imm32.ImmGetConversionStatus_ = (PFN_ImmGetConversionStatus) _glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmGetConversionStatus"); _glfw.win32.imm32.ImmGetDescriptionW_ = (PFN_ImmGetDescriptionW) @@ -621,6 +623,7 @@ GLFWbool _glfwConnectWin32(int platformID, _GLFWplatform* platform) .getClipboardString = _glfwGetClipboardStringWin32, .updatePreeditCursorRectangle = _glfwUpdatePreeditCursorRectangleWin32, .resetPreeditText = _glfwResetPreeditTextWin32, + .setTextInputFocus = _glfwSetTextInputFocusWin32, .setIMEStatus = _glfwSetIMEStatusWin32, .getIMEStatus = _glfwGetIMEStatusWin32, .initJoysticks = _glfwInitJoysticksWin32, @@ -741,4 +744,3 @@ void _glfwTerminateWin32(void) } #endif // _GLFW_WIN32 - diff --git a/src/win32_platform.h b/src/win32_platform.h index 4df8442f0f..a2cea9ede0 100644 --- a/src/win32_platform.h +++ b/src/win32_platform.h @@ -263,6 +263,7 @@ typedef LONG (WINAPI * PFN_RtlVerifyVersionInfo)(OSVERSIONINFOEXW*,ULONG,ULONGLO typedef DWORD (WINAPI * PFN_ImmGetCandidateListW)(HIMC,DWORD,LPCANDIDATELIST,DWORD); typedef LONG (WINAPI * PFN_ImmGetCompositionStringW)(HIMC,DWORD,LPVOID,DWORD); typedef HIMC (WINAPI * PFN_ImmGetContext)(HWND); +typedef HIMC (WINAPI * PFN_ImmAssociateContext)(HWND,HIMC); typedef BOOL (WINAPI * PFN_ImmGetConversionStatus)(HIMC,LPDWORD,LPDWORD); typedef UINT (WINAPI * PFN_ImmGetDescriptionW)(HKL,LPWSTR,UINT); typedef BOOL (WINAPI * PFN_ImmGetOpenStatus)(HIMC); @@ -274,6 +275,7 @@ typedef BOOL (WINAPI * PFN_ImmSetOpenStatus)(HIMC,BOOL); #define ImmGetCandidateListW _glfw.win32.imm32.ImmGetCandidateListW_ #define ImmGetCompositionStringW _glfw.win32.imm32.ImmGetCompositionStringW_ #define ImmGetContext _glfw.win32.imm32.ImmGetContext_ +#define ImmAssociateContext _glfw.win32.imm32.ImmAssociateContext_ #define ImmGetConversionStatus _glfw.win32.imm32.ImmGetConversionStatus_ #define ImmGetDescriptionW _glfw.win32.imm32.ImmGetDescriptionW_ #define ImmGetOpenStatus _glfw.win32.imm32.ImmGetOpenStatus_ @@ -380,6 +382,10 @@ typedef struct _GLFWlibraryWGL typedef struct _GLFWwindowWin32 { HWND handle; + // Saved HIMC while explicit text input focus is out. A NULL HIMC is + // associated with the HWND during that period to keep IMM from routing key + // input through composition and candidate UI. + HIMC textInputContext; HICON bigIcon; HICON smallIcon; @@ -473,6 +479,7 @@ typedef struct _GLFWlibraryWin32 PFN_ImmGetCandidateListW ImmGetCandidateListW_; PFN_ImmGetCompositionStringW ImmGetCompositionStringW_; PFN_ImmGetContext ImmGetContext_; + PFN_ImmAssociateContext ImmAssociateContext_; PFN_ImmGetConversionStatus ImmGetConversionStatus_; PFN_ImmGetDescriptionW ImmGetDescriptionW_; PFN_ImmGetOpenStatus ImmGetOpenStatus_; @@ -578,6 +585,7 @@ const char* _glfwGetClipboardStringWin32(void); void _glfwUpdatePreeditCursorRectangleWin32(_GLFWwindow* window); void _glfwResetPreeditTextWin32(_GLFWwindow* window); +void _glfwSetTextInputFocusWin32(_GLFWwindow* window, GLFWbool focused); void _glfwSetIMEStatusWin32(_GLFWwindow* window, int active); int _glfwGetIMEStatusWin32(_GLFWwindow* window); @@ -609,4 +617,3 @@ void _glfwTerminateWGL(void); GLFWbool _glfwCreateContextWGL(_GLFWwindow* window, const _GLFWctxconfig* ctxconfig, const _GLFWfbconfig* fbconfig); - diff --git a/src/win32_window.c b/src/win32_window.c index 55b9185f4b..28b2bcf95e 100644 --- a/src/win32_window.c +++ b/src/win32_window.c @@ -834,6 +834,11 @@ static void clearImmPreedit(_GLFWwindow* window) _glfwInputPreedit(window); } +static GLFWbool textInputFocusDisabled(_GLFWwindow* window) +{ + return window->textInputFocusExplicit && !window->textInputFocus; +} + // Commit the result texts of Imm32 to character-callback // static GLFWbool commitImmResultStr(_GLFWwindow* window) @@ -904,6 +909,9 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l { case WM_IME_SETCONTEXT: { + if (textInputFocusDisabled(window)) + break; + // To draw preedit text by an application side if (lParam & ISC_SHOWUICOMPOSITIONWINDOW) lParam &= ~ISC_SHOWUICOMPOSITIONWINDOW; @@ -1145,6 +1153,9 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l case WM_IME_COMPOSITION: { + if (textInputFocusDisabled(window)) + return 0; + if (lParam & (GCS_RESULTSTR | GCS_COMPSTR)) { if (lParam & GCS_RESULTSTR) @@ -1158,6 +1169,9 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l case WM_IME_ENDCOMPOSITION: { + if (textInputFocusDisabled(window)) + return 0; + clearImmPreedit(window); // Usually clearing candidates in IMN_CLOSECANDIDATE is sufficient. // However, some IME need it here, e.g. Google Japanese Input. @@ -1167,6 +1181,9 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l case WM_IME_NOTIFY: { + if (textInputFocusDisabled(window)) + return 0; + switch (wParam) { case IMN_SETOPENSTATUS: @@ -1954,6 +1971,9 @@ void _glfwDestroyWindowWin32(_GLFWwindow* window) if (window->win32.handle) { + if (window->win32.textInputContext) + ImmAssociateContext(window->win32.handle, window->win32.textInputContext); + RemovePropW(window->win32.handle, L"GLFW"); DestroyWindow(window->win32.handle); window->win32.handle = NULL; @@ -2856,6 +2876,9 @@ void _glfwUpdatePreeditCursorRectangleWin32(_GLFWwindow* window) int h = preedit->cursorHeight; COMPOSITIONFORM areaRect = { CFS_RECT, { x, y }, { x, y, x + w, y + h } }; + if (!hIMC) + return; + ImmSetCompositionWindow(hIMC, &areaRect); CANDIDATEFORM excludeRect = { 0, CFS_EXCLUDE, { x, y }, { x, y, x + w, y + h } }; @@ -2868,24 +2891,91 @@ void _glfwResetPreeditTextWin32(_GLFWwindow* window) { HWND hWnd = window->win32.handle; HIMC hIMC = ImmGetContext(hWnd); - ImmNotifyIME(hIMC, NI_COMPOSITIONSTR, CPS_CANCEL, 0); - ImmReleaseContext(hWnd, hIMC); + GLFWbool releaseContext = GLFW_TRUE; + + if (!hIMC && window->win32.textInputContext) + { + hIMC = window->win32.textInputContext; + releaseContext = GLFW_FALSE; + } + + if (hIMC) + { + ImmNotifyIME(hIMC, NI_COMPOSITIONSTR, CPS_CANCEL, 0); + if (releaseContext) + ImmReleaseContext(hWnd, hIMC); + } + + clearImmPreedit(window); + clearImmCandidate(window); +} + +void _glfwSetTextInputFocusWin32(_GLFWwindow* window, GLFWbool focused) +{ + if (focused) + { + if (window->win32.textInputContext) + { + ImmAssociateContext(window->win32.handle, + window->win32.textInputContext); + window->win32.textInputContext = NULL; + _glfwUpdatePreeditCursorRectangleWin32(window); + } + } + else + { + _glfwResetPreeditTextWin32(window); + + if (!window->win32.textInputContext) + { + window->win32.textInputContext = + ImmAssociateContext(window->win32.handle, NULL); + } + } } void _glfwSetIMEStatusWin32(_GLFWwindow* window, int active) { HWND hWnd = window->win32.handle; - HIMC hIMC = ImmGetContext(hWnd); + HIMC hIMC = window->win32.textInputContext; + GLFWbool releaseContext = GLFW_FALSE; + + if (!hIMC) + { + hIMC = ImmGetContext(hWnd); + releaseContext = GLFW_TRUE; + } + + if (!hIMC) + return; + ImmSetOpenStatus(hIMC, active ? TRUE : FALSE); - ImmReleaseContext(hWnd, hIMC); + + if (releaseContext) + ImmReleaseContext(hWnd, hIMC); } int _glfwGetIMEStatusWin32(_GLFWwindow* window) { HWND hWnd = window->win32.handle; - HIMC hIMC = ImmGetContext(hWnd); - BOOL result = ImmGetOpenStatus(hIMC); - ImmReleaseContext(hWnd, hIMC); + HIMC hIMC = window->win32.textInputContext; + GLFWbool releaseContext = GLFW_FALSE; + BOOL result; + + if (!hIMC) + { + hIMC = ImmGetContext(hWnd); + releaseContext = GLFW_TRUE; + } + + if (!hIMC) + return GLFW_FALSE; + + result = ImmGetOpenStatus(hIMC); + + if (releaseContext) + ImmReleaseContext(hWnd, hIMC); + return result ? GLFW_TRUE : GLFW_FALSE; } @@ -3019,4 +3109,3 @@ GLFWAPI HWND glfwGetWin32Window(GLFWwindow* handle) } #endif // _GLFW_WIN32 - diff --git a/src/wl_init.c b/src/wl_init.c index 3c00b9df38..102c7462fc 100644 --- a/src/wl_init.c +++ b/src/wl_init.c @@ -468,6 +468,7 @@ GLFWbool _glfwConnectWayland(int platformID, _GLFWplatform* platform) .getClipboardString = _glfwGetClipboardStringWayland, .updatePreeditCursorRectangle = _glfwUpdatePreeditCursorRectangleWayland, .resetPreeditText = _glfwResetPreeditTextWayland, + .setTextInputFocus = _glfwSetTextInputFocusWayland, .setIMEStatus = _glfwSetIMEStatusWayland, .getIMEStatus = _glfwGetIMEStatusWayland, #if defined(GLFW_BUILD_LINUX_JOYSTICK) @@ -1045,4 +1046,3 @@ void _glfwTerminateWayland(void) } #endif // _GLFW_WAYLAND - diff --git a/src/wl_platform.h b/src/wl_platform.h index dc4bc5deae..1b087721cc 100644 --- a/src/wl_platform.h +++ b/src/wl_platform.h @@ -718,6 +718,7 @@ const char* _glfwGetClipboardStringWayland(void); void _glfwUpdatePreeditCursorRectangleWayland(_GLFWwindow* window); void _glfwResetPreeditTextWayland(_GLFWwindow* window); +void _glfwSetTextInputFocusWayland(_GLFWwindow* window, GLFWbool focused); void _glfwSetIMEStatusWayland(_GLFWwindow* window, int active); int _glfwGetIMEStatusWayland(_GLFWwindow* window); @@ -745,4 +746,3 @@ void _glfwAddSeatListenerWayland(struct wl_seat* seat); void _glfwAddDataDeviceListenerWayland(struct wl_data_device* device); GLFWbool _glfwWaitForEGLFrameWayland(_GLFWwindow* window); - diff --git a/src/wl_window.c b/src/wl_window.c index d3591d224e..e3f13d40a3 100644 --- a/src/wl_window.c +++ b/src/wl_window.c @@ -734,6 +734,11 @@ static void deactivateTextInputV1(_GLFWwindow* window) zwp_text_input_v1_deactivate(window->wl.textInputV1, _glfw.wl.seat); } +static GLFWbool textInputFocusDisabled(_GLFWwindow* window) +{ + return window->textInputFocusExplicit && !window->textInputFocus; +} + static void xdgToplevelHandleConfigure(void* userData, struct xdg_toplevel* toplevel, int32_t width, @@ -761,7 +766,8 @@ static void xdgToplevelHandleConfigure(void* userData, break; case XDG_TOPLEVEL_STATE_ACTIVATED: window->wl.pending.activated = GLFW_TRUE; - activateTextInputV1(window); + if (!textInputFocusDisabled(window)) + activateTextInputV1(window); break; } } @@ -1706,7 +1712,8 @@ static void pointerHandleButton(void* userData, // On weston, pressing the title bar will cause leave event and never emit // enter event even though back to content area by pressing mouse button // just after it. So activate it here explicitly. - activateTextInputV1(window); + if (!textInputFocusDisabled(window)) + activateTextInputV1(window); _glfw.wl.serial = serial; @@ -2399,8 +2406,13 @@ static void textInputV3Enter(void* data, struct zwp_text_input_v3* textInputV3, struct wl_surface* surface) { - zwp_text_input_v3_enable(textInputV3); - zwp_text_input_v3_commit(textInputV3); + _GLFWwindow* window = (_GLFWwindow*) data; + + if (!textInputFocusDisabled(window)) + { + zwp_text_input_v3_enable(textInputV3); + zwp_text_input_v3_commit(textInputV3); + } } static void textInputV3Reset(_GLFWwindow* window) @@ -2440,6 +2452,9 @@ static void textInputV3PreeditString(void* data, const char* cur = text; unsigned int cursorLength = 0; + if (textInputFocusDisabled(window)) + return; + preedit->textCount = 0; preedit->blockSizesCount = 0; preedit->focusedBlockIndex = 0; @@ -2513,6 +2528,9 @@ static void textInputV3CommitString(void* data, _GLFWwindow* window = (_GLFWwindow*) data; const char* cur = text; + if (textInputFocusDisabled(window)) + return; + if (!window->callbacks.character) return; @@ -2535,6 +2553,9 @@ static void textInputV3Done(void* data, uint32_t serial) { _GLFWwindow* window = (_GLFWwindow*) data; + if (textInputFocusDisabled(window)) + return; + _glfwUpdatePreeditCursorRectangleWayland(window); _glfwInputPreedit(window); } @@ -2560,7 +2581,9 @@ static void textInputV1Enter(void* data, struct wl_surface* surface) { _GLFWwindow* window = (_GLFWwindow*) data; - activateTextInputV1(window); + + if (!textInputFocusDisabled(window)) + activateTextInputV1(window); } static void textInputV1Reset(_GLFWwindow* window) @@ -2586,6 +2609,13 @@ static void textInputV1Leave(void* data, _GLFWwindow* window = (_GLFWwindow*) data; char* commitText = window->wl.textInputV1Context.commitTextOnReset; + if (textInputFocusDisabled(window)) + { + textInputV1Reset(window); + deactivateTextInputV1(window); + return; + } + textInputV3CommitString(data, NULL, commitText); textInputV1Reset(window); deactivateTextInputV1(window); @@ -2611,6 +2641,9 @@ static void textInputV1PreeditString(void* data, { _GLFWwindow* window = (_GLFWwindow*) data; + if (textInputFocusDisabled(window)) + return; + _glfw_free(window->wl.textInputV1Context.preeditText); _glfw_free(window->wl.textInputV1Context.commitTextOnReset); window->wl.textInputV1Context.preeditText = strdup(text); @@ -2637,6 +2670,9 @@ static void textInputV1PreeditCursor(void* data, const char* text = window->wl.textInputV1Context.preeditText; const char* cur = text; + if (textInputFocusDisabled(window)) + return; + preedit->caretIndex = 0; if (index <= 0 || preedit->textCount == 0) return; @@ -2659,6 +2695,9 @@ static void textInputV1CommitString(void* data, { _GLFWwindow* window = (_GLFWwindow*) data; + if (textInputFocusDisabled(window)) + return; + textInputV1Reset(window); textInputV3CommitString(data, NULL, text); } @@ -2686,6 +2725,10 @@ static void textInputV1Keysym(void* data, uint32_t state, uint32_t modifiers) { + _GLFWwindow* window = (_GLFWwindow*) data; + if (textInputFocusDisabled(window)) + return; + uint32_t scancode; // This code supports only weston-keyboard because we aren't aware @@ -3932,6 +3975,33 @@ void _glfwResetPreeditTextWayland(_GLFWwindow* window) { } +void _glfwSetTextInputFocusWayland(_GLFWwindow* window, GLFWbool focused) +{ + if (window->wl.textInputV3) + { + if (focused) + zwp_text_input_v3_enable(window->wl.textInputV3); + else + { + zwp_text_input_v3_disable(window->wl.textInputV3); + textInputV3Reset(window); + } + + zwp_text_input_v3_commit(window->wl.textInputV3); + } + else if (window->wl.textInputV1) + { + if (focused) + activateTextInputV1(window); + else + { + zwp_text_input_v1_reset(window->wl.textInputV1); + textInputV1Reset(window); + deactivateTextInputV1(window); + } + } +} + void _glfwSetIMEStatusWayland(_GLFWwindow* window, int active) { } @@ -4059,4 +4129,3 @@ GLFWAPI struct wl_surface* glfwGetWaylandWindow(GLFWwindow* handle) } #endif // _GLFW_WAYLAND - diff --git a/src/x11_ibus_module.c b/src/x11_ibus_module.c new file mode 100644 index 0000000000..de4a1c663f --- /dev/null +++ b/src/x11_ibus_module.c @@ -0,0 +1,1468 @@ +#define _POSIX_C_SOURCE 200809L + +//======================================================================== +// GLFW X11 IBus IME module prototype +//======================================================================== + +#include "x11_ime_module.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static const char* IBUS_SERVICE = "org.freedesktop.IBus"; +static const char* IBUS_PATH = "/org/freedesktop/IBus"; +static const char* IBUS_INTERFACE = "org.freedesktop.IBus"; +static const char* IBUS_INPUT_INTERFACE = "org.freedesktop.IBus.InputContext"; + +enum +{ + IBUS_CAP_PREEDIT_TEXT = 1 << 0, + IBUS_CAP_FOCUS = 1 << 3 +}; + +enum +{ + IBUS_SHIFT_MASK = 1 << 0, + IBUS_LOCK_MASK = 1 << 1, + IBUS_CONTROL_MASK = 1 << 2, + IBUS_MOD1_MASK = 1 << 3, + IBUS_MOD2_MASK = 1 << 4, + IBUS_MOD4_MASK = 1 << 6, + IBUS_RELEASE_MASK = 1 << 30 +}; + +enum +{ + IBUS_ATTR_TYPE_UNDERLINE = 1, + IBUS_ATTR_UNDERLINE_SINGLE = 1 +}; + +typedef enum CommandType +{ + COMMAND_KEY, + COMMAND_FOCUS_IN, + COMMAND_FOCUS_OUT, + COMMAND_CURSOR_RECT, + COMMAND_RESET, + COMMAND_SET_STATUS, + COMMAND_STOP +} CommandType; + +typedef enum EventType +{ + EVENT_COMMIT, + EVENT_PREEDIT, + EVENT_CLEAR_PREEDIT, + EVENT_STATUS +} EventType; + +typedef struct Request +{ + unsigned long id; + GLFWx11IMEKeyEvent event; + int completed; + int handled; + int timed_out; + int failed; + double queued_at; + double completed_at; + int refs; + struct Request* next_recent; +} Request; + +typedef struct Command +{ + CommandType type; + void* window; + unsigned long x11_window; + int x, y, w, h; + int status; + Request* request; + struct Command* next; +} Command; + +typedef struct QueuedEvent +{ + EventType type; + void* window; + char* text; + int caret; + int* block_sizes; + int block_count; + int focused_block; + unsigned long request_id; + int request_timed_out; + double timestamp; + struct QueuedEvent* next; +} QueuedEvent; + +typedef struct PreeditBlock +{ + int start; + int end; + int focused; +} PreeditBlock; + +typedef struct PreeditInfo +{ + const char* text; + int caret; + int visible; + int* block_sizes; + int block_count; + int focused_block; +} PreeditInfo; + +struct GLFWx11IMEBackend +{ + GLFWx11IMEHostAPI host; + pthread_t thread; + pthread_mutex_t mutex; + pthread_cond_t cond; + int running; + int ready; + int status; + double timeout_ms; + void* focused_window; + void* active_window; + unsigned long next_request_id; + unsigned long active_request_id; + unsigned long last_request_id; + // Commands are written by the GLFW/X11 thread and consumed by the worker. + Command* command_head; + Command* command_tail; + // Events are written by the worker and drained by GLFW on the main thread. + // The worker never calls GLFW callbacks directly. + QueuedEvent* event_head; + QueuedEvent* event_tail; + // Recent requests are retained only to observe late replies and signals + // after a ProcessKeyEvent timeout. + Request* recent; + DBusConnection* connection; + char* input_context_path; +}; + +static char* xstrdup(const char* string) +{ + size_t length; + char* copy; + + if (!string) + return NULL; + + length = strlen(string) + 1; + copy = malloc(length); + if (copy) + memcpy(copy, string, length); + return copy; +} + +static double now_seconds(GLFWx11IMEBackend* backend) +{ + if (backend->host.get_time) + return backend->host.get_time(); + + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec + ts.tv_nsec / 1000000000.0; +} + +static void log_line(GLFWx11IMEBackend* backend, const char* fmt, ...) +{ + char buffer[1024]; + const char* debug = getenv("GLFW_IME_DEBUG"); + va_list vl; + va_start(vl, fmt); + vsnprintf(buffer, sizeof(buffer), fmt, vl); + va_end(vl); + + if (backend->host.log) + backend->host.log(buffer); + else if (debug && *debug && strcmp(debug, "0") != 0) + fprintf(stderr, "glfw-ibus: %s\n", buffer); +} + +static uint32_t ibus_state_from_glfw(unsigned int mods, int action) +{ + uint32_t state = action == GLFW_RELEASE ? IBUS_RELEASE_MASK : 0; + + if (mods & GLFW_MOD_SHIFT) + state |= IBUS_SHIFT_MASK; + if (mods & GLFW_MOD_CAPS_LOCK) + state |= IBUS_LOCK_MASK; + if (mods & GLFW_MOD_CONTROL) + state |= IBUS_CONTROL_MASK; + if (mods & GLFW_MOD_ALT) + state |= IBUS_MOD1_MASK; + if (mods & GLFW_MOD_NUM_LOCK) + state |= IBUS_MOD2_MASK; + if (mods & GLFW_MOD_SUPER) + state |= IBUS_MOD4_MASK; + + return state; +} + +static void release_request(GLFWx11IMEBackend* backend, Request* request) +{ + int refs; + + if (!request) + return; + + refs = --request->refs; + if (refs == 0) + free(request); + (void) backend; +} + +static Request* find_recent(GLFWx11IMEBackend* backend, unsigned long id) +{ + for (Request* request = backend->recent; request; request = request->next_recent) + { + if (request->id == id) + return request; + } + + return NULL; +} + +static void remember_recent(GLFWx11IMEBackend* backend, Request* request) +{ + int count = 0; + Request* prev = NULL; + Request* item; + + request->refs++; + request->next_recent = backend->recent; + backend->recent = request; + + item = backend->recent; + while (item) + { + count++; + if (count > 64) + { + Request* old = item; + if (prev) + prev->next_recent = NULL; + while (old) + { + Request* next = old->next_recent; + old->next_recent = NULL; + release_request(backend, old); + old = next; + } + break; + } + prev = item; + item = item->next_recent; + } +} + +static void enqueue_event(GLFWx11IMEBackend* backend, + EventType type, + void* window, + const char* text, + int caret, + const int* block_sizes, + int block_count, + int focused_block) +{ + QueuedEvent* event = calloc(1, sizeof(QueuedEvent)); + unsigned long request_id; + Request* request; + + if (!event) + return; + + pthread_mutex_lock(&backend->mutex); + + // IBus signals do not identify the ProcessKeyEvent that caused them. + // For instrumentation we attribute them to the active request, falling + // back to the most recent request, and record whether that request timed out. + request_id = backend->active_request_id ? backend->active_request_id : + backend->last_request_id; + request = find_recent(backend, request_id); + + event->type = type; + event->window = window ? window : + request ? request->event.window : + backend->active_window ? backend->active_window : + backend->focused_window; + event->text = text ? xstrdup(text) : NULL; + if (block_sizes && block_count > 0) + { + event->block_sizes = calloc((size_t) block_count, sizeof(int)); + if (event->block_sizes) + { + memcpy(event->block_sizes, block_sizes, + sizeof(int) * (size_t) block_count); + event->block_count = block_count; + } + } + event->focused_block = focused_block; + event->caret = caret; + event->request_id = request_id; + event->request_timed_out = request ? request->timed_out : 0; + event->timestamp = now_seconds(backend); + + if (backend->event_tail) + backend->event_tail->next = event; + else + backend->event_head = event; + backend->event_tail = event; + + pthread_mutex_unlock(&backend->mutex); + + log_line(backend, + "event queued type=%i request=%lu timed_out=%i timestamp=%.6f caret=%i blocks=%i focused=%i text='%s'", + type, event->request_id, event->request_timed_out, + event->timestamp, event->caret, event->block_count, + event->focused_block, text ? text : ""); + + if (backend->host.post_empty_event) + backend->host.post_empty_event(); +} + +static Command* pop_command(GLFWx11IMEBackend* backend) +{ + Command* command = backend->command_head; + if (command) + { + backend->command_head = command->next; + if (!backend->command_head) + backend->command_tail = NULL; + } + return command; +} + +static void push_command(GLFWx11IMEBackend* backend, Command* command) +{ + pthread_mutex_lock(&backend->mutex); + if (backend->command_tail) + backend->command_tail->next = command; + else + backend->command_head = command; + backend->command_tail = command; + pthread_cond_signal(&backend->cond); + pthread_mutex_unlock(&backend->mutex); +} + +static int call_no_reply(GLFWx11IMEBackend* backend, const char* method, int first_type, ...) +{ + DBusMessage* message; + va_list vl; + dbus_uint32_t serial = 0; + + if (!backend->connection || !backend->input_context_path) + return GLFW_FALSE; + + message = dbus_message_new_method_call(IBUS_SERVICE, + backend->input_context_path, + IBUS_INPUT_INTERFACE, + method); + if (!message) + return GLFW_FALSE; + + va_start(vl, first_type); + if (first_type != DBUS_TYPE_INVALID && + !dbus_message_append_args_valist(message, first_type, vl)) + { + va_end(vl); + dbus_message_unref(message); + return GLFW_FALSE; + } + va_end(vl); + + if (!dbus_connection_send(backend->connection, message, &serial)) + { + dbus_message_unref(message); + return GLFW_FALSE; + } + + dbus_connection_flush(backend->connection); + dbus_message_unref(message); + return GLFW_TRUE; +} + +static int compare_ints(const void* a, const void* b) +{ + const int ia = *(const int*) a; + const int ib = *(const int*) b; + return (ia > ib) - (ia < ib); +} + +static int utf8_count(const char* text) +{ + int count = 0; + const unsigned char* p = (const unsigned char*) text; + + while (p && *p) + { + if ((*p & 0xc0) != 0x80) + count++; + p++; + } + + return count; +} + +static int normalize_ibus_index(const char* text, int char_count, int index) +{ + const char* p = text; + int chars = 0; + + if (index <= 0) + return 0; + + if (index <= char_count) + return index; + + for (int byte = 0; p && *p; byte++) + { + if (byte == index) + return chars; + if (((unsigned char) *p & 0xc0) != 0x80) + chars++; + p++; + } + + return char_count; +} + +static void append_preedit_block(PreeditBlock** blocks, + int* count, + int* capacity, + int start, + int end, + int focused) +{ + if (start >= end) + return; + + if (*count == *capacity) + { + const int new_capacity = *capacity ? *capacity * 2 : 8; + PreeditBlock* new_blocks = + realloc(*blocks, sizeof(PreeditBlock) * (size_t) new_capacity); + if (!new_blocks) + return; + + *blocks = new_blocks; + *capacity = new_capacity; + } + + (*blocks)[*count].start = start; + (*blocks)[*count].end = end; + (*blocks)[*count].focused = focused; + (*count)++; +} + +static void parse_ibus_attribute(DBusMessageIter* iter, + const char* text, + int char_count, + PreeditBlock** blocks, + int* count, + int* capacity) +{ + DBusMessageIter structure; + const char* id = NULL; + dbus_uint32_t type = 0, value = 0, start = 0, end = 0; + + if (dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_VARIANT) + { + DBusMessageIter variant; + dbus_message_iter_recurse(iter, &variant); + parse_ibus_attribute(&variant, text, char_count, blocks, count, capacity); + return; + } + + if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_STRUCT) + return; + + dbus_message_iter_recurse(iter, &structure); + if (dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_STRING) + return; + + dbus_message_iter_get_basic(&structure, &id); + if (!id || strcmp(id, "IBusAttribute") != 0) + return; + + if (!dbus_message_iter_next(&structure) || + !dbus_message_iter_next(&structure) || + dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_UINT32) + { + return; + } + dbus_message_iter_get_basic(&structure, &type); + + if (!dbus_message_iter_next(&structure) || + dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_UINT32) + { + return; + } + dbus_message_iter_get_basic(&structure, &value); + + if (!dbus_message_iter_next(&structure) || + dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_UINT32) + { + return; + } + dbus_message_iter_get_basic(&structure, &start); + + if (!dbus_message_iter_next(&structure) || + dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_UINT32) + { + return; + } + dbus_message_iter_get_basic(&structure, &end); + + append_preedit_block(blocks, count, capacity, + normalize_ibus_index(text, char_count, (int) start), + normalize_ibus_index(text, char_count, (int) end), + type != IBUS_ATTR_TYPE_UNDERLINE || + value != IBUS_ATTR_UNDERLINE_SINGLE); +} + +static void parse_ibus_attr_list(DBusMessageIter* iter, + const char* text, + int char_count, + PreeditBlock** blocks, + int* count, + int* capacity) +{ + DBusMessageIter structure; + const char* id = NULL; + + if (dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_VARIANT) + { + DBusMessageIter variant; + dbus_message_iter_recurse(iter, &variant); + parse_ibus_attr_list(&variant, text, char_count, blocks, count, capacity); + return; + } + + if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_STRUCT) + return; + + dbus_message_iter_recurse(iter, &structure); + if (dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_STRING) + return; + + dbus_message_iter_get_basic(&structure, &id); + if (!id || strcmp(id, "IBusAttrList") != 0) + return; + + while (dbus_message_iter_next(&structure)) + { + if (dbus_message_iter_get_arg_type(&structure) == DBUS_TYPE_ARRAY) + { + DBusMessageIter array; + dbus_message_iter_recurse(&structure, &array); + while (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_INVALID) + { + parse_ibus_attribute(&array, text, char_count, + blocks, count, capacity); + dbus_message_iter_next(&array); + } + } + } +} + +static void build_preedit_blocks(PreeditInfo* info, + PreeditBlock* attrs, + int attr_count) +{ + int boundary_count = 0; + int focused_start = -1; + const int text_count = utf8_count(info->text); + int* boundaries; + + info->focused_block = 0; + + if (!text_count) + return; + + boundaries = calloc((size_t) attr_count * 2 + 2, sizeof(int)); + if (!boundaries) + return; + + boundaries[boundary_count++] = 0; + boundaries[boundary_count++] = text_count; + + for (int i = 0; i < attr_count; i++) + { + int start = attrs[i].start; + int end = attrs[i].end; + + if (start < 0) + start = 0; + if (end > text_count) + end = text_count; + if (start >= end) + continue; + + boundaries[boundary_count++] = start; + boundaries[boundary_count++] = end; + + if (attrs[i].focused && focused_start < 0) + focused_start = start; + } + + qsort(boundaries, (size_t) boundary_count, sizeof(int), compare_ints); + + info->block_sizes = calloc((size_t) boundary_count, sizeof(int)); + if (!info->block_sizes) + { + free(boundaries); + return; + } + + int previous = boundaries[0]; + for (int i = 1; i < boundary_count; i++) + { + const int current = boundaries[i]; + if (current == previous) + continue; + + info->block_sizes[info->block_count] = current - previous; + + if (focused_start >= previous && focused_start < current) + info->focused_block = info->block_count; + else if (focused_start < 0 && + info->caret >= previous && + (info->caret < current || current == text_count)) + { + info->focused_block = info->block_count; + } + + info->block_count++; + previous = current; + } + + free(boundaries); +} + +static int parse_ibus_text_variant(DBusMessageIter* iter, + PreeditInfo* info, + PreeditBlock** attrs, + int* attr_count, + int* attr_capacity) +{ + DBusMessageIter variant, structure; + const char* id = NULL; + + if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_VARIANT) + return GLFW_FALSE; + + dbus_message_iter_recurse(iter, &variant); + if (dbus_message_iter_get_arg_type(&variant) != DBUS_TYPE_STRUCT) + return GLFW_FALSE; + + dbus_message_iter_recurse(&variant, &structure); + if (dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_STRING) + return GLFW_FALSE; + + dbus_message_iter_get_basic(&structure, &id); + if (!id || strcmp(id, "IBusText") != 0) + return GLFW_FALSE; + + dbus_message_iter_next(&structure); + dbus_message_iter_next(&structure); + if (dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_STRING) + return GLFW_FALSE; + + dbus_message_iter_get_basic(&structure, &info->text); + + while (dbus_message_iter_next(&structure)) + parse_ibus_attr_list(&structure, info->text, utf8_count(info->text), + attrs, attr_count, attr_capacity); + + return GLFW_TRUE; +} + +static int parse_ibus_text(DBusMessage* message, PreeditInfo* info) +{ + DBusMessageIter iter; + PreeditBlock* attrs = NULL; + int attr_count = 0; + int attr_capacity = 0; + + memset(info, 0, sizeof(*info)); + info->caret = -1; + info->visible = GLFW_TRUE; + + dbus_message_iter_init(message, &iter); + if (!parse_ibus_text_variant(&iter, info, &attrs, &attr_count, &attr_capacity)) + { + free(attrs); + return GLFW_FALSE; + } + + if (dbus_message_iter_next(&iter) && + dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_UINT32) + { + dbus_uint32_t caret = 0; + dbus_message_iter_get_basic(&iter, &caret); + info->caret = normalize_ibus_index(info->text, + utf8_count(info->text), + (int) caret); + } + + if (dbus_message_iter_next(&iter) && + dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_BOOLEAN) + { + dbus_bool_t visible = 1; + dbus_message_iter_get_basic(&iter, &visible); + info->visible = visible ? GLFW_TRUE : GLFW_FALSE; + } + + build_preedit_blocks(info, attrs, attr_count); + free(attrs); + return GLFW_TRUE; +} + +static DBusHandlerResult dbus_filter(DBusConnection* connection, + DBusMessage* message, + void* data) +{ + GLFWx11IMEBackend* backend = data; + PreeditInfo info; + (void) connection; + + if (dbus_message_is_signal(message, IBUS_INPUT_INTERFACE, "CommitText")) + { + if (parse_ibus_text(message, &info)) + enqueue_event(backend, EVENT_COMMIT, NULL, info.text ? info.text : "", -1, + NULL, 0, 0); + free(info.block_sizes); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_is_signal(message, IBUS_INPUT_INTERFACE, "UpdatePreeditText")) + { + if (parse_ibus_text(message, &info)) + { + if (info.visible) + enqueue_event(backend, EVENT_PREEDIT, NULL, info.text ? info.text : "", + info.caret, info.block_sizes, info.block_count, + info.focused_block); + else + enqueue_event(backend, EVENT_CLEAR_PREEDIT, NULL, NULL, -1, + NULL, 0, 0); + } + free(info.block_sizes); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_is_signal(message, IBUS_INPUT_INTERFACE, "HidePreeditText")) + { + enqueue_event(backend, EVENT_CLEAR_PREEDIT, NULL, NULL, -1, NULL, 0, 0); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_is_signal(message, IBUS_INPUT_INTERFACE, "Enabled")) + { + backend->status = GLFW_TRUE; + enqueue_event(backend, EVENT_STATUS, NULL, NULL, -1, NULL, 0, 0); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_is_signal(message, IBUS_INPUT_INTERFACE, "Disabled")) + { + backend->status = GLFW_FALSE; + enqueue_event(backend, EVENT_STATUS, NULL, NULL, -1, NULL, 0, 0); + return DBUS_HANDLER_RESULT_HANDLED; + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static int read_ibus_address(char* buffer, size_t size) +{ + const char* address = getenv("IBUS_ADDRESS"); + char path[4096]; + char display[128]; + const char* config; + const char* home; + char* machine_id; + DBusError error; + FILE* file; + + if (address && *address) + { + snprintf(buffer, size, "%s", address); + return GLFW_TRUE; + } + + snprintf(display, sizeof(display), "%s", getenv("DISPLAY") ? getenv("DISPLAY") : ":0.0"); + char* colon = strrchr(display, ':'); + if (!colon) + return GLFW_FALSE; + + char* screen = strrchr(display, '.'); + if (screen) + *screen = '\0'; + + *colon = '\0'; + const char* host = *display ? display : "unix"; + const char* number = colon + 1; + + config = getenv("XDG_CONFIG_HOME"); + home = getenv("HOME"); + + dbus_error_init(&error); + machine_id = dbus_try_get_local_machine_id(&error); + if (!machine_id) + { + dbus_error_free(&error); + return GLFW_FALSE; + } + + if (config && *config) + snprintf(path, sizeof(path), "%s/ibus/bus/%s-%s-%s", config, machine_id, host, number); + else if (home && *home) + snprintf(path, sizeof(path), "%s/.config/ibus/bus/%s-%s-%s", home, machine_id, host, number); + else + { + dbus_free(machine_id); + return GLFW_FALSE; + } + + dbus_free(machine_id); + + file = fopen(path, "r"); + if (!file) + return GLFW_FALSE; + + while (fgets(buffer, size, file)) + { + if (strncmp(buffer, "IBUS_ADDRESS=", 13) == 0) + { + char* value = buffer + 13; + char* newline = strchr(value, '\n'); + if (newline) + *newline = '\0'; + memmove(buffer, value, strlen(value) + 1); + fclose(file); + return GLFW_TRUE; + } + } + + fclose(file); + return GLFW_FALSE; +} + +static int connect_ibus(GLFWx11IMEBackend* backend) +{ + char address[1024]; + DBusError error; + DBusMessage* message; + DBusMessage* reply; + const char* client = "GLFW research prototype"; + const char* path = NULL; + dbus_uint32_t caps = IBUS_CAP_FOCUS | IBUS_CAP_PREEDIT_TEXT; + + if (!read_ibus_address(address, sizeof(address))) + { + log_line(backend, "failed to discover IBus address"); + return GLFW_FALSE; + } + + dbus_error_init(&error); + backend->connection = dbus_connection_open_private(address, &error); + if (!backend->connection) + { + log_line(backend, "failed to open IBus connection: %s", error.message ? error.message : ""); + dbus_error_free(&error); + return GLFW_FALSE; + } + + dbus_connection_set_exit_on_disconnect(backend->connection, FALSE); + if (!dbus_bus_register(backend->connection, &error)) + { + log_line(backend, "failed to register IBus bus connection: %s", error.message ? error.message : ""); + dbus_error_free(&error); + return GLFW_FALSE; + } + + message = dbus_message_new_method_call(IBUS_SERVICE, IBUS_PATH, + IBUS_INTERFACE, "CreateInputContext"); + if (!message) + return GLFW_FALSE; + + dbus_message_append_args(message, + DBUS_TYPE_STRING, &client, + DBUS_TYPE_INVALID); + reply = dbus_connection_send_with_reply_and_block(backend->connection, + message, 3000, &error); + dbus_message_unref(message); + if (!reply) + { + log_line(backend, "CreateInputContext failed: %s", error.message ? error.message : ""); + dbus_error_free(&error); + return GLFW_FALSE; + } + + if (!dbus_message_get_args(reply, &error, + DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID)) + { + log_line(backend, "CreateInputContext reply parse failed: %s", error.message ? error.message : ""); + dbus_error_free(&error); + dbus_message_unref(reply); + return GLFW_FALSE; + } + + backend->input_context_path = xstrdup(path); + dbus_message_unref(reply); + + dbus_bus_add_match(backend->connection, + "type='signal',interface='org.freedesktop.IBus.InputContext'", + NULL); + dbus_connection_add_filter(backend->connection, dbus_filter, backend, NULL); + call_no_reply(backend, "SetCapabilities", + DBUS_TYPE_UINT32, &caps, + DBUS_TYPE_INVALID); + backend->ready = GLFW_TRUE; + backend->status = GLFW_TRUE; + + log_line(backend, "connected to IBus at %s path=%s", address, + backend->input_context_path ? backend->input_context_path : ""); + return GLFW_TRUE; +} + +static void process_key_command(GLFWx11IMEBackend* backend, Command* command) +{ + Request* request = command->request; + DBusError error; + DBusMessage* message; + DBusMessage* reply; + dbus_bool_t handled = FALSE; + dbus_uint32_t keyval = request->event.keysym; + dbus_uint32_t keycode = request->event.keycode; + dbus_uint32_t state = ibus_state_from_glfw(request->event.mods, + request->event.action); + + pthread_mutex_lock(&backend->mutex); + backend->active_request_id = request->id; + backend->active_window = request->event.window; + backend->last_request_id = request->id; + pthread_mutex_unlock(&backend->mutex); + + log_line(backend, + "request start id=%lu key_serial=%lu timestamp=%.6f keyval=0x%x keycode=%u state=0x%x", + request->id, request->event.time, request->queued_at, + keyval, keycode, state); + + if (request->event.cursor_rect_valid) + { + log_line(backend, + "request cursor id=%lu valid=1 original=(%i,%i %ix%i) root=(%i,%i %ix%i) sent_before_process=1", + request->id, + request->event.cursor_x, + request->event.cursor_y, + request->event.cursor_width, + request->event.cursor_height, + request->event.cursor_root_x, + request->event.cursor_root_y, + request->event.cursor_width, + request->event.cursor_height); + call_no_reply(backend, "SetCursorLocation", + DBUS_TYPE_INT32, &request->event.cursor_root_x, + DBUS_TYPE_INT32, &request->event.cursor_root_y, + DBUS_TYPE_INT32, &request->event.cursor_width, + DBUS_TYPE_INT32, &request->event.cursor_height, + DBUS_TYPE_INVALID); + } + else + { + log_line(backend, + "request cursor id=%lu valid=0 sent_before_process=0", + request->id); + } + + dbus_error_init(&error); + message = dbus_message_new_method_call(IBUS_SERVICE, + backend->input_context_path, + IBUS_INPUT_INTERFACE, + "ProcessKeyEvent"); + if (message && + dbus_message_append_args(message, + DBUS_TYPE_UINT32, &keyval, + DBUS_TYPE_UINT32, &keycode, + DBUS_TYPE_UINT32, &state, + DBUS_TYPE_INVALID)) + { + reply = dbus_connection_send_with_reply_and_block(backend->connection, + message, 3000, &error); + if (reply) + { + dbus_message_get_args(reply, &error, + DBUS_TYPE_BOOLEAN, &handled, + DBUS_TYPE_INVALID); + dbus_message_unref(reply); + } + else + request->failed = GLFW_TRUE; + } + else + request->failed = GLFW_TRUE; + + if (message) + dbus_message_unref(message); + + if (dbus_error_is_set(&error)) + { + log_line(backend, "request error id=%lu error=%s", request->id, + error.message ? error.message : ""); + dbus_error_free(&error); + request->failed = GLFW_TRUE; + } + + pthread_mutex_lock(&backend->mutex); + backend->active_request_id = 0; + backend->active_window = NULL; + request->handled = handled ? GLFW_TRUE : GLFW_FALSE; + request->completed = GLFW_TRUE; + request->completed_at = now_seconds(backend); + remember_recent(backend, request); + pthread_cond_broadcast(&backend->cond); + pthread_mutex_unlock(&backend->mutex); + + log_line(backend, + "request reply id=%lu latency=%.3fms handled=%i timed_out=%i failed=%i", + request->id, (request->completed_at - request->queued_at) * 1000.0, + request->handled, request->timed_out, request->failed); + + release_request(backend, request); +} + +static void dispatch_dbus(GLFWx11IMEBackend* backend) +{ + if (!backend->connection) + return; + + dbus_connection_read_write_dispatch(backend->connection, 0); + while (dbus_connection_dispatch(backend->connection) == DBUS_DISPATCH_DATA_REMAINS) + { + } +} + +static void* worker_main(void* data) +{ + GLFWx11IMEBackend* backend = data; + + // This thread owns all D-Bus traffic for the module. GLFW only sees the + // synchronous process_key result and queued events drained on the main thread. + connect_ibus(backend); + + for (;;) + { + Command* command = NULL; + + dispatch_dbus(backend); + + pthread_mutex_lock(&backend->mutex); + if (backend->running && !backend->command_head) + { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_nsec += 10 * 1000 * 1000; + if (ts.tv_nsec >= 1000000000L) + { + ts.tv_sec++; + ts.tv_nsec -= 1000000000L; + } + pthread_cond_timedwait(&backend->cond, &backend->mutex, &ts); + } + + command = pop_command(backend); + if (!backend->running && !command) + { + pthread_mutex_unlock(&backend->mutex); + break; + } + pthread_mutex_unlock(&backend->mutex); + + if (!command) + continue; + + if (command->type == COMMAND_STOP) + { + free(command); + break; + } + + if (!backend->ready && command->type != COMMAND_STOP) + { + if (command->request) + { + pthread_mutex_lock(&backend->mutex); + command->request->completed = GLFW_TRUE; + command->request->failed = GLFW_TRUE; + command->request->completed_at = now_seconds(backend); + pthread_cond_broadcast(&backend->cond); + pthread_mutex_unlock(&backend->mutex); + release_request(backend, command->request); + } + free(command); + continue; + } + + switch (command->type) + { + case COMMAND_KEY: + process_key_command(backend, command); + break; + case COMMAND_FOCUS_IN: + backend->focused_window = command->window; + call_no_reply(backend, "FocusIn", DBUS_TYPE_INVALID); + break; + case COMMAND_FOCUS_OUT: + if (backend->focused_window == command->window) + backend->focused_window = NULL; + call_no_reply(backend, "FocusOut", DBUS_TYPE_INVALID); + break; + case COMMAND_CURSOR_RECT: + log_line(backend, + "SetCursorLocation final=(%i,%i %ix%i)", + command->x, command->y, command->w, command->h); + call_no_reply(backend, "SetCursorLocation", + DBUS_TYPE_INT32, &command->x, + DBUS_TYPE_INT32, &command->y, + DBUS_TYPE_INT32, &command->w, + DBUS_TYPE_INT32, &command->h, + DBUS_TYPE_INVALID); + break; + case COMMAND_RESET: + call_no_reply(backend, "Reset", DBUS_TYPE_INVALID); + break; + case COMMAND_SET_STATUS: + backend->status = command->status; + call_no_reply(backend, command->status ? "FocusIn" : "FocusOut", + DBUS_TYPE_INVALID); + break; + default: + break; + } + + free(command); + } + + if (backend->connection) + { + dbus_connection_close(backend->connection); + dbus_connection_unref(backend->connection); + backend->connection = NULL; + } + + free(backend->input_context_path); + backend->input_context_path = NULL; + return NULL; +} + +static GLFWx11IMEBackend* backend_create(const GLFWx11IMEHostAPI* host) +{ + GLFWx11IMEBackend* backend = calloc(1, sizeof(GLFWx11IMEBackend)); + const char* timeout; + + if (!backend) + return NULL; + + backend->host = *host; + backend->running = GLFW_TRUE; + backend->timeout_ms = 100.0; + timeout = getenv("GLFW_IBUS_TIMEOUT_MS"); + if (timeout && *timeout) + backend->timeout_ms = atof(timeout); + + pthread_mutex_init(&backend->mutex, NULL); + pthread_cond_init(&backend->cond, NULL); + + if (pthread_create(&backend->thread, NULL, worker_main, backend) != 0) + { + pthread_cond_destroy(&backend->cond); + pthread_mutex_destroy(&backend->mutex); + free(backend); + return NULL; + } + + log_line(backend, "module created timeout=%.3fms", backend->timeout_ms); + return backend; +} + +static void backend_destroy(GLFWx11IMEBackend* backend) +{ + Command* command; + QueuedEvent* event; + Request* request; + + if (!backend) + return; + + command = calloc(1, sizeof(Command)); + if (command) + { + command->type = COMMAND_STOP; + push_command(backend, command); + } + + pthread_mutex_lock(&backend->mutex); + backend->running = GLFW_FALSE; + pthread_cond_signal(&backend->cond); + pthread_mutex_unlock(&backend->mutex); + + pthread_join(backend->thread, NULL); + + while (backend->command_head) + { + command = backend->command_head; + backend->command_head = command->next; + free(command); + } + + while (backend->event_head) + { + event = backend->event_head; + backend->event_head = event->next; + free(event->text); + free(event); + } + + request = backend->recent; + while (request) + { + Request* next = request->next_recent; + request->next_recent = NULL; + release_request(backend, request); + request = next; + } + + pthread_cond_destroy(&backend->cond); + pthread_mutex_destroy(&backend->mutex); + free(backend); +} + +static void enqueue_simple(GLFWx11IMEBackend* backend, + CommandType type, + void* window, + unsigned long x11_window) +{ + Command* command = calloc(1, sizeof(Command)); + if (!command) + return; + + command->type = type; + command->window = window; + command->x11_window = x11_window; + push_command(backend, command); +} + +static void backend_focus_in(GLFWx11IMEBackend* backend, void* window, unsigned long x11_window) +{ + enqueue_simple(backend, COMMAND_FOCUS_IN, window, x11_window); +} + +static void backend_focus_out(GLFWx11IMEBackend* backend, void* window, unsigned long x11_window) +{ + enqueue_simple(backend, COMMAND_FOCUS_OUT, window, x11_window); +} + +static void backend_set_cursor_rect(GLFWx11IMEBackend* backend, + void* window, + int x, int y, int w, int h) +{ + Command* command = calloc(1, sizeof(Command)); + if (!command) + return; + + command->type = COMMAND_CURSOR_RECT; + command->window = window; + command->x = x; + command->y = y; + command->w = w; + command->h = h; + push_command(backend, command); +} + +static void backend_reset(GLFWx11IMEBackend* backend, void* window) +{ + enqueue_simple(backend, COMMAND_RESET, window, 0); +} + +static int backend_process_key(GLFWx11IMEBackend* backend, + const GLFWx11IMEKeyEvent* event, + GLFWx11IMEKeyResult* result) +{ + Command* command; + Request* request; + struct timespec deadline; + int rc = 0; + + request = calloc(1, sizeof(Request)); + command = calloc(1, sizeof(Command)); + if (!request || !command) + { + free(request); + free(command); + return GLFW_FALSE; + } + + pthread_mutex_lock(&backend->mutex); + request->id = ++backend->next_request_id; + request->event = *event; + request->queued_at = now_seconds(backend); + request->refs = 2; + command->type = COMMAND_KEY; + command->window = event->window; + command->x11_window = event->x11_window; + command->request = request; + + if (backend->command_tail) + backend->command_tail->next = command; + else + backend->command_head = command; + backend->command_tail = command; + pthread_cond_signal(&backend->cond); + + clock_gettime(CLOCK_REALTIME, &deadline); + long nsec = (long) (backend->timeout_ms * 1000000.0); + deadline.tv_sec += nsec / 1000000000L; + deadline.tv_nsec += nsec % 1000000000L; + if (deadline.tv_nsec >= 1000000000L) + { + deadline.tv_sec++; + deadline.tv_nsec -= 1000000000L; + } + + while (!request->completed && rc != ETIMEDOUT) + rc = pthread_cond_timedwait(&backend->cond, &backend->mutex, &deadline); + + if (!request->completed) + { + request->timed_out = GLFW_TRUE; + result->timed_out = GLFW_TRUE; + result->handled = GLFW_FALSE; + result->elapsed_ms = (now_seconds(backend) - request->queued_at) * 1000.0; + result->request_id = request->id; + remember_recent(backend, request); + log_line(backend, "request timeout id=%lu latency=%.3fms key_serial=%lu", + request->id, result->elapsed_ms, event->time); + } + else + { + result->timed_out = request->timed_out; + result->handled = request->handled; + result->elapsed_ms = (request->completed_at - request->queued_at) * 1000.0; + result->request_id = request->id; + } + + release_request(backend, request); + pthread_mutex_unlock(&backend->mutex); + + return GLFW_TRUE; +} + +static int backend_get_status(GLFWx11IMEBackend* backend, void* window) +{ + (void) window; + return backend->status; +} + +static void backend_set_status(GLFWx11IMEBackend* backend, void* window, int enabled) +{ + Command* command = calloc(1, sizeof(Command)); + if (!command) + return; + + command->type = COMMAND_SET_STATUS; + command->window = window; + command->status = enabled ? GLFW_TRUE : GLFW_FALSE; + push_command(backend, command); +} + +static void backend_drain_events(GLFWx11IMEBackend* backend) +{ + for (;;) + { + QueuedEvent* event; + + pthread_mutex_lock(&backend->mutex); + event = backend->event_head; + if (event) + { + backend->event_head = event->next; + if (!backend->event_head) + backend->event_tail = NULL; + } + pthread_mutex_unlock(&backend->mutex); + + if (!event) + break; + + log_line(backend, + "event drain type=%i request=%lu timed_out=%i timestamp=%.6f caret=%i blocks=%i focused=%i text='%s'", + event->type, event->request_id, event->request_timed_out, + event->timestamp, event->caret, event->block_count, + event->focused_block, event->text ? event->text : ""); + + switch (event->type) + { + case EVENT_COMMIT: + if (backend->host.commit_text) + backend->host.commit_text(event->window, event->text ? event->text : ""); + break; + case EVENT_PREEDIT: + if (backend->host.update_preedit) + { + backend->host.update_preedit(event->window, + event->text ? event->text : "", + event->caret, + event->block_sizes, + event->block_count, + event->focused_block); + } + break; + case EVENT_CLEAR_PREEDIT: + if (backend->host.clear_preedit) + backend->host.clear_preedit(event->window); + break; + case EVENT_STATUS: + if (backend->host.status_changed) + backend->host.status_changed(event->window); + break; + } + + free(event->text); + free(event->block_sizes); + free(event); + } +} + +int glfwGetX11IMEBackend(int abiVersion, + const GLFWx11IMEHostAPI* host, + GLFWx11IMEBackendAPI* backend) +{ + if (abiVersion != GLFW_X11_IME_MODULE_ABI_VERSION || !host || !backend) + return GLFW_FALSE; + + memset(backend, 0, sizeof(*backend)); + backend->create = backend_create; + backend->destroy = backend_destroy; + backend->focus_in = backend_focus_in; + backend->focus_out = backend_focus_out; + backend->set_cursor_rect = backend_set_cursor_rect; + backend->reset = backend_reset; + backend->process_key = backend_process_key; + backend->get_status = backend_get_status; + backend->set_status = backend_set_status; + backend->drain_events = backend_drain_events; + return GLFW_TRUE; +} diff --git a/src/x11_ime_module.c b/src/x11_ime_module.c new file mode 100644 index 0000000000..34702722bf --- /dev/null +++ b/src/x11_ime_module.c @@ -0,0 +1,540 @@ +//======================================================================== +// GLFW 3.5 X11 IME module prototype - www.glfw.org +//======================================================================== + +#include "internal.h" + +#if defined(_GLFW_X11) + +#include "x11_ime_module.h" + +#include +#include +#include + +#if defined(_GLFW_EMBED_IBUS_MODULE) +extern int glfwGetX11IMEBackend(int,const GLFWx11IMEHostAPI*,GLFWx11IMEBackendAPI*); +#endif + +static void hostLog(const char* message) +{ + if (message && _glfw.x11.imeModule.debug) + fprintf(stderr, "GLFW IME: %s\n", message); +} + +static void hostCommitText(void* handle, const char* utf8) +{ + _GLFWwindow* window = handle; + const char* c = utf8; + + if (!window || !utf8) + return; + + while (*c) + _glfwInputChar(window, _glfwDecodeUTF8(&c), 0, GLFW_TRUE); +} + +static void ensurePreeditBuffers(_GLFWpreedit* preedit, int textCount, int blockCount) +{ + int textBufferCount = preedit->textBufferCount; + int blockBufferCount = preedit->blockSizesBufferCount; + + while (textBufferCount < textCount + 1) + textBufferCount = textBufferCount ? textBufferCount * 2 : 8; + + if (textBufferCount != preedit->textBufferCount) + { + unsigned int* text = _glfw_realloc(preedit->text, + sizeof(unsigned int) * textBufferCount); + if (!text) + return; + + preedit->text = text; + preedit->textBufferCount = textBufferCount; + } + + while (blockBufferCount < blockCount) + blockBufferCount = blockBufferCount ? blockBufferCount * 2 : 8; + + if (blockBufferCount != preedit->blockSizesBufferCount) + { + int* blocks = _glfw_realloc(preedit->blockSizes, + sizeof(int) * blockBufferCount); + if (!blocks) + return; + + preedit->blockSizes = blocks; + preedit->blockSizesBufferCount = blockBufferCount; + } +} + +static void hostUpdatePreedit(void* handle, + const char* utf8, + int caret, + const int* blockSizes, + int blockCount, + int focusedBlock) +{ + _GLFWwindow* window = handle; + _GLFWpreedit* preedit; + const char* c; + int count = 0; + + if (!window || !utf8) + return; + + c = utf8; + while (*c) + { + _glfwDecodeUTF8(&c); + count++; + } + + preedit = &window->preedit; + if (!blockSizes || blockCount <= 0) + blockCount = count ? 1 : 0; + + ensurePreeditBuffers(preedit, count, blockCount); + if (count && (!preedit->text || !preedit->blockSizes)) + return; + + c = utf8; + for (int i = 0; i < count; i++) + preedit->text[i] = _glfwDecodeUTF8(&c); + + if (preedit->text) + preedit->text[count] = 0; + + preedit->textCount = count; + preedit->blockSizesCount = blockCount; + if (blockSizes && blockCount > 0) + { + for (int i = 0; i < blockCount; i++) + preedit->blockSizes[i] = blockSizes[i]; + } + else if (count) + preedit->blockSizes[0] = count; + + if (focusedBlock < 0 || focusedBlock >= blockCount) + focusedBlock = 0; + preedit->focusedBlockIndex = focusedBlock; + preedit->caretIndex = (caret >= 0 && caret <= count) ? caret : count; + + _glfwInputPreedit(window); +} + +static void hostClearPreedit(void* handle) +{ + _GLFWwindow* window = handle; + _GLFWpreedit* preedit; + + if (!window) + return; + + preedit = &window->preedit; + preedit->textCount = 0; + preedit->blockSizesCount = 0; + preedit->focusedBlockIndex = 0; + preedit->caretIndex = 0; + + _glfwInputPreedit(window); +} + +static void hostStatusChanged(void* handle) +{ + _GLFWwindow* window = handle; + if (window) + _glfwInputIMEStatus(window); +} + +static double hostGetTime(void) +{ + return _glfwPlatformGetTimerValue() / (double) _glfwPlatformGetTimerFrequency(); +} + +static void hostPostEmptyEvent(void) +{ + // IME modules queue callbacks for the main thread and use this to wake + // glfwWaitEvents without exposing their worker-thread file descriptors. + _glfwPostEmptyEventX11(); +} + +GLFWbool _glfwLoadIMEModuleX11(void) +{ + const char* path = getenv("GLFW_IM_MODULE"); + const char* debug = getenv("GLFW_IME_DEBUG"); + GLFWx11IMEHostAPI host; + GLFWx11IMEBackendAPI api; + PFN_glfwGetX11IMEBackend getBackend; + +#if defined(_GLFW_EMBED_IBUS_MODULE) + getBackend = glfwGetX11IMEBackend; +#else + getBackend = NULL; +#endif + + if ((!path || !*path) && !getBackend) + return GLFW_FALSE; + + _glfw.x11.imeModule.debug = debug && *debug && strcmp(debug, "0") != 0; + + if (path && *path) + { + _glfw.x11.imeModule.handle = _glfwPlatformLoadModule(path); + if (!_glfw.x11.imeModule.handle) + { + _glfwInputError(GLFW_PLATFORM_ERROR, + "X11: Failed to load IME module %s", path); + return GLFW_FALSE; + } + + getBackend = (PFN_glfwGetX11IMEBackend) + _glfwPlatformGetModuleSymbol(_glfw.x11.imeModule.handle, + "glfwGetX11IMEBackend"); + if (!getBackend) + { + _glfwInputError(GLFW_PLATFORM_ERROR, + "X11: IME module does not export glfwGetX11IMEBackend"); + _glfwPlatformFreeModule(_glfw.x11.imeModule.handle); + memset(&_glfw.x11.imeModule, 0, sizeof(_glfw.x11.imeModule)); + return GLFW_FALSE; + } + + if (_glfw.x11.imeModule.debug) + fprintf(stderr, "GLFW IME: loaded external module %s\n", path); + } +#if defined(_GLFW_EMBED_IBUS_MODULE) + else if (_glfw.x11.imeModule.debug) + fprintf(stderr, "GLFW IME: using embedded IBus module\n"); +#endif + + memset(&host, 0, sizeof(host)); + host.commit_text = hostCommitText; + host.update_preedit = hostUpdatePreedit; + host.clear_preedit = hostClearPreedit; + host.status_changed = hostStatusChanged; + host.get_time = hostGetTime; + host.post_empty_event = hostPostEmptyEvent; + host.log = hostLog; + + memset(&api, 0, sizeof(api)); + if (!getBackend(GLFW_X11_IME_MODULE_ABI_VERSION, &host, &api) || + !api.create || !api.destroy || !api.process_key || !api.drain_events) + { + _glfwInputError(GLFW_PLATFORM_ERROR, + "X11: IME module rejected ABI version %i", + GLFW_X11_IME_MODULE_ABI_VERSION); + if (_glfw.x11.imeModule.handle) + _glfwPlatformFreeModule(_glfw.x11.imeModule.handle); + memset(&_glfw.x11.imeModule, 0, sizeof(_glfw.x11.imeModule)); + return GLFW_FALSE; + } + + _glfw.x11.imeModule.api = api; + _glfw.x11.imeModule.backend = api.create(&host); + if (!_glfw.x11.imeModule.backend) + { + _glfwInputError(GLFW_PLATFORM_ERROR, + "X11: IME module failed to create backend"); + if (_glfw.x11.imeModule.handle) + _glfwPlatformFreeModule(_glfw.x11.imeModule.handle); + memset(&_glfw.x11.imeModule, 0, sizeof(_glfw.x11.imeModule)); + return GLFW_FALSE; + } + + return GLFW_TRUE; +} + +void _glfwUnloadIMEModuleX11(void) +{ + if (_glfw.x11.imeModule.backend && _glfw.x11.imeModule.api.destroy) + _glfw.x11.imeModule.api.destroy(_glfw.x11.imeModule.backend); + + if (_glfw.x11.imeModule.handle) + _glfwPlatformFreeModule(_glfw.x11.imeModule.handle); + + memset(&_glfw.x11.imeModule, 0, sizeof(_glfw.x11.imeModule)); +} + +GLFWbool _glfwHasIMEModuleX11(void) +{ + return _glfw.x11.imeModule.backend != NULL; +} + +void _glfwDrainIMEModuleX11(void) +{ + if (_glfwHasIMEModuleX11()) + _glfw.x11.imeModule.api.drain_events(_glfw.x11.imeModule.backend); +} + +static void sendCachedCursorRect(_GLFWwindow* window, const char* reason) +{ + if (!_glfwHasIMEModuleX11() || + !_glfw.x11.imeModule.api.set_cursor_rect) + { + return; + } + + if (!window->x11.imeCursorRectValid) + { + if (_glfw.x11.imeModule.debug) + { + fprintf(stderr, + "GLFW IME: skip SetCursorLocation reason=%s valid=0 pending=1 original=(%i,%i %ix%i)\n", + reason, + window->preedit.cursorPosX, + window->preedit.cursorPosY, + window->preedit.cursorWidth, + window->preedit.cursorHeight); + } + window->x11.imeCursorRectPending = GLFW_TRUE; + return; + } + + if (!window->x11.imeCursorRectSent && _glfw.x11.imeModule.debug) + { + fprintf(stderr, + "GLFW IME: first SetCursorLocation reason=%s original=(%i,%i %ix%i) window_root=(%i,%i) final=(%i,%i %ix%i)\n", + reason, + window->x11.imeCursorX, + window->x11.imeCursorY, + window->x11.imeCursorWidth, + window->x11.imeCursorHeight, + window->x11.imeWindowRootX, + window->x11.imeWindowRootY, + window->x11.imeCursorRootX, + window->x11.imeCursorRootY, + window->x11.imeCursorWidth, + window->x11.imeCursorHeight); + } + + _glfw.x11.imeModule.api.set_cursor_rect(_glfw.x11.imeModule.backend, + window, + window->x11.imeCursorRootX, + window->x11.imeCursorRootY, + window->x11.imeCursorWidth, + window->x11.imeCursorHeight); + + window->x11.imeCursorRectSent = GLFW_TRUE; + window->x11.imeCursorRectPending = GLFW_FALSE; +} + +void _glfwRefreshCursorRectIMEModuleX11(_GLFWwindow* window, const char* reason) +{ + Window child; + XWindowAttributes attributes; + int glfwX = 0; + int glfwY = 0; + int windowRootX = 0; + int windowRootY = 0; + int cursorRootX = window->preedit.cursorPosX; + int cursorRootY = window->preedit.cursorPosY; + const int localX = window->preedit.cursorPosX; + const int localY = window->preedit.cursorPosY; + const int localW = window->preedit.cursorWidth; + const int localH = window->preedit.cursorHeight; + const Bool windowTranslated = + XTranslateCoordinates(_glfw.x11.display, + window->x11.handle, + _glfw.x11.root, + 0, 0, + &windowRootX, &windowRootY, + &child); + const Bool cursorTranslated = + XTranslateCoordinates(_glfw.x11.display, + window->x11.handle, + _glfw.x11.root, + localX, localY, + &cursorRootX, &cursorRootY, + &child); + const Status attributesStatus = + XGetWindowAttributes(_glfw.x11.display, window->x11.handle, &attributes); + const int screenWidth = DisplayWidth(_glfw.x11.display, _glfw.x11.screen); + const int screenHeight = DisplayHeight(_glfw.x11.display, _glfw.x11.screen); + const GLFWbool onScreen = + cursorRootX > -screenWidth && + cursorRootY > -screenHeight && + cursorRootX < screenWidth * 2 && + cursorRootY < screenHeight * 2; + const GLFWbool looksInitialized = + window->x11.imeNormalKeySeen || + windowRootX != 0 || + windowRootY != 0 || + window->x11.imeCursorRectRetries > 0; + const GLFWbool plausible = + windowTranslated && + cursorTranslated && + attributesStatus && + attributes.map_state == IsViewable && + onScreen && + looksInitialized; + + _glfwGetWindowPosX11(window, &glfwX, &glfwY); + + window->x11.imeCursorX = localX; + window->x11.imeCursorY = localY; + window->x11.imeCursorWidth = localW; + window->x11.imeCursorHeight = localH; + window->x11.imeWindowRootX = windowRootX; + window->x11.imeWindowRootY = windowRootY; + window->x11.imeCursorRootX = cursorRootX; + window->x11.imeCursorRootY = cursorRootY; + window->x11.imeCursorRectValid = plausible; + window->x11.imeCursorRectPending = !plausible; + + if (_glfw.x11.imeModule.debug) + { + fprintf(stderr, + "GLFW IME: cursor translate reason=%s source=0x%lx root=0x%lx window_ret=%i cursor_ret=%i window_root=(%i,%i) cursor_root=(%i,%i) glfw_pos=(%i,%i) local=(%i,%i %ix%i) map_state=%i normal_key_seen=%i retries=%i plausible=%i\n", + reason, + (unsigned long) window->x11.handle, + (unsigned long) _glfw.x11.root, + windowTranslated ? 1 : 0, + cursorTranslated ? 1 : 0, + windowRootX, windowRootY, + cursorRootX, cursorRootY, + glfwX, glfwY, + localX, localY, localW, localH, + attributesStatus ? attributes.map_state : -1, + window->x11.imeNormalKeySeen ? 1 : 0, + window->x11.imeCursorRectRetries, + plausible ? 1 : 0); + } + + if (!plausible && window->x11.imeCursorRectRetries < 2) + { + window->x11.imeCursorRectRetries++; + _glfwPostEmptyEventX11(); + } +} + +void _glfwRefreshPendingCursorRectsIMEModuleX11(const char* reason) +{ + if (!_glfwHasIMEModuleX11()) + return; + + for (_GLFWwindow* window = _glfw.windowListHead; window; window = window->next) + { + if (window->x11.imeCursorRectPending) + { + _glfwRefreshCursorRectIMEModuleX11(window, reason); + sendCachedCursorRect(window, reason); + } + } +} + +void _glfwNotifyNormalKeyIMEModuleX11(_GLFWwindow* window) +{ + if (!_glfwHasIMEModuleX11()) + return; + + window->x11.imeNormalKeySeen = GLFW_TRUE; + if (window->x11.imeCursorRectPending) + { + _glfwRefreshCursorRectIMEModuleX11(window, "after-normal-key"); + sendCachedCursorRect(window, "after-normal-key"); + } +} + +GLFWbool _glfwProcessKeyIMEModuleX11(_GLFWwindow* window, + unsigned int keycode, + unsigned int keysym, + unsigned int state, + int action, + int mods, + unsigned long time) +{ + GLFWx11IMEKeyEvent event; + GLFWx11IMEKeyResult result; + + if (!_glfwHasIMEModuleX11()) + return GLFW_FALSE; + + _glfwRefreshCursorRectIMEModuleX11(window, "before-key"); + sendCachedCursorRect(window, "before-key"); + + memset(&event, 0, sizeof(event)); + event.window = window; + event.x11_window = window->x11.handle; + event.keycode = keycode; + event.keysym = keysym; + event.state = state; + event.action = action; + event.mods = mods; + event.time = time; + event.cursor_rect_valid = window->x11.imeCursorRectValid; + event.cursor_x = window->x11.imeCursorX; + event.cursor_y = window->x11.imeCursorY; + event.cursor_width = window->x11.imeCursorWidth; + event.cursor_height = window->x11.imeCursorHeight; + event.cursor_root_x = window->x11.imeCursorRootX; + event.cursor_root_y = window->x11.imeCursorRootY; + + memset(&result, 0, sizeof(result)); + if (!_glfw.x11.imeModule.api.process_key(_glfw.x11.imeModule.backend, + &event, &result)) + { + return GLFW_FALSE; + } + + _glfwDrainIMEModuleX11(); + return result.handled ? GLFW_TRUE : GLFW_FALSE; +} + +void _glfwFocusInIMEModuleX11(_GLFWwindow* window) +{ + if (_glfwHasIMEModuleX11() && _glfw.x11.imeModule.api.focus_in) + { + window->x11.imeLogNextKey = GLFW_TRUE; + _glfw.x11.imeModule.api.focus_in(_glfw.x11.imeModule.backend, + window, window->x11.handle); + _glfwRefreshCursorRectIMEModuleX11(window, "focus-in"); + sendCachedCursorRect(window, "focus-in"); + } +} + +void _glfwFocusOutIMEModuleX11(_GLFWwindow* window) +{ + if (_glfwHasIMEModuleX11() && _glfw.x11.imeModule.api.focus_out) + _glfw.x11.imeModule.api.focus_out(_glfw.x11.imeModule.backend, + window, window->x11.handle); +} + +void _glfwSetCursorRectIMEModuleX11(_GLFWwindow* window, int x, int y, int w, int h) +{ + if (_glfwHasIMEModuleX11() && _glfw.x11.imeModule.api.set_cursor_rect) + { + // Applications provide client-area coordinates via the public IME API. + // The IBus X11 panel needs root-window coordinates for candidate + // placement, so the conversion stays local to this experimental path. + window->preedit.cursorPosX = x; + window->preedit.cursorPosY = y; + window->preedit.cursorWidth = w; + window->preedit.cursorHeight = h; + _glfwRefreshCursorRectIMEModuleX11(window, "cursor-update"); + sendCachedCursorRect(window, "cursor-update"); + } +} + +void _glfwResetIMEModuleX11(_GLFWwindow* window) +{ + if (_glfwHasIMEModuleX11() && _glfw.x11.imeModule.api.reset) + _glfw.x11.imeModule.api.reset(_glfw.x11.imeModule.backend, window); +} + +void _glfwSetStatusIMEModuleX11(_GLFWwindow* window, int active) +{ + if (_glfwHasIMEModuleX11() && _glfw.x11.imeModule.api.set_status) + _glfw.x11.imeModule.api.set_status(_glfw.x11.imeModule.backend, + window, active); +} + +int _glfwGetStatusIMEModuleX11(_GLFWwindow* window) +{ + if (_glfwHasIMEModuleX11() && _glfw.x11.imeModule.api.get_status) + return _glfw.x11.imeModule.api.get_status(_glfw.x11.imeModule.backend, window); + + return GLFW_FALSE; +} + +#endif // _GLFW_X11 diff --git a/src/x11_ime_module.h b/src/x11_ime_module.h new file mode 100644 index 0000000000..c0da57e296 --- /dev/null +++ b/src/x11_ime_module.h @@ -0,0 +1,102 @@ +//======================================================================== +// GLFW 3.5 X11 IME module prototype - www.glfw.org +//------------------------------------------------------------------------ +// This is an experimental internal ABI for dynamically loaded X11 IME +// modules. It is intentionally not part of the public GLFW API. +//======================================================================== + +#ifndef _glfw3_x11_ime_module_h_ +#define _glfw3_x11_ime_module_h_ + +#define GLFW_X11_IME_MODULE_ABI_VERSION 2 + +typedef struct GLFWx11IMEBackend GLFWx11IMEBackend; + +typedef struct GLFWx11IMEKeyEvent +{ + void* window; + unsigned long x11_window; + unsigned int keycode; + unsigned int keysym; + unsigned int state; + int action; + int mods; + unsigned long time; + int cursor_rect_valid; + int cursor_x, cursor_y, cursor_width, cursor_height; + int cursor_root_x, cursor_root_y; +} GLFWx11IMEKeyEvent; + +typedef struct GLFWx11IMEKeyResult +{ + int handled; + int timed_out; + double elapsed_ms; + unsigned long request_id; +} GLFWx11IMEKeyResult; + +typedef struct GLFWx11IMEHostAPI +{ + void (*commit_text)(void* window, const char* utf8); + void (*update_preedit)(void* window, + const char* utf8, + int caret, + const int* block_sizes, + int block_count, + int focused_block); + void (*clear_preedit)(void* window); + void (*status_changed)(void* window); + double (*get_time)(void); + void (*post_empty_event)(void); + void (*log)(const char* message); +} GLFWx11IMEHostAPI; + +typedef struct GLFWx11IMEBackendAPI +{ + GLFWx11IMEBackend* (*create)(const GLFWx11IMEHostAPI* host); + void (*destroy)(GLFWx11IMEBackend* backend); + void (*focus_in)(GLFWx11IMEBackend* backend, void* window, unsigned long x11_window); + void (*focus_out)(GLFWx11IMEBackend* backend, void* window, unsigned long x11_window); + void (*set_cursor_rect)(GLFWx11IMEBackend* backend, void* window, int x, int y, int w, int h); + void (*reset)(GLFWx11IMEBackend* backend, void* window); + int (*process_key)(GLFWx11IMEBackend* backend, + const GLFWx11IMEKeyEvent* event, + GLFWx11IMEKeyResult* result); + int (*get_status)(GLFWx11IMEBackend* backend, void* window); + void (*set_status)(GLFWx11IMEBackend* backend, void* window, int enabled); + void (*drain_events)(GLFWx11IMEBackend* backend); +} GLFWx11IMEBackendAPI; + +typedef int (* PFN_glfwGetX11IMEBackend)(int,const GLFWx11IMEHostAPI*,GLFWx11IMEBackendAPI*); + +/* + * The experimental IME module ABI currently references _GLFWwindow in GLFW's + * internal X11 helpers, so we need the internal declaration here. + * + * A future public/stable module ABI should avoid exposing internal GLFW types + * and pass the required state explicitly instead. + */ +#include "internal.h" + +int _glfwLoadIMEModuleX11(void); +void _glfwUnloadIMEModuleX11(void); +int _glfwHasIMEModuleX11(void); +void _glfwDrainIMEModuleX11(void); +int _glfwProcessKeyIMEModuleX11(_GLFWwindow* window, + unsigned int keycode, + unsigned int keysym, + unsigned int state, + int action, + int mods, + unsigned long time); +void _glfwFocusInIMEModuleX11(_GLFWwindow* window); +void _glfwFocusOutIMEModuleX11(_GLFWwindow* window); +void _glfwSetCursorRectIMEModuleX11(_GLFWwindow* window, int x, int y, int w, int h); +void _glfwRefreshCursorRectIMEModuleX11(_GLFWwindow* window, const char* reason); +void _glfwRefreshPendingCursorRectsIMEModuleX11(const char* reason); +void _glfwNotifyNormalKeyIMEModuleX11(_GLFWwindow* window); +void _glfwResetIMEModuleX11(_GLFWwindow* window); +void _glfwSetStatusIMEModuleX11(_GLFWwindow* window, int active); +int _glfwGetStatusIMEModuleX11(_GLFWwindow* window); + +#endif // _glfw3_x11_ime_module_h_ diff --git a/src/x11_init.c b/src/x11_init.c index d2f0da0f93..c4ca12a3c8 100644 --- a/src/x11_init.c +++ b/src/x11_init.c @@ -1190,6 +1190,7 @@ GLFWbool _glfwConnectX11(int platformID, _GLFWplatform* platform) .getClipboardString = _glfwGetClipboardStringX11, .updatePreeditCursorRectangle = _glfwUpdatePreeditCursorRectangleX11, .resetPreeditText = _glfwResetPreeditTextX11, + .setTextInputFocus = _glfwSetTextInputFocusX11, .setIMEStatus = _glfwSetIMEStatusX11, .getIMEStatus = _glfwGetIMEStatusX11, #if defined(GLFW_BUILD_LINUX_JOYSTICK) @@ -1552,7 +1553,9 @@ int _glfwInitX11(void) _glfw.x11.helperWindowHandle = createHelperWindow(); _glfw.x11.hiddenCursorHandle = createHiddenCursor(); - if (XSupportsLocale() && _glfw.x11.xlib.utf8) + _glfwLoadIMEModuleX11(); + + if (!_glfwHasIMEModuleX11() && XSupportsLocale() && _glfw.x11.xlib.utf8) { XSetLocaleModifiers(""); @@ -1590,10 +1593,13 @@ void _glfwTerminateX11(void) _glfw_free(_glfw.x11.primarySelectionString); _glfw_free(_glfw.x11.clipboardString); - XUnregisterIMInstantiateCallback(_glfw.x11.display, - NULL, NULL, NULL, - inputMethodInstantiateCallback, - NULL); + if (!_glfwHasIMEModuleX11()) + { + XUnregisterIMInstantiateCallback(_glfw.x11.display, + NULL, NULL, NULL, + inputMethodInstantiateCallback, + NULL); + } if (_glfw.x11.im) { @@ -1601,6 +1607,8 @@ void _glfwTerminateX11(void) _glfw.x11.im = NULL; } + _glfwUnloadIMEModuleX11(); + if (_glfw.x11.display) { XCloseDisplay(_glfw.x11.display); @@ -1633,4 +1641,3 @@ void _glfwTerminateX11(void) } #endif // _GLFW_X11 - diff --git a/src/x11_platform.h b/src/x11_platform.h index 4d6693133d..5643f178f4 100644 --- a/src/x11_platform.h +++ b/src/x11_platform.h @@ -50,6 +50,8 @@ // The Shape extension provides custom window shapes #include +#include "x11_ime_module.h" + #define GLX_VENDOR 1 #define GLX_RGBA_BIT 0x00000001 #define GLX_WINDOW_BIT 0x00000001 @@ -565,6 +567,15 @@ typedef struct _GLFWwindowX11 XIMCallback statusDrawCallback; int imeFocus; + GLFWbool imeCursorRectValid; + GLFWbool imeCursorRectSent; + GLFWbool imeLogNextKey; + GLFWbool imeNormalKeySeen; + GLFWbool imeCursorRectPending; + int imeCursorRectRetries; + int imeCursorX, imeCursorY, imeCursorWidth, imeCursorHeight; + int imeWindowRootX, imeWindowRootY; + int imeCursorRootX, imeCursorRootY; } _GLFWwindowX11; // X11-specific global data @@ -898,6 +909,13 @@ typedef struct _GLFWlibraryX11 PFN_XShapeQueryVersion QueryVersion; PFN_XShapeCombineMask ShapeCombineMask; } xshape; + + struct { + void* handle; + GLFWx11IMEBackend* backend; + GLFWx11IMEBackendAPI api; + GLFWbool debug; + } imeModule; } _GLFWlibraryX11; // X11-specific per-monitor data @@ -981,6 +999,7 @@ const char* _glfwGetClipboardStringX11(void); void _glfwUpdatePreeditCursorRectangleX11(_GLFWwindow* window); void _glfwResetPreeditTextX11(_GLFWwindow* window); +void _glfwSetTextInputFocusX11(_GLFWwindow* window, GLFWbool focused); void _glfwSetIMEStatusX11(_GLFWwindow* window, int active); int _glfwGetIMEStatusX11(_GLFWwindow* window); @@ -1032,4 +1051,3 @@ GLFWbool _glfwChooseVisualGLX(const _GLFWwndconfig* wndconfig, const _GLFWctxconfig* ctxconfig, const _GLFWfbconfig* fbconfig, Visual** visual, int* depth); - diff --git a/src/x11_window.c b/src/x11_window.c index dc6d3e5681..4c93a80f6e 100644 --- a/src/x11_window.c +++ b/src/x11_window.c @@ -244,6 +244,16 @@ static int translateKey(int scancode) return _glfw.x11.keycodes[scancode]; } +static KeySym getKeySym(XKeyEvent* event, int keycode) +{ + if (_glfw.x11.xkb.available) + return XkbKeycodeToKeysym(_glfw.x11.display, keycode, _glfw.x11.xkb.group, 0); + + KeySym keysym = NoSymbol; + XLookupString(event, NULL, 0, &keysym, NULL); + return keysym; +} + // Sends an EWMH or ICCCM event to the window manager // static void sendEventToWM(_GLFWwindow* window, Atom type, @@ -1357,12 +1367,18 @@ static void processEvent(XEvent *event) { int keycode = 0; Bool filtered = False; + const GLFWbool imeModuleActive = _glfwHasIMEModuleX11(); // HACK: Save scancode as some IMs clear the field in XFilterEvent if (event->type == KeyPress || event->type == KeyRelease) keycode = event->xkey.keycode; - filtered = XFilterEvent(event, None); + if (!imeModuleActive) + { + filtered = XFilterEvent(event, None); + if (filtered) + return; + } if (_glfw.x11.randr.available) { @@ -1455,6 +1471,24 @@ static void processEvent(XEvent *event) const int key = translateKey(keycode); const int mods = translateState(event->xkey.state); const int plain = !(mods & (GLFW_MOD_CONTROL | GLFW_MOD_ALT)); + const KeySym keysym = getKeySym(&event->xkey, keycode); + const GLFWbool textInputFocused = + !window->textInputFocusExplicit || window->textInputFocus; + GLFWbool moduleHandled = GLFW_FALSE; + + if (imeModuleActive && textInputFocused) + { + moduleHandled = + _glfwProcessKeyIMEModuleX11(window, keycode, (unsigned int) keysym, + event->xkey.state, GLFW_PRESS, mods, + event->xkey.time); + } + + if (imeModuleActive && window->x11.imeLogNextKey) + window->x11.imeLogNextKey = GLFW_FALSE; + + if (moduleHandled) + return; if (window->x11.ic) { @@ -1519,6 +1553,9 @@ static void processEvent(XEvent *event) _glfwInputChar(window, codepoint, mods, plain); } + if (imeModuleActive) + _glfwNotifyNormalKeyIMEModuleX11(window); + return; } @@ -1526,6 +1563,7 @@ static void processEvent(XEvent *event) { const int key = translateKey(keycode); const int mods = translateState(event->xkey.state); + const KeySym keysym = getKeySym(&event->xkey, keycode); if (!_glfw.x11.xkb.detectable) { @@ -1559,6 +1597,14 @@ static void processEvent(XEvent *event) } } + if (_glfwProcessKeyIMEModuleX11(window, keycode, (unsigned int) keysym, + event->xkey.state, GLFW_RELEASE, mods, + event->xkey.time)) + { + _glfwInputKey(window, key, keycode, GLFW_RELEASE, mods); + return; + } + _glfwInputKey(window, key, keycode, GLFW_RELEASE, mods); return; } @@ -1961,8 +2007,13 @@ static void processEvent(XEvent *event) else if (window->cursorMode == GLFW_CURSOR_CAPTURED) captureCursor(window); - if (window->x11.ic) + if (window->x11.ic && + (!window->textInputFocusExplicit || window->textInputFocus)) + { XSetICFocus(window->x11.ic); + } + else if (!window->textInputFocusExplicit || window->textInputFocus) + _glfwFocusInIMEModuleX11(window); _glfwInputWindowFocus(window, GLFW_TRUE); return; @@ -1985,6 +2036,8 @@ static void processEvent(XEvent *event) if (window->x11.ic) XUnsetICFocus(window->x11.ic); + else + _glfwFocusOutIMEModuleX11(window); if (window->monitor && window->autoIconify) _glfwIconifyWindowX11(window); @@ -2270,6 +2323,9 @@ GLFWbool _glfwCreateWindowX11(_GLFWwindow* window, if (wndconfig->mousePassthrough) _glfwSetWindowMousePassthroughX11(window, GLFW_TRUE); + if (_glfwHasIMEModuleX11()) + window->x11.imeLogNextKey = GLFW_TRUE; + if (window->monitor) { _glfwShowWindowX11(window); @@ -2306,6 +2362,8 @@ void _glfwDestroyWindowX11(_GLFWwindow* window) XDestroyIC(window->x11.ic); window->x11.ic = NULL; } + else + _glfwFocusOutIMEModuleX11(window); if (window->context.destroy) window->context.destroy(window); @@ -3053,6 +3111,8 @@ GLFWbool _glfwRawMouseMotionSupportedX11(void) void _glfwPollEventsX11(void) { drainEmptyEvents(); + _glfwRefreshPendingCursorRectsIMEModuleX11("event-drain"); + _glfwDrainIMEModuleX11(); #if defined(GLFW_BUILD_LINUX_JOYSTICK) if (_glfw.joysticksInitialized) @@ -3065,6 +3125,8 @@ void _glfwPollEventsX11(void) XEvent event; XNextEvent(_glfw.x11.display, &event); processEvent(&event); + _glfwRefreshPendingCursorRectsIMEModuleX11("after-event"); + _glfwDrainIMEModuleX11(); } _GLFWwindow* window = _glfw.x11.disabledCursorWindow; @@ -3083,6 +3145,8 @@ void _glfwPollEventsX11(void) } XFlush(_glfw.x11.display); + _glfwRefreshPendingCursorRectsIMEModuleX11("poll-end"); + _glfwDrainIMEModuleX11(); } void _glfwWaitEventsX11(void) @@ -3363,6 +3427,16 @@ void _glfwUpdatePreeditCursorRectangleX11(_GLFWwindow* window) XPoint spot; _GLFWpreedit* preedit = &window->preedit; + if (_glfwHasIMEModuleX11()) + { + _glfwSetCursorRectIMEModuleX11(window, + preedit->cursorPosX, + preedit->cursorPosY, + preedit->cursorWidth, + preedit->cursorHeight); + return; + } + if (!window->x11.ic) return; @@ -3383,6 +3457,12 @@ void _glfwResetPreeditTextX11(_GLFWwindow* window) XVaNestedList preedit_attr; char* result; + if (_glfwHasIMEModuleX11()) + { + _glfwResetIMEModuleX11(window); + return; + } + if (!ic) return; @@ -3416,6 +3496,12 @@ void _glfwSetIMEStatusX11(_GLFWwindow* window, int active) { XIC ic = window->x11.ic; + if (_glfwHasIMEModuleX11()) + { + _glfwSetStatusIMEModuleX11(window, active); + return; + } + if (!ic) return; @@ -3429,8 +3515,45 @@ void _glfwSetIMEStatusX11(_GLFWwindow* window, int active) XUnsetICFocus(ic); } +void _glfwSetTextInputFocusX11(_GLFWwindow* window, GLFWbool focused) +{ + XIC ic = window->x11.ic; + + if (_glfwHasIMEModuleX11()) + { + if (focused) + { + if (_glfwWindowFocusedX11(window)) + _glfwFocusInIMEModuleX11(window); + } + else + { + _glfwResetPreeditTextX11(window); + _glfwFocusOutIMEModuleX11(window); + } + + return; + } + + if (!ic) + return; + + // This API reflects the text input focus abstraction discussed in the + // clear-code/glfw#5 and clear-code/glfw#7 IME support work. + if (focused) + XSetICFocus(ic); + else + { + _glfwResetPreeditTextX11(window); + XUnsetICFocus(ic); + } +} + int _glfwGetIMEStatusX11(_GLFWwindow* window) { + if (_glfwHasIMEModuleX11()) + return _glfwGetStatusIMEModuleX11(window); + if (!window->x11.ic) return GLFW_FALSE; @@ -3715,4 +3838,3 @@ GLFWAPI const char* glfwGetX11SelectionString(void) } #endif // _GLFW_X11 - diff --git a/tests/input_text.c b/tests/input_text.c index cc006f2532..a315db28cf 100644 --- a/tests/input_text.c +++ b/tests/input_text.c @@ -112,6 +112,14 @@ static int fontNum = 0; static int currentFontIndex = 0; static int currentIMEStatus = GLFW_FALSE; +enum text_input_focus_status +{ + TEXT_INPUT_FOCUS_COMPATIBLE, + TEXT_INPUT_FOCUS_IN, + TEXT_INPUT_FOCUS_OUT +}; +static enum text_input_focus_status currentTextInputFocusStatus = + TEXT_INPUT_FOCUS_COMPATIBLE; #define MAX_PREEDIT_LEN 128 static char preeditBuf[MAX_PREEDIT_LEN] = ""; @@ -510,13 +518,27 @@ static int set_font_selecter(GLFWwindow* window, struct nk_context* nk, int heig static void set_ime_buttons(GLFWwindow* window, struct nk_context* nk, int height) { - nk_layout_row_dynamic(nk, height, 2); + nk_layout_row_dynamic(nk, height, 3); if (nk_button_label(nk, "Toggle IME status")) { glfwSetInputMode(window, GLFW_IME, !currentIMEStatus); } + if (nk_button_label(nk, "Toggle text input focus")) + { + if (currentTextInputFocusStatus == TEXT_INPUT_FOCUS_IN) + { + glfwSetTextInputFocus(window, GLFW_FALSE); + currentTextInputFocusStatus = TEXT_INPUT_FOCUS_OUT; + } + else + { + glfwSetTextInputFocus(window, GLFW_TRUE); + currentTextInputFocusStatus = TEXT_INPUT_FOCUS_IN; + } + } + if (nk_button_label(nk, "Reset preedit text")) { glfwResetPreeditText(window); @@ -615,8 +637,23 @@ static void set_preedit_cursor_edit(GLFWwindow* window, struct nk_context* nk, i static void set_ime_stauts_labels(GLFWwindow* window, struct nk_context* nk, int height) { - nk_layout_row_dynamic(nk, height, 1); + const char* textInputFocusStatus = "Text input focus: compatible"; + + switch (currentTextInputFocusStatus) + { + case TEXT_INPUT_FOCUS_IN: + textInputFocusStatus = "Text input focus: Focus In"; + break; + case TEXT_INPUT_FOCUS_OUT: + textInputFocusStatus = "Text input focus: Focus Out"; + break; + case TEXT_INPUT_FOCUS_COMPATIBLE: + break; + } + + nk_layout_row_dynamic(nk, height, 2); nk_value_bool(nk, "IME status", currentIMEStatus); + nk_label(nk, textInputFocusStatus, NK_TEXT_LEFT); } static void set_preedit_labels(GLFWwindow* window, struct nk_context* nk, int height)