0

Reland "[GTK] Detect GTK4 IME incompatibilities"

This is a reland of commit 276ae2984b
Reland fixes the non-x11 build.

Cq-Include-Trybots: luci.chromium.try:linux-wayland-rel

Original change's description:
> [GTK] Detect GTK4 IME incompatibilities
>
> Some users have GTK3 input modules for IBus and Fcitx, but are missing
> the corresponding GTK4 modules. This CL aims to detect that case and
> prevent defaulting to GTK4. It also prevents defaulting to GTK4 for IBus
> with Korean locales, since some implementations are known to be buggy.
>
> Change-Id: Ic891836d35219fb07fbb1ede873f11b0f312562b
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6499945
> Commit-Queue: Thomas Anderson <thomasanderson@chromium.org>
> Reviewed-by: Orko Garai <orko@igalia.com>
> Cr-Commit-Position: refs/heads/main@{#1454733}

Change-Id: I466c9b1bb96b6e01c097ca4bdfd5cd93aa5498b3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6507359
Auto-Submit: Thomas Anderson <thomasanderson@chromium.org>
Commit-Queue: Orko Garai <orko@igalia.com>
Reviewed-by: Orko Garai <orko@igalia.com>
Cr-Commit-Position: refs/heads/main@{#1455174}
This commit is contained in:
Tom Anderson
2025-05-02 13:52:35 -07:00
committed by Chromium LUCI CQ
parent ef87f3cdf3
commit a0d007e46c
6 changed files with 372 additions and 5 deletions

@ -19,6 +19,7 @@
#include "base/no_destructor.h"
#include "base/observer_list.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/threading/thread_local.h"
#include "base/trace_event/trace_event.h"
#include "ui/gfx/switches.h"
@ -98,6 +99,18 @@ Window GetWindowPropertyAsWindow(const GetPropertyResponse& value) {
return Window::None;
}
std::map<std::string, std::string> ParseXResources(std::string_view resources) {
std::map<std::string, std::string> result;
base::StringPairs pairs;
base::SplitStringIntoKeyValuePairs(resources, ':', '\n', &pairs);
for (const auto& pair : pairs) {
auto key = base::TrimWhitespaceASCII(pair.first, base::TRIM_ALL);
auto value = base::TrimWhitespaceASCII(pair.second, base::TRIM_ALL);
result[std::string(key)] = std::string(value);
}
return result;
}
} // namespace
// static
@ -188,7 +201,7 @@ Connection::Connection(const std::string& address)
root_props_ = std::make_unique<PropertyCache>(
this, default_root(),
std::vector<Atom>{GetAtom("_NET_SUPPORTING_WM_CHECK"),
GetAtom("_NET_SUPPORTED")},
GetAtom("_NET_SUPPORTED"), Atom::RESOURCE_MANAGER},
base::BindRepeating(&Connection::OnRootPropertyChanged,
base::Unretained(this)));
}
@ -343,6 +356,13 @@ bool Connection::WmSupportsHint(Atom atom) const {
return false;
}
const std::map<std::string, std::string> Connection::GetXResources() {
// Fetch the initial property value which will call `OnPropertyChanged` and
// populate `xresources_` if it is not already populated.
root_props_->Get(Atom::RESOURCE_MANAGER);
return xresources_;
}
Connection::Request::Request(ResponseCallback callback)
: callback(std::move(callback)) {}
@ -934,6 +954,8 @@ uint32_t Connection::GenerateIdImpl() {
void Connection::OnRootPropertyChanged(Atom property,
const GetPropertyResponse& value) {
// `root_props_` may be null during initialization, so this function should
// rely on `value` directly.
Atom check_atom = GetAtom("_NET_SUPPORTING_WM_CHECK");
if (property == check_atom) {
// We've detected a new window manager, which may have different behavior
@ -947,6 +969,10 @@ void Connection::OnRootPropertyChanged(Atom property,
this, wm_window,
std::vector<Atom>{check_atom, GetAtom("_NET_WM_NAME")});
}
} else if (property == Atom::RESOURCE_MANAGER) {
auto xresources = PropertyCache::GetAsSpan<char>(value);
xresources_ =
ParseXResources(std::string_view(xresources.begin(), xresources.end()));
}
}
@ -957,7 +983,7 @@ bool Connection::WmSupportsEwmh() const {
if (!wm_props_) {
return false;
}
if (const x11::Window* wm_check = wm_props_->GetAs<Window>(check_atom)) {
if (const Window* wm_check = wm_props_->GetAs<Window>(check_atom)) {
return *wm_check == wm_window;
}
return false;
@ -974,7 +1000,7 @@ void Connection::OnWmSynced() {
synced_with_wm_ = true;
}
ScopedXGrabServer::ScopedXGrabServer(x11::Connection* connection)
ScopedXGrabServer::ScopedXGrabServer(Connection* connection)
: connection_(connection) {
connection_->GrabServer();
}

@ -451,6 +451,8 @@ class COMPONENT_EXPORT(X11) Connection final : public XProto,
bool WmSupportsHint(Atom atom) const;
const std::map<std::string, std::string> GetXResources();
// The viz compositor thread hangs a PlatformEventSource off the connection so
// that it gets destroyed at the appropriate time.
// TODO(thomasanderson): This is a layering violation and this should be moved
@ -603,6 +605,8 @@ class COMPONENT_EXPORT(X11) Connection final : public XProto,
std::unique_ptr<PropertyCache> root_props_;
std::unique_ptr<PropertyCache> wm_props_;
std::map<std::string, std::string> xresources_;
};
// Grab/release the X server connection within a scope. This can help avoid race

@ -154,6 +154,8 @@ component("gtk") {
if (ozone_platform_x11) {
sources += [
"ime_compat_check.cc",
"ime_compat_check.h",
"x/gtk_ui_platform_x11.cc",
"x/gtk_ui_platform_x11.h",
]

@ -17,6 +17,7 @@
#include "base/strings/string_number_conversions.h"
#include "ui/gfx/color_palette.h"
#include "ui/gtk/gtk_stubs.h"
#include "ui/gtk/ime_compat_check.h"
namespace gtk {
@ -149,8 +150,17 @@ bool LoadGtkImpl() {
// RPM-based distributions that are supported.
gtk_version = 4;
}
// Prefer GTK3 for non-GNOME desktops as the GTK4 ecosystem is still immature.
return gtk_version == 4 ? LoadGtk4() || LoadGtk3() : LoadGtk3() || LoadGtk4();
// Default to GTK4 on GNOME except when IME is detected to be incompatible.
// Allow the command line switch to override this.
return gtk_version == 4 &&
#if defined(OZONE_PLATFORM_X11)
(cmd->HasSwitch(kGtkVersionFlag) ||
CheckGtk4X11ImeCompatibility())
#else
true
#endif
? LoadGtk4() || LoadGtk3()
: LoadGtk3() || LoadGtk4();
}
gfx::Insets InsetsFromGtkBorder(const GtkBorder& border) {

307
ui/gtk/ime_compat_check.cc Normal file

@ -0,0 +1,307 @@
// Copyright 2025 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/ime_compat_check.h"
#include <dlfcn.h>
#include <string>
#include <string_view>
#include <vector>
#include "base/check.h"
#include "base/environment.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/strings/string_split.h"
#include "ui/gfx/x/connection.h"
#include "ui/linux/linux_ui_delegate.h"
// The functions in this file are run before GTK is loaded, so it must not
// depend on any GTK functions or types.
namespace gtk {
namespace {
struct InputMethod {
std::string_view path;
std::string_view id;
std::string_view domain;
std::vector<std::string_view> locales;
};
std::vector<base::FilePath> GetLibrarySearchPaths() {
std::vector<base::FilePath> search_path;
void* handle = dlopen("libc.so.6", RTLD_GLOBAL | RTLD_LAZY | RTLD_NOLOAD);
if (!handle) {
return search_path;
}
Dl_serinfo serinfo;
if (dlinfo(handle, RTLD_DI_SERINFOSIZE, &serinfo) == -1) {
return search_path;
}
std::unique_ptr<Dl_serinfo, base::FreeDeleter> sip(
static_cast<Dl_serinfo*>(malloc(serinfo.dls_size)));
if (dlinfo(handle, RTLD_DI_SERINFOSIZE, sip.get()) == -1) {
return search_path;
}
if (dlinfo(handle, RTLD_DI_SERINFO, sip.get()) == -1) {
return search_path;
}
for (size_t j = 0; j < serinfo.dls_cnt; j++) {
// SAFETY: The range is bound by `serinfo.dls_cnt`.
search_path.emplace_back(UNSAFE_BUFFERS(sip->dls_serpath[j].dls_name));
}
return search_path;
}
base::FilePath GetGtk3ImModulesCacheFile() {
auto env = base::Environment::Create();
auto module_file_var = env->GetVar("GTK_IM_MODULE_FILE");
base::FilePath immodules_cache;
if (module_file_var) {
immodules_cache = base::FilePath(*module_file_var);
} else {
auto gtk_exe_prefix = env->GetVar("GTK_EXE_PREFIX");
if (gtk_exe_prefix) {
immodules_cache = base::FilePath(*gtk_exe_prefix)
.Append("lib/gtk-3.0/3.0.0/immodules.cache");
} else {
for (const auto& libdir : GetLibrarySearchPaths()) {
base::FilePath path = libdir.Append("gtk-3.0/3.0.0/immodules.cache");
if (base::PathExists(path)) {
immodules_cache = path;
break;
}
}
}
}
return immodules_cache;
}
std::vector<std::string_view> ParseImModulesCacheLine(std::string_view line) {
std::vector<std::string_view> result;
size_t pos = 0;
while (true) {
pos = line.find('"', pos);
if (pos == std::string_view::npos) {
break;
}
size_t start = pos + 1;
size_t quote = start;
// Find the matching closing quote.
while (true) {
quote = line.find('"', quote);
if (quote == std::string_view::npos) {
// Unmatched quote
return result;
}
if (quote > start && line.substr(quote - 1, 1) == "\\") {
// If there's a backslash immediately before it, it's escaped.
++quote;
} else {
// Otherwise, this is the real closing quote.
result.push_back(line.substr(start, quote - start));
pos = quote + 1;
break;
}
}
}
return result;
}
void ParseImModulesCacheFile(std::string_view contents,
std::vector<InputMethod>& ims,
std::map<std::string_view, size_t>& im_map) {
std::string_view current_path;
for (const auto& line : base::SplitStringPiece(
contents, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY)) {
if (line.starts_with("#")) {
continue;
}
auto parts = ParseImModulesCacheLine(line);
if (parts.size() == 1) {
current_path = parts[0];
} else if (parts.size() == 5) {
ims.emplace_back(
current_path, parts[0], parts[2],
base::SplitStringPiece(parts[4], ":", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY));
im_map[parts[0]] = ims.size() - 1;
} else {
LOG(ERROR) << "Invalid immodules.cache line: " << line;
}
}
}
std::vector<std::string> GetForcedIms() {
auto env = base::Environment::Create();
std::string forced_ims = env->GetVar("GTK_IM_MODULE").value_or(std::string());
if (auto* connection = x11::Connection::Get()) {
const auto& resources = connection->GetXResources();
if (auto it = resources.find("gtk-im-module"); it != resources.end()) {
forced_ims += ':' + it->second;
}
}
return base::SplitString(forced_ims, ":", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
}
std::string GetLocale() {
const char* lc_ctype = setlocale(LC_CTYPE, nullptr);
std::string locale = lc_ctype ? lc_ctype : "";
// Remove everything after the first "." or "@".
size_t pos = locale.find_first_of(".@");
if (pos != std::string::npos) {
locale = locale.substr(0, pos);
}
return locale.empty() ? "C" : locale;
}
const InputMethod* GetGtk3Im(const std::vector<InputMethod>& ims,
const std::map<std::string_view, size_t>& im_map) {
const InputMethod* gtk3_im = nullptr;
for (const std::string& im : GetForcedIms()) {
if (im == "gtk-im-context-simple" || im == "gtk-im-context-none") {
// GTK4 has these available.
return nullptr;
}
auto it = im_map.find(im);
if (it != im_map.end()) {
gtk3_im = &ims[it->second];
break;
}
}
const std::string locale = GetLocale();
if (!gtk3_im) {
int best_score = 0;
for (const auto& entry : ims) {
if (entry.id == "wayland" || entry.id == "waylandgtk" ||
entry.id == "broadway") {
continue;
}
for (std::string_view lc : entry.locales) {
// This is the scoring that GTK3 IM module loading uses.
int score = 0;
if (lc == "*") {
score = 1;
} else if (locale == lc) {
score = 4;
} else if (locale.substr(0, 2) == lc.substr(0, 2)) {
score = lc.size() == 2 ? 3 : 2;
}
if (score > best_score) {
best_score = score;
gtk3_im = &entry;
}
}
}
}
return gtk3_im;
}
std::vector<base::FilePath> GetGtk4ImModulePaths() {
auto env = base::Environment::Create();
base::FilePath default_dir;
auto exe_prefix = env->GetVar("GTK_EXE_PREFIX");
if (exe_prefix) {
default_dir = base::FilePath(*exe_prefix).Append("lib/gtk-4.0");
} else {
for (const auto& libdir : GetLibrarySearchPaths()) {
base::FilePath path = libdir.Append("gtk-4.0");
if (base::PathExists(path)) {
default_dir = path;
break;
}
}
}
std::vector<base::FilePath> result;
auto add_path = [&](const base::FilePath& path) {
result.emplace_back(path.Append("4.0.0/linux/immodules"));
result.emplace_back(path.Append("4.0.0/immodules"));
result.emplace_back(path.Append("linux/immodules"));
result.emplace_back(path.Append("immodules"));
};
if (auto module_path_env = env->GetVar("GTK_PATH")) {
for (const auto& path :
base::SplitStringPiece(*module_path_env, "", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY)) {
add_path(base::FilePath(path));
}
}
if (!default_dir.empty()) {
add_path(default_dir);
}
return result;
}
} // namespace
bool CheckGtk4X11ImeCompatibility() {
auto* delegate = ui::LinuxUiDelegate::GetInstance();
CHECK(delegate);
if (delegate->GetBackend() != ui::LinuxUiBackend::kX11) {
// This function is only relevant for X11.
return true;
}
const base::FilePath immodules_cache = GetGtk3ImModulesCacheFile();
if (!base::PathExists(immodules_cache)) {
// GTK3 not installed or no immodules.cache file found.
return true;
}
std::string contents;
base::ReadFileToString(base::FilePath(immodules_cache), &contents);
std::vector<InputMethod> ims;
std::map<std::string_view, size_t> im_map;
ParseImModulesCacheFile(contents, ims, im_map);
const auto* gtk3_im = GetGtk3Im(ims, im_map);
if (!gtk3_im) {
// Using a supported built-in input method, or GTK3 is not installed, or no
// input method is available. Allow GTK4 to use it's default input method.
return true;
}
const std::string locale = GetLocale();
if (locale.substr(0, 2) == "ko" && gtk3_im->id == "ibus") {
// Older versions of IBus are buggy with Korean locales.
return false;
}
if (gtk3_im->domain == "gtk30") {
// Builtin modules have been removed in GTK4.
return false;
}
auto base_name = base::FilePath(gtk3_im->path).BaseName().value();
for (const auto& path : GetGtk4ImModulePaths()) {
base::FilePath gtk4_im = path.Append("lib" + base_name);
if (base::PathExists(gtk4_im)) {
// GTK4 has a compatible input method.
return true;
}
}
return false;
}
} // namespace gtk

18
ui/gtk/ime_compat_check.h Normal file

@ -0,0 +1,18 @@
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef UI_GTK_IME_COMPAT_CHECK_H_
#define UI_GTK_IME_COMPAT_CHECK_H_
namespace gtk {
// Some distros have packaging issues where GTK3 IMEs may be installed but not
// GTK4 IMEs. This function checks for that case, and returns true if the GTK4
// IME is usable. This workaround may be removed when support for older
// distributions like Ubuntu 22.04 is dropped.
[[nodiscard]] bool CheckGtk4X11ImeCompatibility();
} // namespace gtk
#endif // UI_GTK_IME_COMPAT_CHECK_H_