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:

committed by
Chromium LUCI CQ

parent
ef87f3cdf3
commit
a0d007e46c
@ -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
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
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_
|
Reference in New Issue
Block a user