diff --git a/CMakeLists.txt b/CMakeLists.txt index 063533c4ed..89af4530d3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,10 +11,21 @@ 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_INFER_TEXT_INPUT_FOCUS + "Infer text input focus from legacy IME and cursor input state" OFF) include(GNUInstallDirs) include(CMakeDependentOption) +set(GLFW_IME_MODULE_INSTALL_DIR "${CMAKE_INSTALL_LIBDIR}/glfw" CACHE PATH + "Install directory for experimental X11 IME modules") +if (IS_ABSOLUTE "${GLFW_IME_MODULE_INSTALL_DIR}") + set(GLFW_IME_MODULE_INSTALL_FULL_DIR "${GLFW_IME_MODULE_INSTALL_DIR}") +else() + set(GLFW_IME_MODULE_INSTALL_FULL_DIR + "${CMAKE_INSTALL_PREFIX}/${GLFW_IME_MODULE_INSTALL_DIR}") +endif() + if (GLFW_USE_OSMESA) message(FATAL_ERROR "GLFW_USE_OSMESA has been removed; set the GLFW_PLATFORM init hint") endif() @@ -28,6 +39,12 @@ 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_INSTALL_IME_MODULES + "Install experimental X11 IME modules" + OFF "UNIX;NOT APPLE;GLFW_BUILD_X11;GLFW_INSTALL" OFF) cmake_dependent_option(GLFW_USE_HYBRID_HPG "Force use of high-performance GPU on hybrid systems" OFF "WIN32" OFF) @@ -133,4 +150,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..1e08687a17 --- /dev/null +++ b/docs/ime-ibus-prototype.md @@ -0,0 +1,434 @@ +# 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. + +The standalone IME modules are not installed by default. Configure with +`-DGLFW_INSTALL_IME_MODULES=ON` to install `glfw-ibus.so` and +`glfw-fcitx5.so`. The default install location is +`${CMAKE_INSTALL_LIBDIR}/glfw` relative to the install prefix, typically +`lib/glfw` under that prefix. For example, with the default `/usr/local` +prefix this is usually `/usr/local/lib/glfw`. + +When `GLFW_IM_MODULE` does not contain a `/`, GLFW also searches this default +module directory. The `glfw-` prefix and platform module suffix may be omitted, +so `GLFW_IM_MODULE=ibus` and `GLFW_IM_MODULE=fcitx5` load the installed +`glfw-ibus.so` and `glfw-fcitx5.so` modules. Full paths continue to be loaded +as specified. + +### 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 +- passes the X event keysym, including modifier-translated printable keysyms, + to IBus `ProcessKeyEvent` +- 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. A separate native Fcitx5 experiment is built as +`glfw-fcitx5.so` from the same private X11 IME module ABI. + +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 + +### Native Fcitx5 Experiment + +The native Fcitx5 module is a sibling of the IBus module. It is loaded with the +same runtime mechanism: + +```sh +GLFW_IM_MODULE=/path/to/glfw-fcitx5.so ./application +``` + +It keeps Fcitx5 and D-Bus protocol knowledge inside the module. GLFW core still +only sees the private X11 IME backend ABI. + +The module connects to Fcitx5 through the session bus name `org.fcitx.Fcitx5`, +creates an input context with `org.fcitx.Fcitx.InputMethod1.CreateInputContext`, +then talks to the returned `org.fcitx.Fcitx.InputContext1` object. It currently +uses: + +- `SetCapability` +- `FocusIn` +- `FocusOut` +- `Reset` +- `SetCursorRect` +- `ProcessKeyEvent` +- `CommitString` +- `UpdateFormattedPreedit` +- `NotifyFocusOut` + +The first version intentionally maps Fcitx5 formatted preedit segments directly +to GLFW preedit blocks. This preserves useful segmentation without exposing +Fcitx5 formatting values to GLFW core. + +The module treats a non-zero formatted-preedit segment format as the focused +GLFW preedit block. If no formatted segment is marked, it falls back to using +the reported caret position. + +Unlike the IBus module, the Fcitx5 module sends the X11 keycode received from +GLFW directly to `ProcessKeyEvent`. Fcitx5 appears to interpret this field as +an XKB keycode. Using the IBus-style `keycode - 8` conversion caused obvious +layout mismatches, such as the Enter key being interpreted as `t`. + +The module retries connection to Fcitx5 when startup, input context creation or +key processing fails. While disconnected, focus state is retained, key events +are reported as not handled, and the worker retries connection periodically. + +Native Fcitx5 status control is intentionally left as future work. The current +prototype keeps status behavior minimal and focuses on commit, preedit, +candidate positioning and key processing. + +### 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 can infer text input focus from +legacy IME and cursor input state. Set `GLFW_INFER_TEXT_INPUT_FOCUS` to a +non-zero value, or build GLFW with `GLFW_INFER_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. + +When this compatibility mode is active, GLFW also treats cursor mode changes as +text input focus hints. The requested text input focus state is kept separately +from the effective platform focus, so cursor-disabled gameplay can suppress IME +routing while cursor-normal text entry can re-enable it. This helps existing +applications that switch between gameplay and text entry with cursor mode only. + +`glfwGetInputMode(window, GLFW_IME)` still returns the native platform IME +status until the remapping path has initialized text input focus state for the +window. After that, it returns the application-requested text input focus +state, not the cursor-mode-masked effective platform focus. 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/window.md b/docs/window.md index 2140f0975b..6f80e6d3d9 100644 --- a/docs/window.md +++ b/docs/window.md @@ -277,6 +277,12 @@ either or both of these hints are set to `GLFW_ANY_POSITION` then the window manager will position the window where it thinks the user will prefer it. Possible values are any valid screen coordinates and `GLFW_ANY_POSITION`. +@anchor GLFW_SOFT_FULLSCREEN_hint +__GLFW_SOFT_FULLSCREEN__ specifies whether full screen windows should avoid +exclusive full screen behavior where supported. This can allow system UI such +as input method windows, notifications or overlays to appear above a full screen +window. Possible values are `GLFW_TRUE` and `GLFW_FALSE`. + #### Framebuffer related hints {#window_hints_fb} @@ -548,6 +554,7 @@ GLFW_SCALE_FRAMEBUFFER | `GLFW_TRUE` | `GLFW_TRUE` or `GL GLFW_MOUSE_PASSTHROUGH | `GLFW_FALSE` | `GLFW_TRUE` or `GLFW_FALSE` GLFW_POSITION_X | `GLFW_ANY_POSITION` | Any valid screen x-coordinate or `GLFW_ANY_POSITION` GLFW_POSITION_Y | `GLFW_ANY_POSITION` | Any valid screen y-coordinate or `GLFW_ANY_POSITION` +GLFW_SOFT_FULLSCREEN | `GLFW_FALSE` | `GLFW_TRUE` or `GLFW_FALSE` GLFW_RED_BITS | 8 | 0 to `INT_MAX` or `GLFW_DONT_CARE` GLFW_GREEN_BITS | 8 | 0 to `INT_MAX` or `GLFW_DONT_CARE` GLFW_BLUE_BITS | 8 | 0 to `INT_MAX` or `GLFW_DONT_CARE` diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h index 06ba3dde4b..d62479d75b 100644 --- a/include/GLFW/glfw3.h +++ b/include/GLFW/glfw3.h @@ -941,6 +941,12 @@ extern "C" { */ #define GLFW_POSITION_Y 0x0002000F +/*! @brief Soft full screen window hint. + * + * Soft full screen [window hint](@ref GLFW_SOFT_FULLSCREEN_hint). + */ +#define GLFW_SOFT_FULLSCREEN 0x00020010 + /*! @brief Framebuffer bit depth hint. * * Framebuffer bit depth [hint](@ref GLFW_RED_BITS). diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bcd3871d6d..b57794d81e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -46,7 +46,11 @@ endif() if (GLFW_BUILD_X11) target_compile_definitions(glfw PRIVATE _GLFW_X11) - target_sources(glfw PRIVATE x11_platform.h x11_init.c + target_compile_definitions(glfw PRIVATE + "_GLFW_X11_IME_MODULE_DIR=\"${GLFW_IME_MODULE_INSTALL_FULL_DIR}\"" + "_GLFW_X11_IME_MODULE_SUFFIX=\"${CMAKE_SHARED_MODULE_SUFFIX}\"") + 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 +144,10 @@ target_include_directories(glfw PRIVATE "${GLFW_BINARY_DIR}/src") target_link_libraries(glfw PRIVATE Threads::Threads) +if (GLFW_INFER_TEXT_INPUT_FOCUS) + target_compile_definitions(glfw PRIVATE _GLFW_INFER_TEXT_INPUT_FOCUS) +endif() + if (GLFW_BUILD_WIN32) list(APPEND glfw_PKG_LIBS "-lgdi32") endif() @@ -213,6 +221,67 @@ 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 (GLFW_INSTALL_IME_MODULES AND NOT DBUS1_FOUND) + message(FATAL_ERROR "D-Bus development package is required for installing X11 IME modules") + 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}) + + if (GLFW_INSTALL_IME_MODULES) + install(TARGETS glfw-ibus + LIBRARY DESTINATION "${GLFW_IME_MODULE_INSTALL_DIR}") + endif() + + add_library(glfw-fcitx5 MODULE x11_fcitx5_module.c x11_ime_module.h) + set_target_properties(glfw-fcitx5 PROPERTIES + PREFIX "" + C_STANDARD 99 + C_EXTENSIONS OFF + FOLDER "GLFW3") + target_include_directories(glfw-fcitx5 PRIVATE + "${GLFW_SOURCE_DIR}/src" + "${GLFW_SOURCE_DIR}/include" + ${DBUS1_INCLUDE_DIRS}) + target_link_libraries(glfw-fcitx5 PRIVATE Threads::Threads ${DBUS1_LIBRARIES}) + target_compile_options(glfw-fcitx5 PRIVATE ${DBUS1_CFLAGS_OTHER}) + target_link_directories(glfw-fcitx5 PRIVATE ${DBUS1_LIBRARY_DIRS}) + + if (GLFW_INSTALL_IME_MODULES) + install(TARGETS glfw-fcitx5 + LIBRARY DESTINATION "${GLFW_IME_MODULE_INSTALL_DIR}") + endif() + else() + message(STATUS "D-Bus development package not found; skipping X11 IME prototype modules") + endif() + endif() endif() if (UNIX AND NOT APPLE) @@ -345,4 +414,3 @@ if (GLFW_INSTALL) ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}") endif() - diff --git a/src/cocoa_window.m b/src/cocoa_window.m index 81b3549530..45b5e8aa4f 100644 --- a/src/cocoa_window.m +++ b/src/cocoa_window.m @@ -38,6 +38,35 @@ // having been (according to documentation) added in Mac OS X 10.7 #define NSWindowCollectionBehaviorFullScreenNone (1 << 9) +// Returns whether the window is in macOS native full screen +// +static GLFWbool isSoftFullscreen(_GLFWwindow* window) +{ + return [window->ns.object styleMask] & NSWindowStyleMaskFullScreen; +} + +// Set macOS native full screen without switching monitor video modes +// +static void setSoftFullscreen(_GLFWwindow* window, GLFWbool enabled) +{ + if (isSoftFullscreen(window) == enabled) + return; + + NSUInteger styleMask = [window->ns.object styleMask]; + + // Native full screen requires a resizable window style + styleMask |= NSWindowStyleMaskResizable; + [window->ns.object setStyleMask:styleMask]; + + [window->ns.object toggleFullScreen:nil]; + + if (!window->resizable) + { + styleMask &= ~NSWindowStyleMaskResizable; + [window->ns.object setStyleMask:styleMask]; + } +} + // Returns whether the cursor is in the content area of the specified window // static GLFWbool cursorInContentArea(_GLFWwindow* window) @@ -1062,6 +1091,13 @@ GLFWbool _glfwCreateWindowCocoa(_GLFWwindow* window, { @autoreleasepool { + GLFWbool softFullscreen = GLFW_FALSE; + if (_glfw.hints.window.softFullscreen && window->monitor) + { + softFullscreen = GLFW_TRUE; + _glfwInputWindowMonitor(window, NULL); + } + if (!createNativeWindow(window, wndconfig, fbconfig)) return GLFW_FALSE; @@ -1126,6 +1162,9 @@ GLFWbool _glfwCreateWindowCocoa(_GLFWwindow* window, name:NSTextInputContextKeyboardSelectionDidChangeNotification object:nil]; + if (softFullscreen) + setSoftFullscreen(window, GLFW_TRUE); + return GLFW_TRUE; } // autoreleasepool @@ -1394,6 +1433,14 @@ void _glfwSetWindowMonitorCocoa(_GLFWwindow* window, { @autoreleasepool { + if (_glfw.hints.window.softFullscreen) + { + setSoftFullscreen(window, monitor != NULL); + + if (monitor) + return; + } + if (window->monitor == monitor) { if (monitor) diff --git a/src/input.c b/src/input.c index 96fa01c76c..5bfeffef21 100644 --- a/src/input.c +++ b/src/input.c @@ -593,6 +593,31 @@ void _glfwCenterCursorInContentArea(_GLFWwindow* window) _glfw.platform.setCursorPos(window, width / 2.0, height / 2.0); } +GLFWbool _glfwInferTextInputFocus(void) +{ + const char* value = getenv("GLFW_INFER_TEXT_INPUT_FOCUS"); + + if (value && *value) + return strcmp(value, "0") != 0; + +#if defined(_GLFW_INFER_TEXT_INPUT_FOCUS) + return GLFW_TRUE; +#else + return GLFW_FALSE; +#endif +} + +static GLFWbool getEffectiveTextInputFocus(_GLFWwindow* window) +{ + if (_glfwInferTextInputFocus()) + { + return window->textInputFocusRequested && + window->cursorMode == GLFW_CURSOR_NORMAL; + } + + return window->textInputFocusRequested; +} + ////////////////////////////////////////////////////////////////////////// ////// GLFW public API ////// @@ -620,6 +645,12 @@ GLFWAPI int glfwGetInputMode(GLFWwindow* handle, int mode) case GLFW_UNLIMITED_MOUSE_BUTTONS: return window->disableMouseButtonLimit; case GLFW_IME: + if (_glfwInferTextInputFocus() && + window->textInputFocusInitialized) + { + return window->textInputFocusRequested; + } + return _glfw.platform.getIMEStatus(window); } @@ -658,6 +689,23 @@ GLFWAPI void glfwSetInputMode(GLFWwindow* handle, int mode, int value) &window->virtualCursorPosX, &window->virtualCursorPosY); _glfw.platform.setCursorMode(window, value); + if (_glfwInferTextInputFocus()) + { + if (!window->textInputFocusInitialized) + { + window->textInputFocusInitialized = GLFW_TRUE; + window->textInputFocusRequested = GLFW_TRUE; + window->textInputFocus = GLFW_TRUE; + } + + const GLFWbool focused = getEffectiveTextInputFocus(window); + + if (window->textInputFocus != focused) + { + window->textInputFocus = focused; + _glfw.platform.setTextInputFocus(window, focused); + } + } return; } @@ -737,6 +785,16 @@ GLFWAPI void glfwSetInputMode(GLFWwindow* handle, int mode, int value) case GLFW_IME: { + if (_glfwInferTextInputFocus()) + { + value = value ? GLFW_TRUE : GLFW_FALSE; + window->textInputFocusInitialized = GLFW_TRUE; + window->textInputFocusRequested = value; + window->textInputFocus = getEffectiveTextInputFocus(window); + _glfw.platform.setTextInputFocus(window, window->textInputFocus); + return; + } + _glfw.platform.setIMEStatus(window, value ? GLFW_TRUE : GLFW_FALSE); return; } @@ -1049,8 +1107,9 @@ GLFWAPI void glfwSetTextInputFocus(GLFWwindow* handle, int focused) focused = focused ? GLFW_TRUE : GLFW_FALSE; window->textInputFocusInitialized = GLFW_TRUE; - window->textInputFocus = focused; - _glfw.platform.setTextInputFocus(window, focused); + window->textInputFocusRequested = focused; + window->textInputFocus = getEffectiveTextInputFocus(window); + _glfw.platform.setTextInputFocus(window, window->textInputFocus); } GLFWAPI unsigned int* glfwGetPreeditCandidate(GLFWwindow* handle, int index, int* textCount) diff --git a/src/internal.h b/src/internal.h index 57b43315d7..b2474fcc9c 100644 --- a/src/internal.h +++ b/src/internal.h @@ -419,6 +419,7 @@ struct _GLFWwndconfig bool mousePassthrough; bool scaleToMonitor; bool scaleFramebuffer; + bool softFullscreen; struct { char frameName[256]; } ns; @@ -596,6 +597,7 @@ struct _GLFWwindow // Preserve legacy text input behavior for backward compatibility until // glfwSetTextInputFocus is used for this window. GLFWbool textInputFocusInitialized; + GLFWbool textInputFocusRequested; GLFWbool textInputFocus; int cursorMode; @@ -1018,6 +1020,8 @@ void _glfwInputError(int code, const char* format, ...); GLFWbool _glfwSelectPlatform(int platformID, _GLFWplatform* platform); +GLFWbool _glfwInferTextInputFocus(void); + GLFWbool _glfwStringInExtensionString(const char* string, const char* extensions); const _GLFWfbconfig* _glfwChooseFBConfig(const _GLFWfbconfig* desired, const _GLFWfbconfig* alternatives, diff --git a/src/win32_window.c b/src/win32_window.c index c9ea008781..3a57bfc9a1 100644 --- a/src/win32_window.c +++ b/src/win32_window.c @@ -467,7 +467,9 @@ static void fitToMonitor(_GLFWwindow* window) mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left, - mi.rcMonitor.bottom - mi.rcMonitor.top, + _glfw.hints.window.softFullscreen ? + mi.rcMonitor.bottom - mi.rcMonitor.top + 1 : + mi.rcMonitor.bottom - mi.rcMonitor.top, SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOCOPYBITS); } @@ -2309,7 +2311,9 @@ void _glfwSetWindowMonitorWin32(_GLFWwindow* window, mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left, - mi.rcMonitor.bottom - mi.rcMonitor.top, + _glfw.hints.window.softFullscreen ? + mi.rcMonitor.bottom - mi.rcMonitor.top + 1 : + mi.rcMonitor.bottom - mi.rcMonitor.top, flags); } else diff --git a/src/window.c b/src/window.c index 68e0c597f9..495198a67b 100644 --- a/src/window.c +++ b/src/window.c @@ -280,6 +280,7 @@ void glfwDefaultWindowHints(void) _glfw.hints.window.xpos = GLFW_ANY_POSITION; _glfw.hints.window.ypos = GLFW_ANY_POSITION; _glfw.hints.window.scaleFramebuffer = true; + _glfw.hints.window.softFullscreen = false; // The default is 24 bits of color, 24 bits of depth and 8 bits of stencil, // double buffered @@ -402,6 +403,9 @@ GLFWAPI void glfwWindowHint(int hint, int value) case GLFW_MOUSE_PASSTHROUGH: _glfw.hints.window.mousePassthrough = value; return; + case GLFW_SOFT_FULLSCREEN: + _glfw.hints.window.softFullscreen = value; + return; case GLFW_CLIENT_API: _glfw.hints.context.client = value; return; diff --git a/src/x11_fcitx5_module.c b/src/x11_fcitx5_module.c new file mode 100644 index 0000000000..85e110ece7 --- /dev/null +++ b/src/x11_fcitx5_module.c @@ -0,0 +1,1404 @@ +#define _POSIX_C_SOURCE 200809L + +//======================================================================== +// GLFW X11 Fcitx5 IME module prototype +//======================================================================== + +#include "x11_ime_module.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static const char* FCITX5_SERVICE = "org.fcitx.Fcitx5"; +static const char* FCITX5_PATH = "/org/freedesktop/portal/inputmethod"; +static const char* FCITX5_INTERFACE = "org.fcitx.Fcitx.InputMethod1"; +static const char* FCITX5_INPUT_INTERFACE = "org.fcitx.Fcitx.InputContext1"; + +#define FCITX5_CAP_PREEDIT (1ULL << 1) +#define FCITX5_CAP_FORMATTED_PREEDIT (1ULL << 4) +#define FCITX5_CAP_CLIENT_UNFOCUS_COMMIT (1ULL << 5) +#define FCITX5_CAP_KEY_EVENT_ORDER_FIX (1ULL << 37) + +enum +{ + FCITX5_SHIFT_MASK = 1 << 0, + FCITX5_LOCK_MASK = 1 << 1, + FCITX5_CONTROL_MASK = 1 << 2, + FCITX5_MOD1_MASK = 1 << 3, + FCITX5_MOD2_MASK = 1 << 4, + FCITX5_MOD4_MASK = 1 << 6 +}; + +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 PreeditInfo +{ + char* text; + int caret; + 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; + double reconnect_at; + double reconnect_interval; + 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; + int filter_added; +}; + +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-fcitx5: %s\n", buffer); +} + +static uint32_t fcitx5_state_from_glfw(unsigned int mods) +{ + uint32_t state = 0; + + if (mods & GLFW_MOD_SHIFT) + state |= FCITX5_SHIFT_MASK; + if (mods & GLFW_MOD_CAPS_LOCK) + state |= FCITX5_LOCK_MASK; + if (mods & GLFW_MOD_CONTROL) + state |= FCITX5_CONTROL_MASK; + if (mods & GLFW_MOD_ALT) + state |= FCITX5_MOD1_MASK; + if (mods & GLFW_MOD_NUM_LOCK) + state |= FCITX5_MOD2_MASK; + if (mods & GLFW_MOD_SUPER) + state |= FCITX5_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); + + // Fcitx5 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(FCITX5_SERVICE, + backend->input_context_path, + FCITX5_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 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_fcitx5_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 int append_text(char** buffer, size_t* length, size_t* capacity, const char* text) +{ + size_t text_length; + char* next; + + if (!text) + text = ""; + + text_length = strlen(text); + if (*length + text_length + 1 > *capacity) + { + size_t next_capacity = *capacity ? *capacity * 2 : 32; + while (*length + text_length + 1 > next_capacity) + next_capacity *= 2; + + next = realloc(*buffer, next_capacity); + if (!next) + return GLFW_FALSE; + + *buffer = next; + *capacity = next_capacity; + } + + memcpy(*buffer + *length, text, text_length); + *length += text_length; + (*buffer)[*length] = '\0'; + return GLFW_TRUE; +} + +static int append_preedit_block_size(PreeditInfo* info, int size) +{ + int* block_sizes; + + if (size <= 0) + return GLFW_TRUE; + + block_sizes = realloc(info->block_sizes, + sizeof(int) * (size_t) (info->block_count + 1)); + if (!block_sizes) + return GLFW_FALSE; + + info->block_sizes = block_sizes; + info->block_sizes[info->block_count++] = size; + return GLFW_TRUE; +} + +static int parse_fcitx5_formatted_preedit(GLFWx11IMEBackend* backend, + DBusMessage* message, + PreeditInfo* info) +{ + DBusMessageIter iter, array; + size_t length = 0; + size_t capacity = 0; + int char_count = 0; + int segment_index = 0; + int focused_segment = -1; + + memset(info, 0, sizeof(*info)); + info->caret = -1; + + if (!dbus_message_iter_init(message, &iter) || + dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) + { + return GLFW_FALSE; + } + + dbus_message_iter_recurse(&iter, &array); + while (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_INVALID) + { + DBusMessageIter structure; + const char* segment = NULL; + dbus_int32_t format = 0; + int segment_count; + + if (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_STRUCT) + { + free(info->text); + free(info->block_sizes); + info->text = NULL; + info->block_sizes = NULL; + return GLFW_FALSE; + } + + dbus_message_iter_recurse(&array, &structure); + if (dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_STRING) + { + free(info->text); + free(info->block_sizes); + info->text = NULL; + info->block_sizes = NULL; + return GLFW_FALSE; + } + + dbus_message_iter_get_basic(&structure, &segment); + + if (!dbus_message_iter_next(&structure) || + dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_INT32) + { + free(info->text); + free(info->block_sizes); + info->text = NULL; + info->block_sizes = NULL; + return GLFW_FALSE; + } + + dbus_message_iter_get_basic(&structure, &format); + segment_count = utf8_count(segment); + + log_line(backend, + "preedit segment index=%i chars=%i format=%i text='%s'", + segment_index, segment_count, format, segment ? segment : ""); + + if (format != 0 && focused_segment < 0) + focused_segment = segment_index; + + if (!append_text(&info->text, &length, &capacity, segment) || + !append_preedit_block_size(info, segment_count)) + { + free(info->text); + free(info->block_sizes); + info->text = NULL; + info->block_sizes = NULL; + return GLFW_FALSE; + } + + char_count += segment_count; + segment_index++; + dbus_message_iter_next(&array); + } + + if (!info->text) + info->text = xstrdup(""); + + if (dbus_message_iter_next(&iter) && + dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_INT32) + { + dbus_int32_t caret = 0; + dbus_message_iter_get_basic(&iter, &caret); + info->caret = normalize_fcitx5_index(info->text, char_count, caret); + } + + if (info->caret < 0) + info->caret = char_count; + + if (info->block_count <= 0 && char_count > 0) + append_preedit_block_size(info, char_count); + + if (focused_segment >= 0 && focused_segment < info->block_count) + info->focused_block = focused_segment; + else + { + for (int i = 0, offset = 0; i < info->block_count; i++) + { + const int next = offset + info->block_sizes[i]; + if (info->caret >= offset && (info->caret < next || i == info->block_count - 1)) + { + info->focused_block = i; + break; + } + offset = next; + } + } + + return GLFW_TRUE; +} + +static int parse_create_input_context_reply(DBusMessage* reply, + const char** path) +{ + DBusMessageIter iter; + + *path = NULL; + + if (!dbus_message_iter_init(reply, &iter) || + dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_OBJECT_PATH) + { + return GLFW_FALSE; + } + + dbus_message_iter_get_basic(&iter, path); + return *path && **path; +} + +static DBusHandlerResult dbus_filter(DBusConnection* connection, + DBusMessage* message, + void* data) +{ + GLFWx11IMEBackend* backend = data; + PreeditInfo info; + (void) connection; + + if (dbus_message_is_signal(message, FCITX5_INPUT_INTERFACE, "CommitString")) + { + const char* text = NULL; + DBusError error; + + dbus_error_init(&error); + if (dbus_message_get_args(message, &error, + DBUS_TYPE_STRING, &text, + DBUS_TYPE_INVALID)) + { + enqueue_event(backend, EVENT_COMMIT, NULL, text ? text : "", -1, + NULL, 0, 0); + } + dbus_error_free(&error); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_is_signal(message, FCITX5_INPUT_INTERFACE, "UpdateFormattedPreedit")) + { + if (parse_fcitx5_formatted_preedit(backend, message, &info)) + { + if (info.text && *info.text) + { + 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.text); + free(info.block_sizes); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_is_signal(message, FCITX5_INPUT_INTERFACE, "NotifyFocusOut")) + { + enqueue_event(backend, EVENT_CLEAR_PREEDIT, NULL, NULL, -1, NULL, 0, 0); + return DBUS_HANDLER_RESULT_HANDLED; + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static void disconnect_fcitx5(GLFWx11IMEBackend* backend, const char* reason) +{ + if (backend->connection || backend->input_context_path || backend->ready) + log_line(backend, "disconnecting from Fcitx5 reason=%s", reason ? reason : ""); + + backend->ready = GLFW_FALSE; + backend->active_request_id = 0; + backend->active_window = NULL; + + if (backend->connection) + { + if (backend->filter_added) + dbus_connection_remove_filter(backend->connection, dbus_filter, backend); + dbus_connection_close(backend->connection); + dbus_connection_unref(backend->connection); + backend->connection = NULL; + } + + free(backend->input_context_path); + backend->input_context_path = NULL; + backend->filter_added = GLFW_FALSE; + backend->reconnect_at = now_seconds(backend) + backend->reconnect_interval; +} + +static void complete_failed_request(GLFWx11IMEBackend* backend, Request* request) +{ + if (!request) + return; + + pthread_mutex_lock(&backend->mutex); + request->completed = GLFW_TRUE; + request->failed = GLFW_TRUE; + request->handled = GLFW_FALSE; + request->completed_at = now_seconds(backend); + pthread_cond_broadcast(&backend->cond); + pthread_mutex_unlock(&backend->mutex); + + release_request(backend, request); +} + +static int connect_fcitx5(GLFWx11IMEBackend* backend) +{ + DBusError error; + DBusMessage* message; + DBusMessage* reply; + const char* path = NULL; + dbus_uint64_t caps = FCITX5_CAP_PREEDIT | + FCITX5_CAP_FORMATTED_PREEDIT | + FCITX5_CAP_CLIENT_UNFOCUS_COMMIT | + FCITX5_CAP_KEY_EVENT_ORDER_FIX; + + log_line(backend, "connecting to Fcitx5 on session bus"); + + dbus_error_init(&error); + backend->connection = dbus_bus_get_private(DBUS_BUS_SESSION, &error); + if (!backend->connection) + { + log_line(backend, "failed to open session bus: %s", error.message ? error.message : ""); + dbus_error_free(&error); + backend->reconnect_at = now_seconds(backend) + backend->reconnect_interval; + return GLFW_FALSE; + } + + dbus_connection_set_exit_on_disconnect(backend->connection, FALSE); + + log_line(backend, "creating Fcitx5 input context"); + + message = dbus_message_new_method_call(FCITX5_SERVICE, FCITX5_PATH, + FCITX5_INTERFACE, "CreateInputContext"); + if (!message) + { + disconnect_fcitx5(backend, "CreateInputContext message allocation failed"); + return GLFW_FALSE; + } + + { + DBusMessageIter iter, array, structure; + const char* program_key = "program"; + const char* program_value = "GLFW research prototype"; + const char* display_key = "display"; + char display_value[256]; + const char* display_value_string = display_value; + + snprintf(display_value, sizeof(display_value), "x11:%s", + getenv("DISPLAY") ? getenv("DISPLAY") : ""); + + dbus_message_iter_init_append(message, &iter); + if (!dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "(ss)", &array)) + { + dbus_message_unref(message); + disconnect_fcitx5(backend, "CreateInputContext argument open failed"); + return GLFW_FALSE; + } + + if (!dbus_message_iter_open_container(&array, DBUS_TYPE_STRUCT, NULL, &structure) || + !dbus_message_iter_append_basic(&structure, DBUS_TYPE_STRING, &program_key) || + !dbus_message_iter_append_basic(&structure, DBUS_TYPE_STRING, &program_value) || + !dbus_message_iter_close_container(&array, &structure) || + !dbus_message_iter_open_container(&array, DBUS_TYPE_STRUCT, NULL, &structure) || + !dbus_message_iter_append_basic(&structure, DBUS_TYPE_STRING, &display_key) || + !dbus_message_iter_append_basic(&structure, DBUS_TYPE_STRING, &display_value_string) || + !dbus_message_iter_close_container(&array, &structure)) + { + dbus_message_unref(message); + disconnect_fcitx5(backend, "CreateInputContext argument append failed"); + return GLFW_FALSE; + } + + dbus_message_iter_close_container(&iter, &array); + } + + 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); + disconnect_fcitx5(backend, "CreateInputContext failed"); + return GLFW_FALSE; + } + + if (!parse_create_input_context_reply(reply, &path)) + { + log_line(backend, "CreateInputContext reply parse failed"); + dbus_message_unref(reply); + disconnect_fcitx5(backend, "CreateInputContext reply parse failed"); + return GLFW_FALSE; + } + + backend->input_context_path = xstrdup(path); + dbus_message_unref(reply); + if (!backend->input_context_path) + { + disconnect_fcitx5(backend, "input context path allocation failed"); + return GLFW_FALSE; + } + + log_line(backend, "created Fcitx5 input context path=%s", + backend->input_context_path ? backend->input_context_path : ""); + + dbus_error_init(&error); + dbus_bus_add_match(backend->connection, + "type='signal',interface='org.fcitx.Fcitx.InputContext1'", + &error); + if (dbus_error_is_set(&error)) + { + log_line(backend, "failed to add Fcitx5 signal match: %s", + error.message ? error.message : ""); + dbus_error_free(&error); + disconnect_fcitx5(backend, "signal match failed"); + return GLFW_FALSE; + } + + if (!dbus_connection_add_filter(backend->connection, dbus_filter, backend, NULL)) + { + disconnect_fcitx5(backend, "signal filter failed"); + return GLFW_FALSE; + } + backend->filter_added = GLFW_TRUE; + log_line(backend, "setting Fcitx5 capabilities=0x%llx", + (unsigned long long) caps); + if (!call_no_reply(backend, "SetCapability", + DBUS_TYPE_UINT64, &caps, + DBUS_TYPE_INVALID)) + { + disconnect_fcitx5(backend, "SetCapability failed"); + return GLFW_FALSE; + } + backend->ready = GLFW_TRUE; + backend->status = GLFW_TRUE; + backend->reconnect_at = 0.0; + + log_line(backend, "connected to Fcitx5 path=%s", + backend->input_context_path ? backend->input_context_path : ""); + + if (backend->focused_window && + !call_no_reply(backend, "FocusIn", DBUS_TYPE_INVALID)) + { + disconnect_fcitx5(backend, "restored FocusIn failed"); + return GLFW_FALSE; + } + + 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 x11_keycode = request->event.keycode; + dbus_uint32_t keycode = x11_keycode; + dbus_uint32_t state = fcitx5_state_from_glfw(request->event.mods); + dbus_bool_t is_release = request->event.action == GLFW_RELEASE; + dbus_uint32_t time = request->event.time; + int disconnect_after_request = GLFW_FALSE; + + 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 x11_keycode=%u fcitx5_keycode=%u state=0x%x release=%i", + request->id, request->event.time, request->queued_at, + keyval, x11_keycode, keycode, state, is_release); + + 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, "SetCursorRect", + 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(FCITX5_SERVICE, + backend->input_context_path, + FCITX5_INPUT_INTERFACE, + "ProcessKeyEvent"); + if (message && + dbus_message_append_args(message, + DBUS_TYPE_UINT32, &keyval, + DBUS_TYPE_UINT32, &keycode, + DBUS_TYPE_UINT32, &state, + DBUS_TYPE_BOOLEAN, &is_release, + DBUS_TYPE_UINT32, &time, + 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; + disconnect_after_request = GLFW_TRUE; + } + } + else + { + request->failed = GLFW_TRUE; + disconnect_after_request = 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; + disconnect_after_request = 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); + + if (disconnect_after_request) + disconnect_fcitx5(backend, "ProcessKeyEvent failed"); +} + +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) + { + } + + if (!dbus_connection_get_is_connected(backend->connection)) + disconnect_fcitx5(backend, "D-Bus connection disconnected"); +} + +static void handle_unready_command(GLFWx11IMEBackend* backend, Command* command) +{ + switch (command->type) + { + case COMMAND_FOCUS_IN: + backend->focused_window = command->window; + break; + case COMMAND_FOCUS_OUT: + if (backend->focused_window == command->window) + backend->focused_window = NULL; + break; + case COMMAND_SET_STATUS: + backend->status = command->status; + break; + case COMMAND_KEY: + complete_failed_request(backend, command->request); + command->request = NULL; + break; + default: + break; + } +} + +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. + for (;;) + { + Command* command = NULL; + + if (backend->running && !backend->ready) + { + const double now = now_seconds(backend); + if (backend->reconnect_at <= 0.0 || now >= backend->reconnect_at) + connect_fcitx5(backend); + } + + 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) + { + handle_unready_command(backend, command); + 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, + "SetCursorRect final=(%i,%i %ix%i)", + command->x, command->y, command->w, command->h); + call_no_reply(backend, "SetCursorRect", + 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); + } + + disconnect_fcitx5(backend, "worker shutdown"); + 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->status = GLFW_TRUE; + backend->timeout_ms = 100.0; + backend->reconnect_interval = 1.0; + timeout = getenv("GLFW_FCITX5_TIMEOUT_MS"); + if (timeout && *timeout) + backend->timeout_ms = atof(timeout); + + dbus_threads_init_default(); + + 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->block_sizes); + 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_ibus_module.c b/src/x11_ibus_module.c new file mode 100644 index 0000000000..8591e359c3 --- /dev/null +++ b/src/x11_ibus_module.c @@ -0,0 +1,1546 @@ +#define _POSIX_C_SOURCE 200809L + +//======================================================================== +// GLFW X11 IBus IME module prototype +//======================================================================== + +#include "x11_ime_module.h" + +#include +#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_file(const char* path, char* buffer, size_t size) +{ + FILE* 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 read_ibus_address_from_bus_dir(const char* directory, + const char* machine_id, + char* buffer, + size_t size) +{ + DIR* dir; + struct dirent* entry; + const size_t machine_id_length = strlen(machine_id); + + dir = opendir(directory); + if (!dir) + return GLFW_FALSE; + + while ((entry = readdir(dir))) + { + char* path; + size_t path_size; + + if (strncmp(entry->d_name, machine_id, machine_id_length) != 0 || + entry->d_name[machine_id_length] != '-') + { + continue; + } + + path_size = strlen(directory) + 1 + strlen(entry->d_name) + 1; + path = malloc(path_size); + if (!path) + continue; + + snprintf(path, path_size, "%s/%s", directory, entry->d_name); + if (read_ibus_address_file(path, buffer, size)) + { + free(path); + closedir(dir); + return GLFW_TRUE; + } + + free(path); + } + + closedir(dir); + return GLFW_FALSE; +} + +static int read_ibus_address(char* buffer, size_t size) +{ + const char* address = getenv("IBUS_ADDRESS"); + char bus_dir[4096]; + char display[128]; + const char* config; + const char* home; + char* machine_id; + DBusError error; + + 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(bus_dir, sizeof(bus_dir), "%s/ibus/bus", config); + else if (home && *home) + snprintf(bus_dir, sizeof(bus_dir), "%s/.config/ibus/bus", home); + else + { + dbus_free(machine_id); + return GLFW_FALSE; + } + + { + char* path; + size_t path_size = strlen(bus_dir) + 1 + strlen(machine_id) + 1 + + strlen(host) + 1 + strlen(number) + 1; + + path = malloc(path_size); + if (!path) + { + dbus_free(machine_id); + return GLFW_FALSE; + } + + snprintf(path, path_size, "%s/%s-%s-%s", bus_dir, machine_id, host, number); + if (read_ibus_address_file(path, buffer, size)) + { + free(path); + dbus_free(machine_id); + return GLFW_TRUE; + } + + free(path); + } + + if (read_ibus_address_from_bus_dir(bus_dir, machine_id, buffer, size)) + { + dbus_free(machine_id); + return GLFW_TRUE; + } + + dbus_free(machine_id); + 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 x11_keycode = request->event.keycode; + dbus_uint32_t keycode = x11_keycode >= 8 ? x11_keycode - 8 : x11_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 x11_keycode=%u ibus_keycode=%u state=0x%x", + request->id, request->event.time, request->queued_at, + keyval, x11_keycode, 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..2cccb95643 --- /dev/null +++ b/src/x11_ime_module.c @@ -0,0 +1,672 @@ +//======================================================================== +// 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 + +#if !defined(_GLFW_X11_IME_MODULE_DIR) +#define _GLFW_X11_IME_MODULE_DIR "" +#endif + +#if !defined(_GLFW_X11_IME_MODULE_SUFFIX) +#define _GLFW_X11_IME_MODULE_SUFFIX ".so" +#endif + +static void hostLog(const char* message) +{ + if (message && _glfw.x11.imeModule.debug) + fprintf(stderr, "GLFW IME: %s\n", message); +} + +static GLFWbool hasPathSeparator(const char* path) +{ + return strchr(path, '/') != NULL; +} + +static GLFWbool hasSuffix(const char* string, const char* suffix) +{ + const size_t stringLength = strlen(string); + const size_t suffixLength = strlen(suffix); + + if (suffixLength > stringLength) + return GLFW_FALSE; + + return strcmp(string + stringLength - suffixLength, suffix) == 0; +} + +static char* makeIMEModulePath(const char* directory, + const char* prefix, + const char* name, + const char* suffix) +{ + const size_t directoryLength = strlen(directory); + const size_t prefixLength = strlen(prefix); + const size_t nameLength = strlen(name); + const size_t suffixLength = strlen(suffix); + const size_t length = directoryLength + 1 + prefixLength + nameLength + + suffixLength; + char* path = _glfw_calloc(length + 1, 1); + + if (!path) + return NULL; + + snprintf(path, length + 1, "%s/%s%s%s", directory, prefix, name, suffix); + return path; +} + +static void* loadIMEModulePath(const char* path, char** loadedPath) +{ + void* handle = _glfwPlatformLoadModule(path); + + if (!handle) + return NULL; + + *loadedPath = _glfw_strdup(path); + return handle; +} + +static void* loadIMEModuleName(const char* name, char** loadedPath) +{ + void* handle; + const GLFWbool hasModuleSuffix = hasSuffix(name, _GLFW_X11_IME_MODULE_SUFFIX); + const GLFWbool hasModulePrefix = strncmp(name, "glfw-", 5) == 0; + const char* directory = _GLFW_X11_IME_MODULE_DIR; + char* path; + + *loadedPath = NULL; + + if (hasPathSeparator(name)) + return loadIMEModulePath(name, loadedPath); + + handle = loadIMEModulePath(name, loadedPath); + if (handle) + return handle; + + if (!directory[0]) + return NULL; + + path = makeIMEModulePath(directory, "", name, ""); + if (path) + { + handle = loadIMEModulePath(path, loadedPath); + _glfw_free(path); + if (handle) + return handle; + } + + if (!hasModuleSuffix) + { + path = makeIMEModulePath(directory, "", name, + _GLFW_X11_IME_MODULE_SUFFIX); + if (path) + { + handle = loadIMEModulePath(path, loadedPath); + _glfw_free(path); + if (handle) + return handle; + } + } + + if (!hasModulePrefix) + { + path = makeIMEModulePath(directory, "glfw-", name, ""); + if (path) + { + handle = loadIMEModulePath(path, loadedPath); + _glfw_free(path); + if (handle) + return handle; + } + + if (!hasModuleSuffix) + { + path = makeIMEModulePath(directory, "glfw-", name, + _GLFW_X11_IME_MODULE_SUFFIX); + if (path) + { + handle = loadIMEModulePath(path, loadedPath); + _glfw_free(path); + if (handle) + return handle; + } + } + } + + return NULL; +} + +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; + char* loadedPath = NULL; + +#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 = loadIMEModuleName(path, &loadedPath); + 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); + _glfw_free(loadedPath); + 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", + loadedPath ? loadedPath : path); + } + + _glfw_free(loadedPath); + } +#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 737b911cbe..c9f66fd63d 100644 --- a/src/x11_init.c +++ b/src/x11_init.c @@ -1553,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(""); @@ -1591,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) { @@ -1602,6 +1607,8 @@ void _glfwTerminateX11(void) _glfw.x11.im = NULL; } + _glfwUnloadIMEModuleX11(); + if (_glfw.x11.display) { XCloseDisplay(_glfw.x11.display); diff --git a/src/x11_platform.h b/src/x11_platform.h index 293b91264d..415939c2f0 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 diff --git a/src/x11_window.c b/src/x11_window.c index 4f4b3946c2..be21dbc387 100644 --- a/src/x11_window.c +++ b/src/x11_window.c @@ -244,6 +244,27 @@ 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; +} + +static KeySym getIMEKeySym(XKeyEvent* event, int keycode) +{ + KeySym keysym = NoSymbol; + XLookupString(event, NULL, 0, &keysym, NULL); + + if (keysym != NoSymbol) + return keysym; + + return getKeySym(event, keycode); +} + // Sends an EWMH or ICCCM event to the window manager // static void sendEventToWM(_GLFWwindow* window, Atom type, @@ -365,7 +386,8 @@ static void updateWindowMode(_GLFWwindow* window) // Enable compositor bypass if (!window->x11.transparent) { - const unsigned long value = 1; + const unsigned long value = + _glfw.hints.window.softFullscreen ? 2 : 1; XChangeProperty(_glfw.x11.display, window->x11.handle, _glfw.x11.NET_WM_BYPASS_COMPOSITOR, XA_CARDINAL, 32, @@ -1357,12 +1379,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 +1483,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 imeKeysym = getIMEKeySym(&event->xkey, keycode); + const GLFWbool textInputFocused = + !window->textInputFocusInitialized || window->textInputFocus; + GLFWbool moduleHandled = GLFW_FALSE; + + if (imeModuleActive && textInputFocused) + { + moduleHandled = + _glfwProcessKeyIMEModuleX11(window, keycode, (unsigned int) imeKeysym, + 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 +1565,9 @@ static void processEvent(XEvent *event) _glfwInputChar(window, codepoint, mods, plain); } + if (imeModuleActive) + _glfwNotifyNormalKeyIMEModuleX11(window); + return; } @@ -1526,6 +1575,9 @@ static void processEvent(XEvent *event) { const int key = translateKey(keycode); const int mods = translateState(event->xkey.state); + const KeySym imeKeysym = getIMEKeySym(&event->xkey, keycode); + const GLFWbool textInputFocused = + !window->textInputFocusInitialized || window->textInputFocus; if (!_glfw.x11.xkb.detectable) { @@ -1559,6 +1611,15 @@ static void processEvent(XEvent *event) } } + if (imeModuleActive && textInputFocused && + _glfwProcessKeyIMEModuleX11(window, keycode, (unsigned int) imeKeysym, + 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; } @@ -1966,6 +2027,8 @@ static void processEvent(XEvent *event) { XSetICFocus(window->x11.ic); } + else if (!window->textInputFocusInitialized || window->textInputFocus) + _glfwFocusInIMEModuleX11(window); _glfwInputWindowFocus(window, GLFW_TRUE); return; @@ -1988,6 +2051,8 @@ static void processEvent(XEvent *event) if (window->x11.ic) XUnsetICFocus(window->x11.ic); + else + _glfwFocusOutIMEModuleX11(window); if (window->monitor && window->autoIconify) _glfwIconifyWindowX11(window); @@ -2273,6 +2338,9 @@ GLFWbool _glfwCreateWindowX11(_GLFWwindow* window, if (wndconfig->mousePassthrough) _glfwSetWindowMousePassthroughX11(window, GLFW_TRUE); + if (_glfwHasIMEModuleX11()) + window->x11.imeLogNextKey = GLFW_TRUE; + if (window->monitor) { _glfwShowWindowX11(window); @@ -2309,6 +2377,8 @@ void _glfwDestroyWindowX11(_GLFWwindow* window) XDestroyIC(window->x11.ic); window->x11.ic = NULL; } + else + _glfwFocusOutIMEModuleX11(window); if (window->context.destroy) window->context.destroy(window); @@ -3056,6 +3126,8 @@ GLFWbool _glfwRawMouseMotionSupportedX11(void) void _glfwPollEventsX11(void) { drainEmptyEvents(); + _glfwRefreshPendingCursorRectsIMEModuleX11("event-drain"); + _glfwDrainIMEModuleX11(); #if defined(GLFW_BUILD_LINUX_JOYSTICK) if (_glfw.joysticksInitialized) @@ -3068,6 +3140,8 @@ void _glfwPollEventsX11(void) XEvent event; XNextEvent(_glfw.x11.display, &event); processEvent(&event); + _glfwRefreshPendingCursorRectsIMEModuleX11("after-event"); + _glfwDrainIMEModuleX11(); } _GLFWwindow* window = _glfw.x11.disabledCursorWindow; @@ -3086,6 +3160,8 @@ void _glfwPollEventsX11(void) } XFlush(_glfw.x11.display); + _glfwRefreshPendingCursorRectsIMEModuleX11("poll-end"); + _glfwDrainIMEModuleX11(); } void _glfwWaitEventsX11(void) @@ -3366,6 +3442,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; @@ -3386,6 +3472,12 @@ void _glfwResetPreeditTextX11(_GLFWwindow* window) XVaNestedList preedit_attr; char* result; + if (_glfwHasIMEModuleX11()) + { + _glfwResetIMEModuleX11(window); + return; + } + if (!ic) return; @@ -3419,6 +3511,12 @@ void _glfwSetIMEStatusX11(_GLFWwindow* window, int active) { XIC ic = window->x11.ic; + if (_glfwHasIMEModuleX11()) + { + _glfwSetStatusIMEModuleX11(window, active); + return; + } + if (!ic) return; @@ -3436,6 +3534,22 @@ 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; @@ -3450,6 +3564,9 @@ void _glfwSetTextInputFocusX11(_GLFWwindow* window, GLFWbool focused) int _glfwGetIMEStatusX11(_GLFWwindow* window) { + if (_glfwHasIMEModuleX11()) + return _glfwGetStatusIMEModuleX11(window); + if (!window->x11.ic) return GLFW_FALSE;