blob: b65dee40299a3db1e1d695bf5b2e802062936774 [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/gtk/input_method_context_impl_gtk.h"
#include <cstddef>
#include "base/strings/utf_string_conversions.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/ime/composition_text.h"
#include "ui/base/ime/linux/composition_text_util_pango.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/base/ime/text_input_flags.h"
#include "ui/events/event.h"
#include "ui/events/event_utils.h"
#include "ui/gfx/geometry/dip_util.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/native_widget_types.h"
#include "ui/gtk/gtk_compat.h"
#include "ui/gtk/gtk_ui.h"
#include "ui/gtk/gtk_ui_platform.h"
#include "ui/gtk/gtk_util.h"
#include "ui/linux/linux_ui.h"
namespace gtk {
namespace {
// Get IME KeyEvent's target window. Assumes root aura::Window is set to
// Event::target(), otherwise returns null.
GdkWindow* GetTargetWindow(const ui::KeyEvent& key_event) {
if (!key_event.target()) {
return nullptr;
}
aura::Window* window = static_cast<aura::Window*>(key_event.target());
DCHECK(window) << "KeyEvent target window not set.";
auto window_id = window->GetHost()->GetAcceleratedWidget();
return GtkUi::GetPlatform()->GetGdkWindow(window_id);
}
int GetKeyEventProperty(const ui::KeyEvent& key_event,
const char* property_key) {
auto* properties = key_event.properties();
if (!properties) {
return 0;
}
auto it = properties->find(property_key);
DCHECK(it == properties->end() || it->second.size() == 1);
return (it != properties->end()) ? it->second[0] : 0;
}
GdkModifierType GetGdkKeyEventState(const ui::KeyEvent& key_event) {
// ui::KeyEvent uses a normalized modifier state which is not respected by
// Gtk, so instead we obtain the original value from annotated properties.
// See also x11_event_translation.cc where it is annotated.
// cf) https://crbug.com/1086946#c11.
const ui::Event::Properties* properties = key_event.properties();
if (!properties) {
return static_cast<GdkModifierType>(0);
}
auto it = properties->find(ui::kPropertyKeyboardState);
if (it == properties->end()) {
return static_cast<GdkModifierType>(0);
}
DCHECK_EQ(it->second.size(), 4u);
// Stored in little endian.
int result = 0;
int bitshift = 0;
for (uint8_t value : it->second) {
result |= value << bitshift;
bitshift += 8;
}
return static_cast<GdkModifierType>(result);
}
// Xkb Events store group attribute into XKeyEvent::state bit field, along with
// other state-related info, while GdkEventKey objects have separate fields for
// that purpose, they are ::state and ::group. This function is responsible for
// recomposing them into a single bit field value when translating GdkEventKey
// into XKeyEvent. This is similar to XkbBuildCoreState(), but assumes state is
// an uint rather than an uchar.
//
// More details:
// https://gitlab.freedesktop.org/xorg/proto/xorgproto/blob/master/include/X11/extensions/XKB.h#L372
int BuildXkbStateFromGdkEvent(unsigned int state, unsigned char group) {
return state | ((group & 0x3) << 13);
}
// Translate IME ui::KeyEvent to a GdkEventKey.
GdkEventKey* GdkEventFromImeKeyEvent(const ui::KeyEvent& key_event) {
DCHECK(!GtkCheckVersion(4));
GdkEventType event_type = key_event.type() == ui::EventType::kKeyPressed
? GdkKeyPress()
: GdkKeyRelease();
auto event_time = key_event.time_stamp() - base::TimeTicks();
int hw_code = GetKeyEventProperty(key_event, ui::kPropertyKeyboardHwKeyCode);
int group = GetKeyEventProperty(key_event, ui::kPropertyKeyboardGroup);
GdkKeymap* keymap = gdk_keymap_get_for_display(gdk_display_get_default());
// Get keyval and state
GdkModifierType state = GetGdkKeyEventState(key_event);
guint keyval = GDK_KEY_VoidSymbol;
GdkModifierType consumed;
gdk_keymap_translate_keyboard_state(keymap, hw_code, state, group, &keyval,
nullptr, nullptr, &consumed);
gdk_keymap_add_virtual_modifiers(keymap, &state);
DCHECK_NE(keyval, static_cast<guint>(GDK_KEY_VoidSymbol));
// Build GdkEvent
GdkEvent* gdk_event = gdk_event_new(event_type);
GdkEventKey* gdk_event_key = reinterpret_cast<GdkEventKey*>(gdk_event);
gdk_event_key->type = event_type;
gdk_event_key->time = event_time.InMilliseconds();
gdk_event_key->hardware_keycode = hw_code;
gdk_event_key->keyval = keyval;
gdk_event_key->state = BuildXkbStateFromGdkEvent(state, group);
gdk_event_key->group = group;
gdk_event_key->send_event = key_event.flags() & ui::EF_FINAL;
gdk_event_key->is_modifier = state & GDK_MODIFIER_MASK;
gdk_event_key->length = 0;
gdk_event_key->string = nullptr;
return gdk_event_key;
}
} // namespace
InputMethodContextImplGtk::InputMethodContextImplGtk(
ui::LinuxInputMethodContextDelegate* delegate)
: delegate_(delegate) {
CHECK(delegate_);
gtk_context_ = TakeGObject(gtk_im_multicontext_new());
static const char kAllowGtkWaylandIm[] = "allow-gtk-wayland-im";
static const gchar* const kContextIdWayland = "wayland";
static const gchar* kContextIdIbus = "ibus";
const gchar* context_id = gtk_im_multicontext_get_context_id(
GTK_IM_MULTICONTEXT(gtk_context_.get()));
// switch to allow wayland IM module if it is picked.
if (context_id) {
if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
kAllowGtkWaylandIm) &&
(std::string(context_id) == kContextIdWayland)) {
// The wayland IM module doesn't work at all because of our usage of dummy
// window. So try using ibus module instead. Direct Wayland IM integration
// is being tracked under crbug.com/40113488.
// TODO(crbug.com/40801194) Remove this if dummy window is no longer used.
VLOG(1) << "Overriding wayland IM context to ibus";
gtk_im_multicontext_set_context_id(
GTK_IM_MULTICONTEXT(gtk_context_.get()), kContextIdIbus);
} else {
// This is the case where a non-wayland IM module is picked as per the
// user's configuration.
VLOG(1) << "Using GTK IM context: " << context_id;
}
}
gtk_simple_context_ = TakeGObject(gtk_im_context_simple_new());
auto connect = [&](const char* detailed_signal, auto receiver) {
for (auto context : {gtk_context_, gtk_simple_context_}) {
// Unretained() is safe since InputMethodContextImplGtk will own the
// ScopedGSignal.
signals_.emplace_back(
context, detailed_signal,
base::BindRepeating(receiver, base::Unretained(this)));
}
};
connect("commit", &InputMethodContextImplGtk::OnCommit);
connect("preedit-changed", &InputMethodContextImplGtk::OnPreeditChanged);
connect("preedit-end", &InputMethodContextImplGtk::OnPreeditEnd);
connect("preedit-start", &InputMethodContextImplGtk::OnPreeditStart);
// TODO(shuchen): Handle operations on surrounding text.
// "delete-surrounding" and "retrieve-surrounding" signals should be
// handled.
if (GtkCheckVersion(4)) {
auto* dummy_window = GetDummyWindow();
gtk_im_context_set_client_widget(gtk_context_, dummy_window);
gtk_im_context_set_client_widget(gtk_simple_context_, dummy_window);
}
}
InputMethodContextImplGtk::~InputMethodContextImplGtk() = default;
// Overridden from ui::LinuxInputMethodContext
bool InputMethodContextImplGtk::DispatchKeyEvent(
const ui::KeyEvent& key_event) {
auto* gtk_context = GetIMContext();
if (!gtk_context) {
return false;
}
GdkEventKey* event = nullptr;
if (!GtkCheckVersion(4)) {
event = GdkEventFromImeKeyEvent(key_event);
DCHECK(event);
auto* window = GetTargetWindow(key_event);
event->window = window;
auto& last_set_client_window =
gtk_context == gtk_context_
? last_set_client_window_for_gtk_context_
: last_set_client_window_for_gtk_simple_comtext_;
if (last_set_client_window != window) {
gtk_im_context_set_client_window(gtk_context, window);
last_set_client_window = window;
}
}
// Convert the last known caret bounds relative to the screen coordinates
// to a GdkRectangle relative to the client window.
aura::Window* window = static_cast<aura::Window*>(key_event.target());
gfx::Rect caret_bounds;
if (gtk_context == gtk_context_) {
caret_bounds = last_caret_bounds_;
}
// Use absolute coordinates on GTK4 since a dummy context window is provided
// to GTK at position (0, 0).
if (!GtkCheckVersion(4)) {
caret_bounds -= window->GetBoundsInScreen().OffsetFromOrigin();
}
// Chrome's DIPs may be different from GTK's DIPs if
// --force-device-scale-factor is used.
caret_bounds = ScaleToRoundedRect(
caret_bounds,
GetDeviceScaleFactor() / gtk_widget_get_scale_factor(GetDummyWindow()));
GdkRectangle gdk_rect = {caret_bounds.x(), caret_bounds.y(),
caret_bounds.width(), caret_bounds.height()};
gtk_im_context_set_cursor_location(gtk_context, &gdk_rect);
if (!GtkCheckVersion(4)) {
const bool handled = GtkImContextFilterKeypress(gtk_context, event);
gdk_event_free(reinterpret_cast<GdkEvent*>(event));
return handled;
}
// In GTK4, clients can no longer create or modify events. This makes using
// the gtk_im_context_filter_keypress() API impossible. Fortunately, an
// alternative API called gtk_im_context_filter_key() was added for clients
// that would have needed to construct their own event. The parameters to
// the new API are just a deconstructed version of a KeyEvent.
bool press = key_event.type() == ui::EventType::kKeyPressed;
auto* surface =
gtk_native_get_surface(gtk_widget_get_native(GetDummyWindow()));
auto* device = gdk_seat_get_keyboard(
gdk_display_get_default_seat(gdk_display_get_default()));
auto time = (key_event.time_stamp() - base::TimeTicks()).InMilliseconds();
auto keycode = GetKeyEventProperty(key_event, ui::kPropertyKeyboardHwKeyCode);
auto group = GetKeyEventProperty(key_event, ui::kPropertyKeyboardGroup);
auto state = GetGdkKeyEventState(key_event);
return gtk_im_context_filter_key(gtk_context, press, surface, device, time,
keycode, state, group);
}
bool InputMethodContextImplGtk::IsPeekKeyEvent(const ui::KeyEvent& key_event) {
// Peek events are only sent to make Lacros work with Wayland. Gtk does not
// send peek events.
return false;
}
void InputMethodContextImplGtk::Reset() {
gtk_im_context_reset(gtk_context_);
gtk_im_context_reset(gtk_simple_context_);
// Some input methods may not honour the reset call.
// Focusing out/in the to make sure it gets reset correctly.
if (type_ != ui::TEXT_INPUT_TYPE_NONE) {
gtk_im_context_focus_out(gtk_context_);
gtk_im_context_focus_in(gtk_context_);
}
}
void InputMethodContextImplGtk::UpdateFocus(
bool has_client,
ui::TextInputType old_type,
const TextInputClientAttributes& new_client_attributes,
ui::TextInputClient::FocusReason reason) {
type_ = new_client_attributes.input_type;
// We only focus when the focus is in a textfield.
if (old_type != ui::TEXT_INPUT_TYPE_NONE) {
gtk_im_context_focus_out(gtk_context_);
}
if (new_client_attributes.input_type != ui::TEXT_INPUT_TYPE_NONE) {
gtk_im_context_focus_in(gtk_context_);
}
// simple context can be used in any textfield, including password box, and
// even if the focused text input client's text input type is
// ui::TEXT_INPUT_TYPE_NONE.
if (has_client) {
gtk_im_context_focus_in(gtk_simple_context_);
} else {
gtk_im_context_focus_out(gtk_simple_context_);
}
if (new_client_attributes.flags & ui::TEXT_INPUT_FLAG_VERTICAL) {
g_object_set(gtk_context_, "input-hints", GTK_INPUT_HINT_VERTICAL_WRITING,
nullptr);
g_object_set(gtk_simple_context_, "input-hints",
GTK_INPUT_HINT_VERTICAL_WRITING, nullptr);
}
}
void InputMethodContextImplGtk::SetCursorLocation(const gfx::Rect& rect) {
// Remember the caret bounds so that we can set the cursor location later.
// gtk_im_context_set_cursor_location() takes the location relative to the
// client window, which is unknown at this point. So we'll call
// gtk_im_context_set_cursor_location() later in DispatchKeyEvent() where
// (and only where) we know the client window.
last_caret_bounds_ = rect;
}
void InputMethodContextImplGtk::SetSurroundingText(
const std::u16string& text,
const gfx::Range& text_range,
const gfx::Range& composition_range,
const gfx::Range& selection_range) {}
// private:
// GtkIMContext event handlers.
void InputMethodContextImplGtk::OnCommit(GtkIMContext* context, gchar* text) {
if (context != GetIMContext()) {
return;
}
delegate_->OnCommit(base::UTF8ToUTF16(text));
}
void InputMethodContextImplGtk::OnPreeditChanged(GtkIMContext* context) {
if (context != GetIMContext()) {
return;
}
gchar* str = nullptr;
PangoAttrList* attrs = nullptr;
gint cursor_pos = 0;
gtk_im_context_get_preedit_string(context, &str, &attrs, &cursor_pos);
ui::CompositionText composition_text;
ui::ExtractCompositionTextFromGtkPreedit(str, attrs, cursor_pos,
&composition_text);
g_free(str);
pango_attr_list_unref(attrs);
delegate_->OnPreeditChanged(composition_text);
}
void InputMethodContextImplGtk::OnPreeditEnd(GtkIMContext* context) {
if (context != GetIMContext()) {
return;
}
delegate_->OnPreeditEnd();
}
void InputMethodContextImplGtk::OnPreeditStart(GtkIMContext* context) {
if (context != GetIMContext()) {
return;
}
delegate_->OnPreeditStart();
}
ui::VirtualKeyboardController*
InputMethodContextImplGtk::GetVirtualKeyboardController() {
return nullptr;
}
GtkIMContext* InputMethodContextImplGtk::GetIMContext() {
switch (type_) {
case ui::TEXT_INPUT_TYPE_NONE:
case ui::TEXT_INPUT_TYPE_PASSWORD:
return gtk_simple_context_;
default:
return gtk_context_;
}
}
} // namespace gtk