diff --git a/CMakeLists.txt b/CMakeLists.txt index ce8ffbdd86f..f0768f6c2d4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -511,7 +511,7 @@ Boost uses ICU library (required for linking with static Boost built with libicu endif() # Misc -if(WIN32 OR APPLE) +if(WIN32 OR APPLE OR ((UNIX AND (NOT HAIKU)) AND WITH_GHOST_WAYLAND)) option(WITH_INPUT_IME "Enable Input Method Editor (IME) for complex Asian character input" ON) else() set(WITH_INPUT_IME OFF) diff --git a/intern/ghost/CMakeLists.txt b/intern/ghost/CMakeLists.txt index 9cdff6aa71f..1f1d6de6920 100644 --- a/intern/ghost/CMakeLists.txt +++ b/intern/ghost/CMakeLists.txt @@ -439,6 +439,12 @@ elseif(WITH_GHOST_X11 OR WITH_GHOST_WAYLAND) generate_protocol_bindings( "${WAYLAND_PROTOCOLS_DIR}/unstable/primary-selection/primary-selection-unstable-v1.xml" ) + if(WITH_INPUT_IME) + generate_protocol_bindings( + "${WAYLAND_PROTOCOLS_DIR}/unstable/text-input/text-input-unstable-v3.xml" + ) + add_definitions(-DWITH_INPUT_IME) + endif() unset(INC_DST) diff --git a/intern/ghost/GHOST_Types.h b/intern/ghost/GHOST_Types.h index d4d7dea4d45..79494825bef 100644 --- a/intern/ghost/GHOST_Types.h +++ b/intern/ghost/GHOST_Types.h @@ -119,6 +119,10 @@ typedef enum { * Support for sampling a color outside of the Blender windows. */ GHOST_kCapabilityDesktopSample = (1 << 5), + /** + * Supports IME text input methods (when `WITH_INPUT_IME` is defined). + */ + GHOST_kCapabilityInputIME = (1 << 6), } GHOST_TCapabilityFlag; /** @@ -128,7 +132,7 @@ typedef enum { #define GHOST_CAPABILITY_FLAG_ALL \ (GHOST_kCapabilityCursorWarp | GHOST_kCapabilityWindowPosition | \ GHOST_kCapabilityPrimaryClipboard | GHOST_kCapabilityGPUReadFrontBuffer | \ - GHOST_kCapabilityClipboardImages | GHOST_kCapabilityDesktopSample) + GHOST_kCapabilityClipboardImages | GHOST_kCapabilityDesktopSample | GHOST_kCapabilityInputIME) /* Xtilt and Ytilt represent how much the pen is tilted away from * vertically upright in either the X or Y direction, with X and Y the diff --git a/intern/ghost/intern/GHOST_SystemHeadless.hh b/intern/ghost/intern/GHOST_SystemHeadless.hh index 9c015580346..406c608b0e2 100644 --- a/intern/ghost/intern/GHOST_SystemHeadless.hh +++ b/intern/ghost/intern/GHOST_SystemHeadless.hh @@ -51,7 +51,7 @@ class GHOST_SystemHeadless : public GHOST_System { ~(GHOST_kCapabilityWindowPosition | GHOST_kCapabilityCursorWarp | GHOST_kCapabilityPrimaryClipboard | GHOST_kCapabilityDesktopSample | - GHOST_kCapabilityClipboardImages)); + GHOST_kCapabilityClipboardImages | GHOST_kCapabilityInputIME)); } char *getClipboard(bool /*selection*/) const override { diff --git a/intern/ghost/intern/GHOST_SystemSDL.cc b/intern/ghost/intern/GHOST_SystemSDL.cc index 16613751005..9afcf744b03 100644 --- a/intern/ghost/intern/GHOST_SystemSDL.cc +++ b/intern/ghost/intern/GHOST_SystemSDL.cc @@ -781,7 +781,9 @@ GHOST_TCapabilityFlag GHOST_SystemSDL::getCapabilities() const /* This SDL back-end has not yet implemented color sampling the desktop. */ GHOST_kCapabilityDesktopSample | /* This SDL back-end has not yet implemented image copy/paste. */ - GHOST_kCapabilityClipboardImages)); + GHOST_kCapabilityClipboardImages | + /* No support yet for IME input methods. */ + GHOST_kCapabilityInputIME)); } char *GHOST_SystemSDL::getClipboard(bool /*selection*/) const diff --git a/intern/ghost/intern/GHOST_SystemWayland.cc b/intern/ghost/intern/GHOST_SystemWayland.cc index ea6dca1b908..4e3012c6a97 100644 --- a/intern/ghost/intern/GHOST_SystemWayland.cc +++ b/intern/ghost/intern/GHOST_SystemWayland.cc @@ -68,6 +68,9 @@ #include #include #include +#ifdef WITH_INPUT_IME +# include +#endif /* Decorations `xdg_decor`. */ #include @@ -720,6 +723,51 @@ static void gwl_primary_selection_discard_source(GWL_PrimarySelection *primary) primary->data_source = nullptr; } +#ifdef WITH_INPUT_IME +struct GWL_SeatIME { + struct wl_surface *surface_window = nullptr; + GHOST_TEventImeData event_ime_data = { + /*result_len*/ nullptr, + /*composite_len*/ nullptr, + /*result*/ nullptr, + /*composite*/ nullptr, + /*cursor_position*/ -1, + /*target_start*/ -1, + /*target_end*/ -1, + }; + /** When true, the client has indicated that IME input should be activated on text entry. */ + bool is_enabled = false; + /** + * When true, some pre-edit text has been entered + * (an IME popup may be showing however this isn't known). + */ + bool has_preedit = false; + + /** Storage for #GHOST_TEventImeData::result (the result of the `commit_string` callback). */ + std::string result; + /** Storage for #GHOST_TEventImeData::composite (the result of the `preedit_string` callback). */ + std::string composite; + + /** #zwp_text_input_v3_listener::commit_string was called with a null text argument. */ + bool result_is_null = false; + /** #zwp_text_input_v3_listener::preedit_string was called with a null text argument. */ + bool composite_is_null = false; + + /** #zwp_text_input_v3_listener::preedit_string was called. */ + bool has_preedit_string_callback = false; + /** #zwp_text_input_v3_listener::commit_string was called. */ + bool has_commit_string_callback = false; + + /** Bounds (use for comparison). */ + struct { + int x = -1; + int y = -1; + int w = -1; + int h = -1; + } rect; +}; +#endif /* WITH_INPUT_IME */ + struct GWL_Seat { /** Wayland core types. */ @@ -755,6 +803,10 @@ struct GWL_Seat { /** All currently active tablet tools (needed for changing the cursor). */ std::unordered_set tablet_tools; + +#ifdef WITH_INPUT_IME + struct zwp_text_input_v3 *text_input = nullptr; +#endif } wp; /** XKB native types. */ @@ -780,6 +832,10 @@ struct GWL_Seat { } xkb; +#ifdef WITH_INPUT_IME + GWL_SeatIME ime; +#endif + GHOST_SystemWayland *system = nullptr; std::string name; @@ -914,6 +970,60 @@ static void gwl_seat_key_repeat_timer_remove(GWL_Seat *seat) seat->key_repeat.timer = nullptr; } +#ifdef WITH_INPUT_IME + +static void gwl_seat_ime_full_reset(GWL_Seat *seat) +{ + const GWL_SeatIME ime_default{}; + /* Preserve the following members since they represent the state of the connection to Wayland. + * or which callbacks have run (which shouldn't be reset). */ + wl_surface *surface_window = seat->ime.surface_window; + const bool is_enabled = seat->ime.is_enabled; + const bool has_preedit_string_callback = seat->ime.has_preedit_string_callback; + const bool has_commit_string_callback = seat->ime.has_commit_string_callback; + + seat->ime = ime_default; + + seat->ime.surface_window = surface_window; + seat->ime.is_enabled = is_enabled; + seat->ime.has_preedit_string_callback = has_preedit_string_callback; + seat->ime.has_commit_string_callback = has_commit_string_callback; +} + +static void gwl_seat_ime_result_reset(GWL_Seat *seat) +{ + seat->ime.result.clear(); + seat->ime.result_is_null = false; + + GHOST_TEventImeData &event_ime_data = seat->ime.event_ime_data; + event_ime_data.result_len = nullptr; + event_ime_data.result = nullptr; +} + +static void gwl_seat_ime_preedit_reset(GWL_Seat *seat) +{ + seat->ime.composite.clear(); + seat->ime.composite_is_null = false; + + GHOST_TEventImeData &event_ime_data = seat->ime.event_ime_data; + event_ime_data.composite_len = nullptr; + event_ime_data.composite = nullptr; + + event_ime_data.cursor_position = -1; + event_ime_data.target_start = -1; + event_ime_data.target_end = -1; +} + +static void gwl_seat_ime_rect_reset(GWL_Seat *seat) +{ + seat->ime.rect.x = -1; + seat->ime.rect.y = -1; + seat->ime.rect.w = -1; + seat->ime.rect.h = -1; +} + +#endif /* WITH_INPUT_IME */ + /** \} */ /* -------------------------------------------------------------------- */ @@ -945,6 +1055,9 @@ struct GWL_Display { wp_viewporter *viewporter = nullptr; zwp_pointer_constraints_v1 *pointer_constraints = nullptr; zwp_pointer_gestures_v1 *pointer_gestures = nullptr; +#ifdef WITH_INPUT_IME + struct zwp_text_input_manager_v3 *text_input_manager = nullptr; +#endif } wp; /** Wayland XDG types. */ @@ -4338,6 +4451,229 @@ static const zwp_primary_selection_source_v1_listener primary_selection_source_l /** \} */ +/* -------------------------------------------------------------------- */ +/** \name Listener (Text Input), #zwp_text_input_manager_v3 + * \{ */ + +#ifdef WITH_INPUT_IME + +class GHOST_EventIME : public GHOST_Event { + public: + /** + * Constructor. + * \param msec: The time this event was generated. + * \param type: The type of key event. + * \param key: The key code of the key. + */ + GHOST_EventIME(uint64_t msec, GHOST_TEventType type, GHOST_IWindow *window, void *customdata) + : GHOST_Event(msec, type, window) + { + this->m_data = customdata; + } +}; + +static CLG_LogRef LOG_WL_TEXT_INPUT = {"ghost.wl.handle.text_input"}; +# define LOG (&LOG_WL_TEXT_INPUT) + +static void text_input_handle_enter(void *data, + struct zwp_text_input_v3 * /*zwp_text_input_v3*/, + struct wl_surface *surface) +{ + if (!ghost_wl_surface_own(surface)) { + return; + } + CLOG_INFO(LOG, 2, "enter"); + GWL_Seat *seat = static_cast(data); + seat->ime.surface_window = surface; +} + +static void text_input_handle_leave(void *data, + struct zwp_text_input_v3 * /*zwp_text_input_v3*/, + struct wl_surface *surface) +{ + /* Can be null when closing a window. */ + if (!ghost_wl_surface_own_with_null_check(surface)) { + return; + } + CLOG_INFO(LOG, 2, "leave"); + GWL_Seat *seat = static_cast(data); + if (seat->ime.surface_window == surface) { + seat->ime.surface_window = nullptr; + } +} + +static void text_input_handle_preedit_string(void *data, + struct zwp_text_input_v3 * /*zwp_text_input_v3*/, + const char *text, + int32_t cursor_begin, + int32_t cursor_end) +{ + CLOG_INFO(LOG, + 2, + "preedit_string (text=\"%s\", cursor_begin=%d, cursor_end=%d)", + text ? text : "", + cursor_begin, + cursor_end); + + GWL_Seat *seat = static_cast(data); + if (UNLIKELY(seat->ime.surface_window == nullptr)) { + return; + } + + if (seat->ime.has_preedit == false) { + /* Starting IME input. */ + gwl_seat_ime_full_reset(seat); + } + + seat->ime.composite_is_null = (text == nullptr); + if (!seat->ime.composite_is_null) { + seat->ime.composite = text; + seat->ime.event_ime_data.composite = (void *)seat->ime.composite.c_str(); + seat->ime.event_ime_data.composite_len = (void *)seat->ime.composite.size(); + + seat->ime.event_ime_data.cursor_position = cursor_begin; + seat->ime.event_ime_data.target_start = cursor_begin; + seat->ime.event_ime_data.target_end = cursor_end; + } + + seat->ime.has_preedit_string_callback = true; +} + +static void text_input_handle_commit_string(void *data, + struct zwp_text_input_v3 * /*zwp_text_input_v3*/, + const char *text) +{ + CLOG_INFO(LOG, 2, "commit_string (text=\"%s\")", text ? text : ""); + + GWL_Seat *seat = static_cast(data); + if (UNLIKELY(seat->ime.surface_window == nullptr)) { + return; + } + + seat->ime.result_is_null = (text == nullptr); + if (seat->ime.result_is_null) { + seat->ime.result = ""; + } + else { + seat->ime.result = text; + } + + seat->ime.result_is_null = (text == nullptr); + seat->ime.event_ime_data.result = (void *)seat->ime.result.c_str(); + seat->ime.event_ime_data.result_len = (void *)seat->ime.result.size(); + seat->ime.event_ime_data.cursor_position = seat->ime.result.size(); + + seat->ime.has_commit_string_callback = true; +} + +static void text_input_handle_delete_surrounding_text( + void * /*data*/, + struct zwp_text_input_v3 * /*zwp_text_input_v3*/, + uint32_t before_length, + uint32_t after_length) +{ + CLOG_INFO(LOG, + 2, + "delete_surrounding_text (before_length=%u, after_length=%u)", + before_length, + after_length); + + /* NOTE: Currently unused, do we care about this event? + * SDL ignores this event. */ +} + +static void text_input_handle_done(void *data, + struct zwp_text_input_v3 * /*zwp_text_input_v3*/, + uint32_t /*serial*/) +{ + CLOG_INFO(LOG, 2, "done"); + + GWL_Seat *seat = static_cast(data); + GHOST_SystemWayland *system = seat->system; + + GHOST_WindowWayland *win = ghost_wl_surface_user_data(seat->ime.surface_window); + if (seat->ime.has_commit_string_callback) { + if (seat->ime.has_preedit) { + const bool is_end = seat->ime.composite_is_null; + if (is_end) { + seat->ime.has_preedit = false; + /* `commit_string` (end). */ + system->pushEvent_maybe_pending(new GHOST_EventIME(system->getMilliSeconds(), + GHOST_kEventImeComposition, + win, + &seat->ime.event_ime_data)); + system->pushEvent_maybe_pending(new GHOST_EventIME(system->getMilliSeconds(), + GHOST_kEventImeCompositionEnd, + win, + &seat->ime.event_ime_data)); + } + else { + /* `commit_string` (continues). */ + system->pushEvent_maybe_pending(new GHOST_EventIME(system->getMilliSeconds(), + GHOST_kEventImeComposition, + win, + &seat->ime.event_ime_data)); + } + } + else { + /* `commit_string` ran with no active IME popup, start & end to insert text. */ + system->pushEvent_maybe_pending(new GHOST_EventIME(system->getMilliSeconds(), + GHOST_kEventImeCompositionStart, + win, + &seat->ime.event_ime_data)); + system->pushEvent_maybe_pending(new GHOST_EventIME( + system->getMilliSeconds(), GHOST_kEventImeComposition, win, &seat->ime.event_ime_data)); + system->pushEvent_maybe_pending(new GHOST_EventIME(system->getMilliSeconds(), + GHOST_kEventImeCompositionEnd, + win, + &seat->ime.event_ime_data)); + } + + if (seat->ime.has_preedit == false) { + gwl_seat_ime_preedit_reset(seat); + } + } + else if (seat->ime.has_preedit_string_callback) { + const bool is_end = seat->ime.composite_is_null; + if (is_end) { + /* `preedit_string` (end). */ + seat->ime.has_preedit = false; + system->pushEvent_maybe_pending(new GHOST_EventIME(seat->system->getMilliSeconds(), + GHOST_kEventImeCompositionEnd, + win, + &seat->ime.event_ime_data)); + } + else { + const bool is_start = seat->ime.has_preedit == false; + /* `preedit_string` (start or continue). */ + seat->ime.has_preedit = true; + system->pushEvent_maybe_pending(new GHOST_EventIME( + seat->system->getMilliSeconds(), + is_start ? GHOST_kEventImeCompositionStart : GHOST_kEventImeComposition, + win, + &seat->ime.event_ime_data)); + } + } + + seat->ime.has_preedit_string_callback = false; + seat->ime.has_commit_string_callback = false; +} + +static struct zwp_text_input_v3_listener text_input_listener = { + /*enter*/ text_input_handle_enter, + /*leave*/ text_input_handle_leave, + /*preedit_string*/ text_input_handle_preedit_string, + /*commit_string*/ text_input_handle_commit_string, + /*delete_surrounding_text*/ text_input_handle_delete_surrounding_text, + /*done*/ text_input_handle_done, +}; + +# undef LOG + +#endif /* WITH_INPUT_IME. */ + +/** \} */ + /* -------------------------------------------------------------------- */ /** \name Listener (Seat), #wl_seat_listener * \{ */ @@ -5030,6 +5366,20 @@ static void gwl_registry_wl_seat_update(GWL_Display *display, else { seat->wp.primary_selection_device = nullptr; } + +#ifdef WITH_INPUT_IME + if (display->wp.text_input_manager) { + if (seat->wp.text_input == nullptr) { + seat->wp.text_input = zwp_text_input_manager_v3_get_text_input( + display->wp.text_input_manager, seat->wl.seat); + zwp_text_input_v3_set_user_data(seat->wp.text_input, seat); + zwp_text_input_v3_add_listener(seat->wp.text_input, &text_input_listener, seat); + } + } + else { + seat->wp.text_input = nullptr; + } +#endif /* WITH_INPUT_IME */ } static void gwl_registry_wl_seat_remove(GWL_Display *display, void *user_data, const bool on_exit) { @@ -5312,6 +5662,28 @@ static void gwl_registry_wp_primary_selection_device_manager_remove(GWL_Display *value_p = nullptr; } +#ifdef WITH_INPUT_IME + +/* #GWL_Display.wp_text_input_manager */ + +static void gwl_registry_wp_text_input_manager_add(GWL_Display *display, + const GWL_RegisteryAdd_Params *params) +{ + display->wp.text_input_manager = static_cast(wl_registry_bind( + display->wl.registry, params->name, &zwp_text_input_manager_v3_interface, 1)); + gwl_registry_entry_add(display, params, nullptr); +} +static void gwl_registry_wp_text_input_manager_remove(GWL_Display *display, + void * /*user_data*/, + const bool /*on_exit*/) +{ + struct zwp_text_input_manager_v3 **value_p = &display->wp.text_input_manager; + zwp_text_input_manager_v3_destroy(*value_p); + *value_p = nullptr; +} + +#endif /* WITH_INPUT_IME */ + /** * Map interfaces to initialization functions. * @@ -5379,6 +5751,14 @@ static const GWL_RegistryHandler gwl_registry_handlers[] = { /*update_fn*/ nullptr, /*remove_fn*/ gwl_registry_wp_relative_pointer_manager_remove, }, +#ifdef WITH_INPUT_IME + { + /*interface_p*/ &zwp_text_input_manager_v3_interface.name, + /*add_fn*/ gwl_registry_wp_text_input_manager_add, + /*update_fn*/ nullptr, + /*remove_fn*/ gwl_registry_wp_text_input_manager_remove, + }, +#endif /* Higher level interfaces. */ { /*interface_p*/ &zwp_pointer_constraints_v1_interface.name, @@ -7143,6 +7523,105 @@ GHOST_TimerManager *GHOST_SystemWayland::ghost_timer_manager() /** \} */ +/* -------------------------------------------------------------------- */ +/** \name Public WAYLAND Text Input (IME) Functions + * + * Functionality only used for the WAYLAND implementation. + * \{ */ + +#ifdef WITH_INPUT_IME + +void GHOST_SystemWayland::ime_begin( + GHOST_WindowWayland *win, int32_t x, int32_t y, int32_t w, int32_t h, bool completed) const +{ + GWL_Seat *seat = gwl_display_seat_active_get(display_); + if (UNLIKELY(!seat)) { + return; + } + if (seat->wp.text_input == nullptr) { + return; + } + + /* Prevent a feedback loop because the commits from this function cause + * #zwp_text_input_v3_listener::preedit_string to run again which sends an event, + * refreshing the position, running this function again. */ + gwl_seat_ime_result_reset(seat); + + /* Don't re-enable if we're already enabled. */ + if (seat->ime.is_enabled && completed) { + return; + } + + bool force_rect_update = false; + if (seat->ime.is_enabled == false) { + seat->ime.has_preedit = false; + seat->ime.is_enabled = true; + + /* NOTE(@flibit): For some reason this has to be done twice, + * it appears to be a bug in mutter? Maybe? */ + zwp_text_input_v3_enable(seat->wp.text_input); + zwp_text_input_v3_commit(seat->wp.text_input); + zwp_text_input_v3_enable(seat->wp.text_input); + zwp_text_input_v3_commit(seat->wp.text_input); + + /* Now that it's enabled, set the input properties. */ + zwp_text_input_v3_set_content_type(seat->wp.text_input, + ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE, + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL); + + gwl_seat_ime_rect_reset(seat); + force_rect_update = true; + } + + if ((force_rect_update == false) && /* Was just created, always update. */ + (seat->ime.rect.x == x) && /* X. */ + (seat->ime.rect.y == y) && /* Y. */ + (seat->ime.rect.w == w) && /* W. */ + (seat->ime.rect.h == h)) /* H. */ + { + /* Only re-update the rectangle as needed. */ + printf("DONT REPOSITION\n"); + } + else { + const int rect_x = wl_fixed_to_int(win->wl_fixed_from_window(wl_fixed_from_int(x))); + const int rect_y = wl_fixed_to_int(win->wl_fixed_from_window(wl_fixed_from_int(y))); + const int rect_w = wl_fixed_to_int(win->wl_fixed_from_window(wl_fixed_from_int(w))) + 1; + const int rect_h = wl_fixed_to_int(win->wl_fixed_from_window(wl_fixed_from_int(h))) + 1; + + zwp_text_input_v3_set_cursor_rectangle(seat->wp.text_input, rect_x, rect_y, rect_w, rect_h); + + zwp_text_input_v3_commit(seat->wp.text_input); + + seat->ime.rect.x = x; + seat->ime.rect.y = y; + seat->ime.rect.w = w; + seat->ime.rect.h = h; + } +} + +void GHOST_SystemWayland::ime_end(GHOST_WindowWayland * /*window*/) const +{ + GWL_Seat *seat = gwl_display_seat_active_get(display_); + if (UNLIKELY(!seat)) { + return; + } + + seat->ime.is_enabled = false; + + gwl_seat_ime_rect_reset(seat); + + if (seat->wp.text_input == nullptr) { + return; + } + + zwp_text_input_v3_disable(seat->wp.text_input); + zwp_text_input_v3_commit(seat->wp.text_input); +} + +#endif /* WITH_INPUT_IME */ + +/** \} */ + /* -------------------------------------------------------------------- */ /** \name Public WAYLAND Query Access * \{ */ @@ -7215,6 +7694,9 @@ bool GHOST_SystemWayland::window_surface_unref(const wl_surface *wl_surface) SURFACE_CLEAR_PTR(seat->tablet.wl.surface_window); SURFACE_CLEAR_PTR(seat->keyboard.wl.surface_window); SURFACE_CLEAR_PTR(seat->wl.surface_window_focus_dnd); +#ifdef WITH_INPUT_IME + SURFACE_CLEAR_PTR(seat->ime.surface_window); +#endif } #undef SURFACE_CLEAR_PTR diff --git a/intern/ghost/intern/GHOST_SystemWayland.hh b/intern/ghost/intern/GHOST_SystemWayland.hh index 4f69920537b..de0674320cc 100644 --- a/intern/ghost/intern/GHOST_SystemWayland.hh +++ b/intern/ghost/intern/GHOST_SystemWayland.hh @@ -236,6 +236,10 @@ class GHOST_SystemWayland : public GHOST_System { struct wl_shm *wl_shm_get() const; + void ime_begin( + GHOST_WindowWayland *win, int32_t x, int32_t y, int32_t w, int32_t h, bool completed) const; + void ime_end(GHOST_WindowWayland *win) const; + static const char *xdg_app_id_get(); /* WAYLAND utility functions. */ diff --git a/intern/ghost/intern/GHOST_SystemX11.cc b/intern/ghost/intern/GHOST_SystemX11.cc index cb43ac7ad3b..e9e2332c139 100644 --- a/intern/ghost/intern/GHOST_SystemX11.cc +++ b/intern/ghost/intern/GHOST_SystemX11.cc @@ -1742,7 +1742,9 @@ GHOST_TCapabilityFlag GHOST_SystemX11::getCapabilities() const /* No support yet for desktop sampling. */ GHOST_kCapabilityDesktopSample | /* No support yet for image copy/paste. */ - GHOST_kCapabilityClipboardImages)); + GHOST_kCapabilityClipboardImages | + /* No support yet for IME input methods. */ + GHOST_kCapabilityInputIME)); } void GHOST_SystemX11::addDirtyWindow(GHOST_WindowX11 *bad_wind) diff --git a/intern/ghost/intern/GHOST_WindowWayland.cc b/intern/ghost/intern/GHOST_WindowWayland.cc index 0c5ae473753..4f22652fab0 100644 --- a/intern/ghost/intern/GHOST_WindowWayland.cc +++ b/intern/ghost/intern/GHOST_WindowWayland.cc @@ -2012,6 +2012,20 @@ GHOST_Context *GHOST_WindowWayland::newDrawingContext(GHOST_TDrawingContextType } } +#ifdef WITH_INPUT_IME + +void GHOST_WindowWayland::beginIME(int32_t x, int32_t y, int32_t w, int32_t h, bool completed) +{ + system_->ime_begin(this, x, y, w, h, completed); +} + +void GHOST_WindowWayland::endIME() +{ + system_->ime_end(this); +} + +#endif + /** \} */ /* -------------------------------------------------------------------- */ diff --git a/intern/ghost/intern/GHOST_WindowWayland.hh b/intern/ghost/intern/GHOST_WindowWayland.hh index 791d97d9d46..db1f941c86e 100644 --- a/intern/ghost/intern/GHOST_WindowWayland.hh +++ b/intern/ghost/intern/GHOST_WindowWayland.hh @@ -140,6 +140,11 @@ class GHOST_WindowWayland : public GHOST_Window { void setOpaque() const; #endif +#ifdef WITH_INPUT_IME + void beginIME(int32_t x, int32_t y, int32_t w, int32_t h, bool completed) override; + void endIME() override; +#endif /* WITH_INPUT_IME */ + /* WAYLAND direct-data access. */ int scale_get() const; diff --git a/source/blender/editors/interface/interface_handlers.cc b/source/blender/editors/interface/interface_handlers.cc index fd2f1366797..4d6f18a298b 100644 --- a/source/blender/editors/interface/interface_handlers.cc +++ b/source/blender/editors/interface/interface_handlers.cc @@ -3604,7 +3604,11 @@ static void ui_textedit_end(bContext *C, uiBut *but, uiHandleButtonData *data) data->undo_stack_text = nullptr; #ifdef WITH_INPUT_IME - if (win->ime_data) { + /* See #wm_window_IME_end code-comments for details. */ +# if defined(WIN32) || defined(__APPLE__) + if (win->ime_data) +# endif + { ui_textedit_ime_end(win, but); } #endif diff --git a/source/blender/windowmanager/WM_api.hh b/source/blender/windowmanager/WM_api.hh index 0f9dbbde611..e3f22d9a46f 100644 --- a/source/blender/windowmanager/WM_api.hh +++ b/source/blender/windowmanager/WM_api.hh @@ -172,6 +172,8 @@ enum eWM_CapabilitiesFlag { WM_CAPABILITY_CLIPBOARD_IMAGES = (1 << 4), /** Ability to sample a color outside of Blender windows. */ WM_CAPABILITY_DESKTOP_SAMPLE = (1 << 5), + /** Support for IME input methods. */ + WM_CAPABILITY_INPUT_IME = (1 << 6), /** The initial value, indicates the value needs to be set by inspecting GHOST. */ WM_CAPABILITY_INITIALIZED = (1 << 31), }; diff --git a/source/blender/windowmanager/intern/wm_window.cc b/source/blender/windowmanager/intern/wm_window.cc index 64031a8381a..010762f8918 100644 --- a/source/blender/windowmanager/intern/wm_window.cc +++ b/source/blender/windowmanager/intern/wm_window.cc @@ -1977,6 +1977,9 @@ eWM_CapabilitiesFlag WM_capabilities_flag() if (ghost_flag & GHOST_kCapabilityDesktopSample) { flag |= WM_CAPABILITY_DESKTOP_SAMPLE; } + if (ghost_flag & GHOST_kCapabilityInputIME) { + flag |= WM_CAPABILITY_INPUT_IME; + } return flag; } @@ -2739,6 +2742,9 @@ bool WM_window_is_temp_screen(const wmWindow *win) void wm_window_IME_begin(wmWindow *win, int x, int y, int w, int h, bool complete) { BLI_assert(win); + if ((WM_capabilities_flag() & WM_CAPABILITY_INPUT_IME) == 0) { + return; + } /* Convert to native OS window coordinates. */ float fac = GHOST_GetNativePixelSize(static_cast(win->ghostwin)); @@ -2750,8 +2756,17 @@ void wm_window_IME_begin(wmWindow *win, int x, int y, int w, int h, bool complet void wm_window_IME_end(wmWindow *win) { - BLI_assert(win && win->ime_data); + if ((WM_capabilities_flag() & WM_CAPABILITY_INPUT_IME) == 0) { + return; + } + BLI_assert(win); + /* NOTE(@ideasman42): on WAYLAND a call to "begin" must be closed by an "end" call. + * Even if no IME events were generated (which assigned `ime_data`). + * TODO: check if #GHOST_EndIME can run on WIN32 & APPLE without causing problems. */ +# if defined(WIN32) || defined(__APPLE__) + BLI_assert(win->ime_data); +# endif GHOST_EndIME(static_cast(win->ghostwin)); win->ime_data = nullptr; win->ime_data_is_composing = false;