Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 1 | // Copyright 2013 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
Tom Anderson | 61fbaaf | 2020-04-02 23:16:23 | [diff] [blame] | 5 | #include "ui/gtk/input_method_context_impl_gtk.h" |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 6 | |
Tom Anderson | bf89dc6 | 2021-04-20 20:19:59 | [diff] [blame] | 7 | #include <cstddef> |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 8 | |
| 9 | #include "base/strings/utf_string_conversions.h" |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 10 | #include "ui/aura/window_tree_host.h" |
| 11 | #include "ui/base/ime/composition_text.h" |
| 12 | #include "ui/base/ime/linux/composition_text_util_pango.h" |
| 13 | #include "ui/base/ime/text_input_client.h" |
| 14 | #include "ui/events/event.h" |
Tom Anderson | 371899e9 | 2021-03-15 20:16:22 | [diff] [blame] | 15 | #include "ui/events/event_utils.h" |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 16 | #include "ui/gfx/geometry/dip_util.h" |
danakj | 340909ed | 2020-09-30 17:40:56 | [diff] [blame] | 17 | #include "ui/gfx/geometry/rect_conversions.h" |
Nick Diego Yamane | 4079d37 | 2020-03-28 00:43:01 | [diff] [blame] | 18 | #include "ui/gfx/native_widget_types.h" |
Tom Anderson | 1a799790 | 2021-04-06 20:24:56 | [diff] [blame] | 19 | #include "ui/gtk/gtk_compat.h" |
Tom Anderson | 61fbaaf | 2020-04-02 23:16:23 | [diff] [blame] | 20 | #include "ui/gtk/gtk_ui.h" |
Tom Anderson | bf89dc6 | 2021-04-20 20:19:59 | [diff] [blame] | 21 | #include "ui/gtk/gtk_ui_platform.h" |
Tom Anderson | 61fbaaf | 2020-04-02 23:16:23 | [diff] [blame] | 22 | #include "ui/gtk/gtk_util.h" |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 23 | #include "ui/views/linux_ui/linux_ui.h" |
| 24 | |
Tom Anderson | 5630ac27 | 2020-01-23 02:39:20 | [diff] [blame] | 25 | namespace gtk { |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 26 | |
| 27 | namespace { |
| 28 | |
Tom Anderson | 1a799790 | 2021-04-06 20:24:56 | [diff] [blame] | 29 | GdkEventKey* GdkEventToKey(GdkEvent* event) { |
| 30 | DCHECK(!GtkCheckVersion(4)); |
| 31 | auto* key = reinterpret_cast<GdkEventKey*>(event); |
Tom Anderson | 699a51f | 2021-04-21 00:12:49 | [diff] [blame] | 32 | DCHECK(key->type == GdkKeyPress() || key->type == GdkKeyRelease()); |
Tom Anderson | 1a799790 | 2021-04-06 20:24:56 | [diff] [blame] | 33 | return key; |
| 34 | } |
| 35 | |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 36 | // Get IME KeyEvent's target window. Assumes root aura::Window is set to |
| 37 | // Event::target(), otherwise returns null. |
| 38 | GdkWindow* GetTargetWindow(const ui::KeyEvent& key_event) { |
| 39 | if (!key_event.target()) |
| 40 | return nullptr; |
| 41 | |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 42 | aura::Window* window = static_cast<aura::Window*>(key_event.target()); |
Nick Diego Yamane | 4079d37 | 2020-03-28 00:43:01 | [diff] [blame] | 43 | DCHECK(window) << "KeyEvent target window not set."; |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 44 | |
Nick Diego Yamane | 4079d37 | 2020-03-28 00:43:01 | [diff] [blame] | 45 | auto window_id = window->GetHost()->GetAcceleratedWidget(); |
Tom Anderson | bf89dc6 | 2021-04-20 20:19:59 | [diff] [blame] | 46 | return GtkUi::GetPlatform()->GetGdkWindow(window_id); |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 47 | } |
| 48 | |
| 49 | // Translate IME ui::KeyEvent to a GdkEventKey. |
| 50 | GdkEvent* GdkEventFromImeKeyEvent(const ui::KeyEvent& key_event) { |
Tom Anderson | 1a799790 | 2021-04-06 20:24:56 | [diff] [blame] | 51 | DCHECK(!GtkCheckVersion(4)); |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 52 | GdkEvent* event = GdkEventFromKeyEvent(key_event); |
| 53 | if (!event) |
| 54 | return nullptr; |
| 55 | |
| 56 | GdkWindow* target_window = GetTargetWindow(key_event); |
| 57 | if (!target_window) { |
| 58 | gdk_event_free(event); |
| 59 | return nullptr; |
| 60 | } |
Tom Anderson | 1a799790 | 2021-04-06 20:24:56 | [diff] [blame] | 61 | GdkEventToKey(event)->window = target_window; |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 62 | return event; |
| 63 | } |
| 64 | |
| 65 | } // namespace |
| 66 | |
| 67 | InputMethodContextImplGtk::InputMethodContextImplGtk( |
| 68 | ui::LinuxInputMethodContextDelegate* delegate, |
| 69 | bool is_simple) |
Tom Anderson | 371899e9 | 2021-03-15 20:16:22 | [diff] [blame] | 70 | : delegate_(delegate), is_simple_(is_simple) { |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 71 | CHECK(delegate_); |
| 72 | |
| 73 | gtk_context_ = |
| 74 | is_simple ? gtk_im_context_simple_new() : gtk_im_multicontext_new(); |
| 75 | |
| 76 | g_signal_connect(gtk_context_, "commit", G_CALLBACK(OnCommitThunk), this); |
| 77 | g_signal_connect(gtk_context_, "preedit-changed", |
| 78 | G_CALLBACK(OnPreeditChangedThunk), this); |
| 79 | g_signal_connect(gtk_context_, "preedit-end", G_CALLBACK(OnPreeditEndThunk), |
| 80 | this); |
| 81 | g_signal_connect(gtk_context_, "preedit-start", |
| 82 | G_CALLBACK(OnPreeditStartThunk), this); |
| 83 | // TODO(shuchen): Handle operations on surrounding text. |
| 84 | // "delete-surrounding" and "retrieve-surrounding" signals should be |
| 85 | // handled. |
Tom Anderson | 371899e9 | 2021-03-15 20:16:22 | [diff] [blame] | 86 | |
Tom Anderson | 1a799790 | 2021-04-06 20:24:56 | [diff] [blame] | 87 | if (GtkCheckVersion(4)) |
| 88 | gtk_im_context_set_client_widget(gtk_context_, GetDummyWindow()); |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 89 | } |
| 90 | |
| 91 | InputMethodContextImplGtk::~InputMethodContextImplGtk() { |
| 92 | if (gtk_context_) { |
| 93 | g_object_unref(gtk_context_); |
| 94 | gtk_context_ = nullptr; |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | // Overridden from ui::LinuxInputMethodContext |
| 99 | bool InputMethodContextImplGtk::DispatchKeyEvent( |
| 100 | const ui::KeyEvent& key_event) { |
| 101 | if (!gtk_context_) |
| 102 | return false; |
| 103 | |
Tom Anderson | 1a799790 | 2021-04-06 20:24:56 | [diff] [blame] | 104 | GdkEvent* event = nullptr; |
| 105 | if (!GtkCheckVersion(4)) { |
| 106 | event = GdkEventFromImeKeyEvent(key_event); |
| 107 | if (!event) { |
| 108 | LOG(ERROR) << "Cannot translate a Keyevent to a GdkEvent."; |
| 109 | return false; |
| 110 | } |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 111 | |
Tom Anderson | 1a799790 | 2021-04-06 20:24:56 | [diff] [blame] | 112 | GdkWindow* target_window = GdkEventToKey(event)->window; |
| 113 | if (!target_window) { |
| 114 | LOG(ERROR) << "Cannot get target GdkWindow for KeyEvent."; |
| 115 | return false; |
| 116 | } |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 117 | |
Tom Anderson | 1a799790 | 2021-04-06 20:24:56 | [diff] [blame] | 118 | SetContextClientWindow(target_window); |
| 119 | } |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 120 | |
| 121 | // Convert the last known caret bounds relative to the screen coordinates |
| 122 | // to a GdkRectangle relative to the client window. |
Tom Anderson | 6a8b2d13 | 2021-03-26 02:04:50 | [diff] [blame] | 123 | aura::Window* window = static_cast<aura::Window*>(key_event.target()); |
| 124 | gint win_x = window->GetBoundsInScreen().x(); |
| 125 | gint win_y = window->GetBoundsInScreen().y(); |
| 126 | gint caret_x = last_caret_bounds_.x(); |
| 127 | gint caret_y = last_caret_bounds_.y(); |
| 128 | gint caret_w = last_caret_bounds_.width(); |
| 129 | gint caret_h = last_caret_bounds_.height(); |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 130 | GdkRectangle gdk_rect = {caret_x - win_x, caret_y - win_y, caret_w, caret_h}; |
| 131 | gtk_im_context_set_cursor_location(gtk_context_, &gdk_rect); |
| 132 | |
Tom Anderson | 1a799790 | 2021-04-06 20:24:56 | [diff] [blame] | 133 | if (!GtkCheckVersion(4)) { |
| 134 | const bool handled = |
| 135 | GtkImContextFilterKeypress(gtk_context_, GdkEventToKey(event)); |
| 136 | gdk_event_free(event); |
| 137 | return handled; |
| 138 | } |
Tom Anderson | 371899e9 | 2021-03-15 20:16:22 | [diff] [blame] | 139 | // In GTK4, clients can no longer create or modify events. This makes using |
| 140 | // the gtk_im_context_filter_keypress() API impossible. Fortunately, an |
| 141 | // alternative API called gtk_im_context_filter_key() was added for clients |
| 142 | // that would have needed to construct their own event. The parameters to |
| 143 | // the new API are just a deconstructed version of a KeyEvent. |
| 144 | bool press = key_event.type() == ui::ET_KEY_PRESSED; |
| 145 | auto* surface = |
| 146 | gtk_native_get_surface(gtk_widget_get_native(GetDummyWindow())); |
| 147 | auto* device = gdk_seat_get_keyboard( |
| 148 | gdk_display_get_default_seat(gdk_display_get_default())); |
| 149 | auto time = (key_event.time_stamp() - base::TimeTicks()).InMilliseconds(); |
| 150 | auto keycode = GetKeyEventProperty(key_event, ui::kPropertyKeyboardHwKeyCode); |
| 151 | auto state = GetGdkKeyEventState(key_event); |
| 152 | auto group = GetKeyEventProperty(key_event, ui::kPropertyKeyboardGroup); |
| 153 | return gtk_im_context_filter_key(gtk_context_, press, surface, device, time, |
| 154 | keycode, state, group); |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 155 | } |
| 156 | |
| 157 | void InputMethodContextImplGtk::Reset() { |
| 158 | gtk_im_context_reset(gtk_context_); |
| 159 | |
| 160 | // Some input methods may not honour the reset call. |
| 161 | // Focusing out/in the to make sure it gets reset correctly. |
| 162 | if (!is_simple_ && has_focus_) { |
| 163 | Blur(); |
| 164 | Focus(); |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | void InputMethodContextImplGtk::Focus() { |
| 169 | gtk_im_context_focus_in(gtk_context_); |
| 170 | has_focus_ = true; |
| 171 | } |
| 172 | |
| 173 | void InputMethodContextImplGtk::Blur() { |
| 174 | gtk_im_context_focus_out(gtk_context_); |
| 175 | has_focus_ = false; |
| 176 | } |
| 177 | |
| 178 | void InputMethodContextImplGtk::SetCursorLocation(const gfx::Rect& rect) { |
| 179 | // Remember the caret bounds so that we can set the cursor location later. |
| 180 | // gtk_im_context_set_cursor_location() takes the location relative to the |
| 181 | // client window, which is unknown at this point. So we'll call |
Tom Anderson | 6a8b2d13 | 2021-03-26 02:04:50 | [diff] [blame] | 182 | // gtk_im_context_set_cursor_location() later in DispatchKeyEvent() where |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 183 | // (and only where) we know the client window. |
Tom Anderson | 6a8b2d13 | 2021-03-26 02:04:50 | [diff] [blame] | 184 | last_caret_bounds_ = rect; |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 185 | } |
| 186 | |
| 187 | void InputMethodContextImplGtk::SetSurroundingText( |
Jan Wilken Dörrie | 5263957 | 2021-03-11 16:49:54 | [diff] [blame] | 188 | const std::u16string& text, |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 189 | const gfx::Range& selection_range) {} |
| 190 | |
| 191 | // private: |
| 192 | |
| 193 | // GtkIMContext event handlers. |
| 194 | |
| 195 | void InputMethodContextImplGtk::OnCommit(GtkIMContext* context, gchar* text) { |
| 196 | if (context != gtk_context_) |
| 197 | return; |
| 198 | |
| 199 | delegate_->OnCommit(base::UTF8ToUTF16(text)); |
| 200 | } |
| 201 | |
| 202 | void InputMethodContextImplGtk::OnPreeditChanged(GtkIMContext* context) { |
| 203 | if (context != gtk_context_) |
| 204 | return; |
| 205 | |
| 206 | gchar* str = nullptr; |
| 207 | PangoAttrList* attrs = nullptr; |
| 208 | gint cursor_pos = 0; |
| 209 | gtk_im_context_get_preedit_string(context, &str, &attrs, &cursor_pos); |
| 210 | ui::CompositionText composition_text; |
| 211 | ui::ExtractCompositionTextFromGtkPreedit(str, attrs, cursor_pos, |
| 212 | &composition_text); |
| 213 | g_free(str); |
| 214 | pango_attr_list_unref(attrs); |
| 215 | |
| 216 | delegate_->OnPreeditChanged(composition_text); |
| 217 | } |
| 218 | |
| 219 | void InputMethodContextImplGtk::OnPreeditEnd(GtkIMContext* context) { |
| 220 | if (context != gtk_context_) |
| 221 | return; |
| 222 | |
| 223 | delegate_->OnPreeditEnd(); |
| 224 | } |
| 225 | |
| 226 | void InputMethodContextImplGtk::OnPreeditStart(GtkIMContext* context) { |
| 227 | if (context != gtk_context_) |
| 228 | return; |
| 229 | |
| 230 | delegate_->OnPreeditStart(); |
| 231 | } |
| 232 | |
| 233 | void InputMethodContextImplGtk::SetContextClientWindow(GdkWindow* window) { |
Tom Anderson | 1a799790 | 2021-04-06 20:24:56 | [diff] [blame] | 234 | DCHECK(!GtkCheckVersion(4)); |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 235 | if (window == gdk_last_set_client_window_) |
| 236 | return; |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 237 | gtk_im_context_set_client_window(gtk_context_, window); |
Nick Diego Yamane | 5c9d092b | 2019-09-18 00:55:18 | [diff] [blame] | 238 | |
| 239 | // Prevent leaks when overriding last client window |
| 240 | if (gdk_last_set_client_window_) |
| 241 | g_object_unref(gdk_last_set_client_window_); |
| 242 | gdk_last_set_client_window_ = window; |
| 243 | } |
| 244 | |
Tom Anderson | 5630ac27 | 2020-01-23 02:39:20 | [diff] [blame] | 245 | } // namespace gtk |