]> git.mdlowis.com Git - proto/labwc.git/commitdiff
IME: support input method
authortokyo4j <hrak1529@gmail.com>
Fri, 9 Feb 2024 07:52:37 +0000 (16:52 +0900)
committerJohan Malm <johanmalm@users.noreply.github.com>
Tue, 5 Mar 2024 20:28:15 +0000 (20:28 +0000)
include/input/ime.h [new file with mode: 0644]
include/labwc.h
src/input/ime.c [new file with mode: 0644]
src/input/keyboard.c
src/input/meson.build
src/seat.c
src/server.c

diff --git a/include/input/ime.h b/include/input/ime.h
new file mode 100644 (file)
index 0000000..d904625
--- /dev/null
@@ -0,0 +1,77 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+#ifndef LABWC_IME_H
+#define LABWC_IME_H
+
+#include <wlr/types/wlr_text_input_v3.h>
+#include <wlr/types/wlr_input_method_v2.h>
+#include "labwc.h"
+
+struct keyboard;
+
+/*
+ * The relay structure manages the relationship between text-inputs and
+ * input-method on a given seat. Multiple text-inputs may be bound to a relay,
+ * but at most one will be "active" (commucating with input-method) at a time.
+ * At most one input-method may be bound to the seat. When an input-method and
+ * an active text-input is present, the relay passes messages between them.
+ */
+struct input_method_relay {
+       struct seat *seat;
+       struct wl_list text_inputs; /* struct text_input.link */
+       struct wlr_input_method_v2 *input_method;
+       struct wlr_surface *focused_surface;
+       /*
+        * Text-input which is enabled by the client and communicating with
+        * input-method.
+        * This must be NULL if input-method is not present.
+        * Its client must be the same as that of focused_surface.
+        */
+       struct text_input *active_text_input;
+
+       struct wl_listener new_text_input;
+       struct wl_listener new_input_method;
+
+       struct wl_listener input_method_commit;
+       struct wl_listener input_method_grab_keyboard;
+       struct wl_listener input_method_destroy;
+
+       struct wl_listener keyboard_grab_destroy;
+       struct wl_listener focused_surface_destroy;
+};
+
+struct text_input {
+       struct input_method_relay *relay;
+       struct wlr_text_input_v3 *input;
+       struct wl_list link;
+
+       struct wl_listener enable;
+       struct wl_listener commit;
+       struct wl_listener disable;
+       struct wl_listener destroy;
+};
+
+/*
+ * Forward key event to keyboard grab of the seat from the keyboard
+ * if the keyboard grab exists.
+ * Returns true if the key event was forwarded.
+ */
+bool input_method_keyboard_grab_forward_key(struct keyboard *keyboard,
+       struct wlr_keyboard_key_event *event);
+
+/*
+ * Forward modifier state to keyboard grab of the seat from the keyboard
+ * if the keyboard grab exists.
+ * Returns true if the modifier state was forwarded.
+ */
+bool input_method_keyboard_grab_forward_modifiers(struct keyboard *keyboard);
+
+struct input_method_relay *input_method_relay_create(struct seat *seat);
+
+void input_method_relay_finish(struct input_method_relay *relay);
+
+/* Updates currently focused surface. Surface must belong to the same seat. */
+void input_method_relay_set_focus(struct input_method_relay *relay,
+       struct wlr_surface *surface);
+
+#endif
index 70cef9475a18a7741c4b792ebd44679beaae154b..abe32a6c2958aaec7c2fd94e29acdb72a94a79a9 100644 (file)
 #include <wlr/types/wlr_virtual_pointer_v1.h>
 #include <wlr/types/wlr_virtual_keyboard_v1.h>
 #include <wlr/types/wlr_tearing_control_v1.h>
+#include <wlr/types/wlr_text_input_v3.h>
+#include <wlr/types/wlr_input_method_v2.h>
 #include <wlr/util/log.h>
 #include "config/keybind.h"
 #include "config/rcxml.h"
 #include "input/cursor.h"
+#include "input/ime.h"
 #include "regions.h"
 #include "session-lock.h"
 #if HAVE_NLS
@@ -120,6 +123,8 @@ struct seat {
        /* if set, views cannot receive focus */
        struct wlr_layer_surface_v1 *focused_layer;
 
+       struct input_method_relay *input_method_relay;
+
        /**
         * pressed view/surface/node will usually be NULL and is only set on
         * button press while the mouse is over a view or surface, and reset
@@ -324,6 +329,9 @@ struct server {
        struct wlr_tearing_control_manager_v1 *tearing_control;
        struct wl_listener tearing_new_object;
 
+       struct wlr_input_method_manager_v2 *input_method_manager;
+       struct wlr_text_input_manager_v3 *text_input_manager;
+
        /* Set when in cycle (alt-tab) mode */
        struct osd_state {
                struct view *cycle_view;
diff --git a/src/input/ime.c b/src/input/ime.c
new file mode 100644 (file)
index 0000000..18d5420
--- /dev/null
@@ -0,0 +1,447 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Based on Sway (https://github.com/swaywm/sway) */
+
+#include <assert.h>
+#include "common/mem.h"
+#include "input/ime.h"
+#include "node.h"
+#include "view.h"
+
+#define SAME_CLIENT(wlr_obj1, wlr_obj2) \
+       (wl_resource_get_client((wlr_obj1)->resource) \
+       == wl_resource_get_client((wlr_obj2)->resource))
+
+/*
+ * Get keyboard grab of the seat from keyboard if we should forward events
+ * to it.
+ */
+static struct wlr_input_method_keyboard_grab_v2 *
+get_keyboard_grab(struct keyboard *keyboard)
+{
+       struct wlr_input_method_v2 *input_method =
+               keyboard->base.seat->input_method_relay->input_method;
+       if (!input_method || !input_method->keyboard_grab) {
+               return NULL;
+       }
+
+       /*
+        * Input-methods often use virtual keyboard to send raw key signals
+        * instead of sending encoded text via set_preedit_string and
+        * commit_string. We should not forward those key events back to the
+        * input-method so key events don't loop between the compositor and
+        * the input-method.
+        */
+       struct wlr_virtual_keyboard_v1 *virtual_keyboard =
+               wlr_input_device_get_virtual_keyboard(
+                       keyboard->base.wlr_input_device);
+       if (virtual_keyboard && SAME_CLIENT(virtual_keyboard,
+                       input_method->keyboard_grab)) {
+               return NULL;
+       }
+
+       return input_method->keyboard_grab;
+}
+
+bool
+input_method_keyboard_grab_forward_modifiers(struct keyboard *keyboard)
+{
+       struct wlr_input_method_keyboard_grab_v2 *keyboard_grab =
+               get_keyboard_grab(keyboard);
+       if (keyboard_grab) {
+               wlr_input_method_keyboard_grab_v2_set_keyboard(keyboard_grab,
+                       keyboard->wlr_keyboard);
+               wlr_input_method_keyboard_grab_v2_send_modifiers(keyboard_grab,
+                       &keyboard->wlr_keyboard->modifiers);
+               return true;
+       } else {
+               return false;
+       }
+}
+
+bool
+input_method_keyboard_grab_forward_key(struct keyboard *keyboard,
+               struct wlr_keyboard_key_event *event)
+{
+       struct wlr_input_method_keyboard_grab_v2 *keyboard_grab =
+               get_keyboard_grab(keyboard);
+       if (keyboard_grab) {
+               wlr_input_method_keyboard_grab_v2_set_keyboard(keyboard_grab,
+                       keyboard->wlr_keyboard);
+               wlr_input_method_keyboard_grab_v2_send_key(keyboard_grab,
+                       event->time_msec, event->keycode, event->state);
+               return true;
+       } else {
+               return false;
+       }
+}
+
+/*
+ * update_text_inputs_focused_surface() should be called beforehand to set
+ * right text-inputs to choose from.
+ */
+static struct text_input *
+get_active_text_input(struct input_method_relay *relay)
+{
+       if (!relay->input_method) {
+               return NULL;
+       }
+       struct text_input *text_input;
+       wl_list_for_each(text_input, &relay->text_inputs, link) {
+               if (text_input->input->focused_surface
+                               && text_input->input->current_enabled) {
+                       return text_input;
+               }
+       }
+       return NULL;
+}
+
+/*
+ * Updates active text-input and activates/deactivates the input-method if the
+ * value is changed.
+ */
+static void
+update_active_text_input(struct input_method_relay *relay)
+{
+       struct text_input *active_text_input = get_active_text_input(relay);
+
+       if (relay->input_method && relay->active_text_input
+                       != active_text_input) {
+               if (active_text_input) {
+                       wlr_input_method_v2_send_activate(relay->input_method);
+               } else {
+                       wlr_input_method_v2_send_deactivate(relay->input_method);
+               }
+               wlr_input_method_v2_send_done(relay->input_method);
+       }
+
+       relay->active_text_input = active_text_input;
+}
+
+/*
+ * Updates focused surface of text-inputs and sends enter/leave events to
+ * the text-inputs whose focused surface is changed.
+ * When input-method is present, text-inputs whose client is the same as the
+ * relay's focused surface also have that focused surface. Clients can then
+ * send enable request on a text-input which has the focused surface to make
+ * the text-input active and start communicating with input-method.
+ */
+static void
+update_text_inputs_focused_surface(struct input_method_relay *relay)
+{
+       struct text_input *text_input;
+       wl_list_for_each(text_input, &relay->text_inputs, link) {
+               struct wlr_text_input_v3 *input = text_input->input;
+
+               struct wlr_surface *new_focused_surface;
+               if (relay->input_method && relay->focused_surface
+                               && SAME_CLIENT(input, relay->focused_surface)) {
+                       new_focused_surface = relay->focused_surface;
+               } else {
+                       new_focused_surface = NULL;
+               }
+
+               if (input->focused_surface == new_focused_surface) {
+                       continue;
+               }
+               if (input->focused_surface) {
+                       wlr_text_input_v3_send_leave(input);
+               }
+               if (new_focused_surface) {
+                       wlr_text_input_v3_send_enter(input, new_focused_surface);
+               }
+       }
+}
+
+static void
+handle_input_method_commit(struct wl_listener *listener, void *data)
+{
+       struct input_method_relay *relay =
+               wl_container_of(listener, relay, input_method_commit);
+       struct wlr_input_method_v2 *input_method = data;
+       assert(relay->input_method == input_method);
+
+       struct text_input *text_input = relay->active_text_input;
+       if (!text_input) {
+               return;
+       }
+
+       if (input_method->current.preedit.text) {
+               wlr_text_input_v3_send_preedit_string(
+                       text_input->input,
+                       input_method->current.preedit.text,
+                       input_method->current.preedit.cursor_begin,
+                       input_method->current.preedit.cursor_end);
+       }
+       if (input_method->current.commit_text) {
+               wlr_text_input_v3_send_commit_string(
+                       text_input->input,
+                       input_method->current.commit_text);
+       }
+       if (input_method->current.delete.before_length
+                       || input_method->current.delete.after_length) {
+               wlr_text_input_v3_send_delete_surrounding_text(
+                       text_input->input,
+                       input_method->current.delete.before_length,
+                       input_method->current.delete.after_length);
+       }
+       wlr_text_input_v3_send_done(text_input->input);
+}
+
+static void
+handle_keyboard_grab_destroy(struct wl_listener *listener, void *data)
+{
+       struct input_method_relay *relay =
+               wl_container_of(listener, relay, keyboard_grab_destroy);
+       struct wlr_input_method_keyboard_grab_v2 *keyboard_grab = data;
+       wl_list_remove(&relay->keyboard_grab_destroy.link);
+
+       if (keyboard_grab->keyboard) {
+               /* Send modifier state to original client */
+               wlr_seat_keyboard_notify_modifiers(
+                       keyboard_grab->input_method->seat,
+                       &keyboard_grab->keyboard->modifiers);
+       }
+}
+
+static void
+handle_input_method_grab_keyboard(struct wl_listener *listener, void *data)
+{
+       struct input_method_relay *relay = wl_container_of(listener, relay,
+               input_method_grab_keyboard);
+       struct wlr_input_method_keyboard_grab_v2 *keyboard_grab = data;
+
+       /* Send modifier state to grab */
+       struct wlr_keyboard *active_keyboard =
+               wlr_seat_get_keyboard(relay->seat->seat);
+       wlr_input_method_keyboard_grab_v2_set_keyboard(
+               keyboard_grab, active_keyboard);
+
+       relay->keyboard_grab_destroy.notify = handle_keyboard_grab_destroy;
+       wl_signal_add(&keyboard_grab->events.destroy,
+               &relay->keyboard_grab_destroy);
+}
+
+static void
+handle_input_method_destroy(struct wl_listener *listener, void *data)
+{
+       struct input_method_relay *relay =
+               wl_container_of(listener, relay, input_method_destroy);
+       assert(relay->input_method == data);
+       wl_list_remove(&relay->input_method_commit.link);
+       wl_list_remove(&relay->input_method_grab_keyboard.link);
+       wl_list_remove(&relay->input_method_destroy.link);
+       relay->input_method = NULL;
+
+       update_text_inputs_focused_surface(relay);
+       update_active_text_input(relay);
+}
+
+static void
+handle_new_input_method(struct wl_listener *listener, void *data)
+{
+       struct input_method_relay *relay =
+               wl_container_of(listener, relay, new_input_method);
+       struct wlr_input_method_v2 *input_method = data;
+       if (relay->seat->seat != input_method->seat) {
+               return;
+       }
+
+       if (relay->input_method) {
+               wlr_log(WLR_INFO,
+                       "Attempted to connect second input method to a seat");
+               wlr_input_method_v2_send_unavailable(input_method);
+               return;
+       }
+
+       relay->input_method = input_method;
+
+       relay->input_method_commit.notify = handle_input_method_commit;
+       wl_signal_add(&relay->input_method->events.commit,
+               &relay->input_method_commit);
+
+       relay->input_method_grab_keyboard.notify =
+               handle_input_method_grab_keyboard;
+       wl_signal_add(&relay->input_method->events.grab_keyboard,
+               &relay->input_method_grab_keyboard);
+
+       relay->input_method_destroy.notify = handle_input_method_destroy;
+       wl_signal_add(&relay->input_method->events.destroy,
+               &relay->input_method_destroy);
+
+       update_text_inputs_focused_surface(relay);
+       update_active_text_input(relay);
+}
+
+/* Conveys state from active text-input to input-method */
+static void
+send_state_to_input_method(struct input_method_relay *relay)
+{
+       assert(relay->active_text_input && relay->input_method);
+
+       struct wlr_input_method_v2 *input_method = relay->input_method;
+       struct wlr_text_input_v3 *input = relay->active_text_input->input;
+
+       /* TODO: only send each of those if they were modified */
+       if (input->active_features
+                       & WLR_TEXT_INPUT_V3_FEATURE_SURROUNDING_TEXT) {
+               wlr_input_method_v2_send_surrounding_text(input_method,
+                       input->current.surrounding.text,
+                       input->current.surrounding.cursor,
+                       input->current.surrounding.anchor);
+       }
+       wlr_input_method_v2_send_text_change_cause(input_method,
+               input->current.text_change_cause);
+       if (input->active_features & WLR_TEXT_INPUT_V3_FEATURE_CONTENT_TYPE) {
+               wlr_input_method_v2_send_content_type(input_method,
+                       input->current.content_type.hint,
+                       input->current.content_type.purpose);
+       }
+       wlr_input_method_v2_send_done(input_method);
+}
+
+static void
+handle_text_input_enable(struct wl_listener *listener, void *data)
+{
+       struct text_input *text_input =
+               wl_container_of(listener, text_input, enable);
+       struct input_method_relay *relay = text_input->relay;
+
+       update_active_text_input(relay);
+       if (relay->active_text_input == text_input) {
+               send_state_to_input_method(relay);
+       }
+}
+
+static void
+handle_text_input_disable(struct wl_listener *listener, void *data)
+{
+       struct text_input *text_input =
+               wl_container_of(listener, text_input, disable);
+       /*
+        * When the focus is moved between surfaces from different clients and
+        * then the old client sends "disable" event, the relay ignores it and
+        * doesn't deactivate the input-method.
+        */
+       update_active_text_input(text_input->relay);
+}
+
+static void
+handle_text_input_commit(struct wl_listener *listener, void *data)
+{
+       struct text_input *text_input =
+               wl_container_of(listener, text_input, commit);
+       struct input_method_relay *relay = text_input->relay;
+
+       if (relay->active_text_input == text_input) {
+               send_state_to_input_method(relay);
+       }
+}
+
+static void
+handle_text_input_destroy(struct wl_listener *listener, void *data)
+{
+       struct text_input *text_input =
+               wl_container_of(listener, text_input, destroy);
+       wl_list_remove(&text_input->enable.link);
+       wl_list_remove(&text_input->disable.link);
+       wl_list_remove(&text_input->commit.link);
+       wl_list_remove(&text_input->destroy.link);
+       wl_list_remove(&text_input->link);
+       update_active_text_input(text_input->relay);
+       free(text_input);
+}
+
+static void
+handle_new_text_input(struct wl_listener *listener, void *data)
+{
+       struct input_method_relay *relay =
+               wl_container_of(listener, relay, new_text_input);
+       struct wlr_text_input_v3 *wlr_text_input = data;
+       if (relay->seat->seat != wlr_text_input->seat) {
+               return;
+       }
+
+       struct text_input *text_input = znew(*text_input);
+       text_input->input = wlr_text_input;
+       text_input->relay = relay;
+       wl_list_insert(&relay->text_inputs, &text_input->link);
+
+       text_input->enable.notify = handle_text_input_enable;
+       wl_signal_add(&text_input->input->events.enable, &text_input->enable);
+
+       text_input->disable.notify = handle_text_input_disable;
+       wl_signal_add(&text_input->input->events.disable, &text_input->disable);
+
+       text_input->commit.notify = handle_text_input_commit;
+       wl_signal_add(&text_input->input->events.commit, &text_input->commit);
+
+       text_input->destroy.notify = handle_text_input_destroy;
+       wl_signal_add(&text_input->input->events.destroy, &text_input->destroy);
+
+       update_text_inputs_focused_surface(relay);
+}
+
+/*
+ * Usually this function is not called because the client destroys the surface
+ * role (like xdg_toplevel) first and input_method_relay_set_focus() is called
+ * before wl_surface is destroyed.
+ */
+static void
+handle_focused_surface_destroy(struct wl_listener *listener, void *data)
+{
+       struct input_method_relay *relay =
+               wl_container_of(listener, relay, focused_surface_destroy);
+       assert(relay->focused_surface == data);
+
+       input_method_relay_set_focus(relay, NULL);
+}
+
+struct input_method_relay *
+input_method_relay_create(struct seat *seat)
+{
+       struct input_method_relay *relay = znew(*relay);
+       relay->seat = seat;
+       wl_list_init(&relay->text_inputs);
+
+       relay->new_text_input.notify = handle_new_text_input;
+       wl_signal_add(&seat->server->text_input_manager->events.text_input,
+               &relay->new_text_input);
+
+       relay->new_input_method.notify = handle_new_input_method;
+       wl_signal_add(&seat->server->input_method_manager->events.input_method,
+               &relay->new_input_method);
+
+       relay->focused_surface_destroy.notify = handle_focused_surface_destroy;
+
+       return relay;
+}
+
+void
+input_method_relay_finish(struct input_method_relay *relay)
+{
+       wl_list_remove(&relay->new_text_input.link);
+       wl_list_remove(&relay->new_input_method.link);
+       free(relay);
+}
+
+void
+input_method_relay_set_focus(struct input_method_relay *relay,
+               struct wlr_surface *surface)
+{
+       if (relay->focused_surface == surface) {
+               wlr_log(WLR_INFO, "The surface is already focused");
+               return;
+       }
+
+       if (relay->focused_surface) {
+               wl_list_remove(&relay->focused_surface_destroy.link);
+       }
+       relay->focused_surface = surface;
+       if (surface) {
+               wl_signal_add(&surface->events.destroy,
+                       &relay->focused_surface_destroy);
+       }
+
+       update_text_inputs_focused_surface(relay);
+       update_active_text_input(relay);
+}
index 7379a540545ccd838dcb98661a2b423a725208b2..3b0b6d96697c30a9d0f7563393dd756b2798744c 100644 (file)
@@ -97,7 +97,11 @@ keyboard_modifiers_notify(struct wl_listener *listener, void *data)
                        }
                }
        }
-       wlr_seat_keyboard_notify_modifiers(seat->seat, &wlr_keyboard->modifiers);
+
+       if (!input_method_keyboard_grab_forward_modifiers(keyboard)) {
+               wlr_seat_keyboard_notify_modifiers(seat->seat,
+                       &wlr_keyboard->modifiers);
+       }
 }
 
 static struct keybind *
@@ -523,7 +527,7 @@ keyboard_key_notify(struct wl_listener *listener, void *data)
                if (event->state == WL_KEYBOARD_KEY_STATE_PRESSED) {
                        start_keybind_repeat(seat->server, keyboard, event);
                }
-       } else {
+       } else if (!input_method_keyboard_grab_forward_key(keyboard, event)) {
                wlr_seat_set_keyboard(wlr_seat, keyboard->wlr_keyboard);
                wlr_seat_keyboard_notify_key(wlr_seat, event->time_msec,
                        event->keycode, event->state);
index c8b7717c981f041788390049998e08d9fea5c281..c333184f07d0db65be515e88c81a82841be4f937 100644 (file)
@@ -7,4 +7,5 @@ labwc_sources += files(
   'keyboard.c',
   'key-state.c',
   'touch.c',
+  'ime.c',
 )
index e4e69efea364a597ea2f80c4a72d995451d6d026..27d5d570d5c527bb69e8fca9f1360b1636a4fd57 100644 (file)
@@ -536,6 +536,8 @@ seat_init(struct server *server)
                &seat->virtual_keyboard_new);
        seat->virtual_keyboard_new.notify = new_virtual_keyboard;
 
+       seat->input_method_relay = input_method_relay_create(seat);
+
        seat->cursor = wlr_cursor_create();
        if (!seat->cursor) {
                wlr_log(WLR_ERROR, "unable to create cursor");
@@ -559,6 +561,7 @@ seat_finish(struct server *server)
        }
 
        input_handlers_finish(seat);
+       input_method_relay_finish(seat->input_method_relay);
 }
 
 static void
@@ -614,6 +617,7 @@ seat_focus(struct seat *seat, struct wlr_surface *surface, bool is_lock_surface)
 
        if (!surface) {
                wlr_seat_keyboard_notify_clear_focus(seat->seat);
+               input_method_relay_set_focus(seat->input_method_relay, NULL);
                return;
        }
 
@@ -636,6 +640,8 @@ seat_focus(struct seat *seat, struct wlr_surface *surface, bool is_lock_surface)
        wlr_seat_keyboard_notify_enter(seat->seat, surface,
                pressed_sent_keycodes, nr_pressed_sent_keycodes, &kb->modifiers);
 
+       input_method_relay_set_focus(seat->input_method_relay, surface);
+
        struct wlr_pointer_constraint_v1 *constraint =
                wlr_pointer_constraints_v1_constraint_for_surface(server->constraints,
                        surface, seat->seat);
index e1e48dd6cd62ff2160565b769a897078cdb7a9b2..5d57ff2f4634662dac31a43ad6144c1230b44256 100644 (file)
@@ -384,6 +384,10 @@ server_init(struct server *server)
         */
        wlr_primary_selection_v1_device_manager_create(server->wl_display);
 
+       server->input_method_manager = wlr_input_method_manager_v2_create(
+               server->wl_display);
+       server->text_input_manager = wlr_text_input_manager_v3_create(
+               server->wl_display);
        seat_init(server);
        xdg_shell_init(server);
        kde_server_decoration_init(server);