/* GTK - The GIMP Toolkit * Copyright (C) 2017 Red Hat, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "config.h" #include #include #include "gtk/gtkimcontextwayland.h" #include "gtk/gtkintl.h" #include "gtk/gtkimmoduleprivate.h" #include "gdk/wayland/gdkwayland.h" #include "gtk-text-input-client-protocol.h" typedef struct _GtkIMContextWaylandGlobal GtkIMContextWaylandGlobal; typedef struct _GtkIMContextWayland GtkIMContextWayland; typedef struct _GtkIMContextWaylandClass GtkIMContextWaylandClass; struct _GtkIMContextWaylandGlobal { struct wl_display *display; struct wl_registry *registry; uint32_t text_input_manager_wl_id; struct gtk_text_input_manager *text_input_manager; struct gtk_text_input *text_input; uint32_t enter_serial; GtkIMContext *current; }; struct _GtkIMContextWaylandClass { GtkIMContextSimpleClass parent_class; }; struct _GtkIMContextWayland { GtkIMContextSimple parent_instance; GtkWidget *widget; GtkGesture *gesture; gdouble press_x; gdouble press_y; struct { gchar *text; gint cursor_idx; } surrounding; struct { gchar *text; gint cursor_idx; } preedit; cairo_rectangle_int_t cursor_rect; guint use_preedit : 1; }; G_DEFINE_TYPE_WITH_CODE (GtkIMContextWayland, gtk_im_context_wayland, GTK_TYPE_IM_CONTEXT_SIMPLE, gtk_im_module_ensure_extension_point (); g_io_extension_point_implement (GTK_IM_MODULE_EXTENSION_POINT_NAME, g_define_type_id, "wayland", 10)); static GtkIMContextWaylandGlobal *global = NULL; #define GTK_IM_CONTEXT_WAYLAND(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), gtk_im_context_wayland_get_type (), GtkIMContextWayland)) static void reset_preedit (GtkIMContextWayland *context) { if (context->preedit.text == NULL) return; g_clear_pointer (&context->preedit.text, g_free); context->preedit.cursor_idx = 0; g_signal_emit_by_name (context, "preedit-changed"); } static void text_input_enter (void *data, struct gtk_text_input *text_input, uint32_t serial, struct wl_surface *surface) { GtkIMContextWaylandGlobal *global = data; global->enter_serial = serial; } static void text_input_leave (void *data, struct gtk_text_input *text_input, uint32_t serial, struct wl_surface *surface) { GtkIMContextWayland *context; if (!global->current) return; context = GTK_IM_CONTEXT_WAYLAND (global->current); reset_preedit (context); } static void text_input_preedit (void *data, struct gtk_text_input *text_input, const char *text, guint cursor) { GtkIMContextWayland *context; gboolean state_change; if (!global->current) return; context = GTK_IM_CONTEXT_WAYLAND (global->current); if (!text && !context->preedit.text) return; state_change = ((text == NULL) != (context->preedit.text == NULL)); if (state_change && !context->preedit.text) g_signal_emit_by_name (context, "preedit-start"); g_free (context->preedit.text); context->preedit.text = g_strdup (text); context->preedit.cursor_idx = cursor; g_signal_emit_by_name (context, "preedit-changed"); if (state_change && !context->preedit.text) g_signal_emit_by_name (context, "preedit-end"); } static void text_input_commit (void *data, struct gtk_text_input *text_input, const char *text) { GtkIMContextWaylandGlobal *global = data; if (global->current && text) g_signal_emit_by_name (global->current, "commit", text); } static void text_input_delete_surrounding_text (void *data, struct gtk_text_input *text_input, uint32_t offset, uint32_t len) { GtkIMContextWaylandGlobal *global = data; if (global->current) g_signal_emit_by_name (global->current, "delete-surrounding", offset, len); } static const struct gtk_text_input_listener text_input_listener = { text_input_enter, text_input_leave, text_input_preedit, text_input_commit, text_input_delete_surrounding_text }; static void registry_handle_global (void *data, struct wl_registry *registry, uint32_t id, const char *interface, uint32_t version) { GtkIMContextWaylandGlobal *global = data; GdkSeat *seat = gdk_display_get_default_seat (gdk_display_get_default ()); if (strcmp (interface, "gtk_text_input_manager") == 0) { global->text_input_manager_wl_id = id; global->text_input_manager = wl_registry_bind (global->registry, global->text_input_manager_wl_id, >k_text_input_manager_interface, 1); global->text_input = gtk_text_input_manager_get_text_input (global->text_input_manager, gdk_wayland_seat_get_wl_seat (seat)); gtk_text_input_add_listener (global->text_input, &text_input_listener, global); } } static void registry_handle_global_remove (void *data, struct wl_registry *registry, uint32_t id) { GtkIMContextWaylandGlobal *global = data; if (id != global->text_input_manager_wl_id) return; g_clear_pointer(&global->text_input, gtk_text_input_destroy); g_clear_pointer(&global->text_input_manager, gtk_text_input_manager_destroy); } static const struct wl_registry_listener registry_listener = { registry_handle_global, registry_handle_global_remove }; static void gtk_im_context_wayland_global_init (GdkDisplay *display) { if (global != NULL) return; global = g_new0 (GtkIMContextWaylandGlobal, 1); global->display = gdk_wayland_display_get_wl_display (display); global->registry = wl_display_get_registry (global->display); wl_registry_add_listener (global->registry, ®istry_listener, global); } static void notify_surrounding_text (GtkIMContextWayland *context) { if (!global || !global->text_input) return; if (global->current != GTK_IM_CONTEXT (context)) return; if (!context->surrounding.text) return; gtk_text_input_set_surrounding_text (global->text_input, context->surrounding.text, context->surrounding.cursor_idx, context->surrounding.cursor_idx); } static void notify_cursor_location (GtkIMContextWayland *context) { cairo_rectangle_int_t rect; if (!global || !global->text_input) return; if (global->current != GTK_IM_CONTEXT (context)) return; if (!context->widget) return; rect = context->cursor_rect; gtk_widget_translate_coordinates (context->widget, gtk_widget_get_toplevel (context->widget), rect.x, rect.y, &rect.x, &rect.y); gtk_text_input_set_cursor_rectangle (global->text_input, rect.x, rect.y, rect.width, rect.height); } static uint32_t translate_hints (GtkInputHints input_hints, GtkInputPurpose purpose) { uint32_t hints = 0; if (input_hints & GTK_INPUT_HINT_SPELLCHECK) hints |= GTK_TEXT_INPUT_CONTENT_HINT_SPELLCHECK; if (input_hints & GTK_INPUT_HINT_WORD_COMPLETION) hints |= GTK_TEXT_INPUT_CONTENT_HINT_COMPLETION; if (input_hints & GTK_INPUT_HINT_LOWERCASE) hints |= GTK_TEXT_INPUT_CONTENT_HINT_LOWERCASE; if (input_hints & GTK_INPUT_HINT_UPPERCASE_CHARS) hints |= GTK_TEXT_INPUT_CONTENT_HINT_UPPERCASE; if (input_hints & GTK_INPUT_HINT_UPPERCASE_WORDS) hints |= GTK_TEXT_INPUT_CONTENT_HINT_TITLECASE; if (input_hints & GTK_INPUT_HINT_UPPERCASE_SENTENCES) hints |= GTK_TEXT_INPUT_CONTENT_HINT_AUTO_CAPITALIZATION; if (purpose == GTK_INPUT_PURPOSE_PIN || purpose == GTK_INPUT_PURPOSE_PASSWORD) { hints |= (GTK_TEXT_INPUT_CONTENT_HINT_HIDDEN_TEXT | GTK_TEXT_INPUT_CONTENT_HINT_SENSITIVE_DATA); } return hints; } static uint32_t translate_purpose (GtkInputPurpose purpose) { switch (purpose) { case GTK_INPUT_PURPOSE_FREE_FORM: return GTK_TEXT_INPUT_CONTENT_PURPOSE_NORMAL; case GTK_INPUT_PURPOSE_ALPHA: return GTK_TEXT_INPUT_CONTENT_PURPOSE_ALPHA; case GTK_INPUT_PURPOSE_DIGITS: return GTK_TEXT_INPUT_CONTENT_PURPOSE_DIGITS; case GTK_INPUT_PURPOSE_NUMBER: return GTK_TEXT_INPUT_CONTENT_PURPOSE_NUMBER; case GTK_INPUT_PURPOSE_PHONE: return GTK_TEXT_INPUT_CONTENT_PURPOSE_PHONE; case GTK_INPUT_PURPOSE_URL: return GTK_TEXT_INPUT_CONTENT_PURPOSE_URL; case GTK_INPUT_PURPOSE_EMAIL: return GTK_TEXT_INPUT_CONTENT_PURPOSE_EMAIL; case GTK_INPUT_PURPOSE_NAME: return GTK_TEXT_INPUT_CONTENT_PURPOSE_NAME; case GTK_INPUT_PURPOSE_PASSWORD: return GTK_TEXT_INPUT_CONTENT_PURPOSE_PASSWORD; case GTK_INPUT_PURPOSE_PIN: return GTK_TEXT_INPUT_CONTENT_PURPOSE_PIN; default: g_assert_not_reached (); } return GTK_TEXT_INPUT_CONTENT_PURPOSE_NORMAL; } static void notify_content_type (GtkIMContextWayland *context) { GtkInputHints hints; GtkInputPurpose purpose; if (global->current != GTK_IM_CONTEXT (context)) return; g_object_get (context, "input-hints", &hints, "input-purpose", &purpose, NULL); gtk_text_input_set_content_type (global->text_input, translate_hints (hints, purpose), translate_purpose (purpose)); } static void commit_state (GtkIMContextWayland *context) { if (global->current != GTK_IM_CONTEXT (context)) return; gtk_text_input_commit (global->text_input); } static void enable_text_input (GtkIMContextWayland *context, gboolean toggle_panel) { guint flags = 0; if (context->use_preedit) flags |= GTK_TEXT_INPUT_ENABLE_FLAGS_CAN_SHOW_PREEDIT; if (toggle_panel) flags |= GTK_TEXT_INPUT_ENABLE_FLAGS_TOGGLE_INPUT_PANEL; gtk_text_input_enable (global->text_input, global->enter_serial, flags); } static void gtk_im_context_wayland_finalize (GObject *object) { GtkIMContextWayland *context = GTK_IM_CONTEXT_WAYLAND (object); g_clear_object (&context->widget); g_clear_object (&context->gesture); G_OBJECT_CLASS (gtk_im_context_wayland_parent_class)->finalize (object); } static void pressed_cb (GtkGestureMultiPress *gesture, gint n_press, gdouble x, gdouble y, GtkIMContextWayland *context) { if (n_press == 1) { context->press_x = x; context->press_y = y; } } static void released_cb (GtkGestureMultiPress *gesture, gint n_press, gdouble x, gdouble y, GtkIMContextWayland *context) { GtkInputHints hints; g_object_get (context, "input-hints", &hints, NULL); if (n_press == 1 && (hints & GTK_INPUT_HINT_INHIBIT_OSK) == 0 && !gtk_drag_check_threshold (context->widget, context->press_x, context->press_y, x, y)) { enable_text_input (GTK_IM_CONTEXT_WAYLAND (context), TRUE); } } static void gtk_im_context_wayland_set_client_widget (GtkIMContext *context, GtkWidget *widget) { GtkIMContextWayland *context_wayland = GTK_IM_CONTEXT_WAYLAND (context); if (widget == context_wayland->widget) return; if (context_wayland->widget && context_wayland->widget != widget) g_clear_object (&context_wayland->gesture); g_set_object (&context_wayland->widget, widget); if (widget) { GtkGesture *gesture; gesture = gtk_gesture_multi_press_new (widget); gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (gesture), GTK_PHASE_CAPTURE); g_signal_connect (gesture, "pressed", G_CALLBACK (pressed_cb), context); g_signal_connect (gesture, "released", G_CALLBACK (released_cb), context); context_wayland->gesture = gesture; } } static void gtk_im_context_wayland_get_preedit_string (GtkIMContext *context, gchar **str, PangoAttrList **attrs, gint *cursor_pos) { GtkIMContextWayland *context_wayland = GTK_IM_CONTEXT_WAYLAND (context); const char *preedit_str; GTK_IM_CONTEXT_CLASS (gtk_im_context_wayland_parent_class)->get_preedit_string (context, str, attrs, cursor_pos); /* If the parent implementation returns a len>0 string, go with it */ if (str && *str && **str) return; preedit_str = context_wayland->preedit.text ? context_wayland->preedit.text : ""; if (str) *str = g_strdup (preedit_str); if (cursor_pos) *cursor_pos = context_wayland->preedit.cursor_idx; if (attrs) { *attrs = pango_attr_list_new (); pango_attr_list_insert (*attrs, pango_attr_underline_new (PANGO_UNDERLINE_SINGLE)); } } static gboolean gtk_im_context_wayland_filter_keypress (GtkIMContext *context, GdkEventKey *key) { /* This is done by the compositor */ return GTK_IM_CONTEXT_CLASS (gtk_im_context_wayland_parent_class)->filter_keypress (context, key); } static void gtk_im_context_wayland_focus_in (GtkIMContext *context) { GtkIMContextWayland *context_wayland = GTK_IM_CONTEXT_WAYLAND (context); if (global->current == context) return; if (!global->text_input) return; global->current = context; enable_text_input (context_wayland, FALSE); notify_content_type (context_wayland); notify_surrounding_text (context_wayland); notify_cursor_location (context_wayland); commit_state (context_wayland); } static void gtk_im_context_wayland_focus_out (GtkIMContext *context) { if (global->current != context) return; gtk_text_input_disable (global->text_input); global->current = NULL; } static void gtk_im_context_wayland_reset (GtkIMContext *context) { reset_preedit (GTK_IM_CONTEXT_WAYLAND (context)); GTK_IM_CONTEXT_CLASS (gtk_im_context_wayland_parent_class)->reset (context); } static void gtk_im_context_wayland_set_cursor_location (GtkIMContext *context, GdkRectangle *rect) { GtkIMContextWayland *context_wayland; context_wayland = GTK_IM_CONTEXT_WAYLAND (context); context_wayland->cursor_rect = *rect; notify_cursor_location (context_wayland); commit_state (context_wayland); } static void gtk_im_context_wayland_set_use_preedit (GtkIMContext *context, gboolean use_preedit) { GtkIMContextWayland *context_wayland = GTK_IM_CONTEXT_WAYLAND (context); context_wayland->use_preedit = !!use_preedit; } static void gtk_im_context_wayland_set_surrounding (GtkIMContext *context, const gchar *text, gint len, gint cursor_index) { GtkIMContextWayland *context_wayland; context_wayland = GTK_IM_CONTEXT_WAYLAND (context); g_free (context_wayland->surrounding.text); context_wayland->surrounding.text = g_strdup (text); context_wayland->surrounding.cursor_idx = cursor_index; notify_surrounding_text (context_wayland); commit_state (context_wayland); } static gboolean gtk_im_context_wayland_get_surrounding (GtkIMContext *context, gchar **text, gint *cursor_index) { GtkIMContextWayland *context_wayland; context_wayland = GTK_IM_CONTEXT_WAYLAND (context); if (!context_wayland->surrounding.text) return FALSE; *text = context_wayland->surrounding.text; *cursor_index = context_wayland->surrounding.cursor_idx; return TRUE; } static void gtk_im_context_wayland_class_init (GtkIMContextWaylandClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkIMContextClass *im_context_class = GTK_IM_CONTEXT_CLASS (klass); object_class->finalize = gtk_im_context_wayland_finalize; im_context_class->set_client_widget = gtk_im_context_wayland_set_client_widget; im_context_class->get_preedit_string = gtk_im_context_wayland_get_preedit_string; im_context_class->filter_keypress = gtk_im_context_wayland_filter_keypress; im_context_class->focus_in = gtk_im_context_wayland_focus_in; im_context_class->focus_out = gtk_im_context_wayland_focus_out; im_context_class->reset = gtk_im_context_wayland_reset; im_context_class->set_cursor_location = gtk_im_context_wayland_set_cursor_location; im_context_class->set_use_preedit = gtk_im_context_wayland_set_use_preedit; im_context_class->set_surrounding = gtk_im_context_wayland_set_surrounding; im_context_class->get_surrounding = gtk_im_context_wayland_get_surrounding; } static void on_content_type_changed (GtkIMContextWayland *context) { notify_content_type (context); commit_state (context); } static void gtk_im_context_wayland_init (GtkIMContextWayland *context) { gtk_im_context_wayland_global_init (gdk_display_get_default ()); context->use_preedit = TRUE; g_signal_connect_swapped (context, "notify::input-purpose", G_CALLBACK (on_content_type_changed), context); g_signal_connect_swapped (context, "notify::input-hints", G_CALLBACK (on_content_type_changed), context); }