Files
fcitx5-m3ui/src/classicui.cpp
T
justmeandClaude Sonnet 5.5 0804634dc1 Add Material 3 candidate window addon for Fcitx5
Fork of the classicui addon (Fcitx5 5.1.23) with a Material 3 look, spring
based animations and a right click action menu for candidates.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 21:58:54 +08:00

543 lines
18 KiB
C++

/*
* SPDX-FileCopyrightText: 2016-2016 CSSlayer <wengxt@gmail.com>
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*
*/
#include "classicui.h"
#include <fcntl.h>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
#include <utility>
#include <vector>
#include <cairo.h>
#include "fcitx-config/iniparser.h"
#include "fcitx-config/rawconfig.h"
#include "fcitx-utils/color.h"
#include "fcitx-utils/dbus/message_details.h"
#include "fcitx-utils/eventloopinterface.h"
#include "fcitx-utils/fs.h"
#include "fcitx-utils/i18n.h"
#include "fcitx-utils/log.h"
#include "fcitx-utils/misc.h"
#include "fcitx-utils/misc_p.h"
#include "fcitx-utils/standardpaths.h"
#include "fcitx/addonfactory.h"
#include "fcitx/addoninstance.h"
#include "fcitx/event.h"
#include "fcitx/inputcontext.h"
#include "fcitx/inputcontextmanager.h"
#include "fcitx/instance.h"
#include "fcitx/userinterface.h"
#include "common.h"
#include "notificationitem_public.h"
#include "plasmathemewatchdog.h"
#include "theme.h"
#ifdef ENABLE_X11
#include <xcb/xcb.h>
#include "xcb_public.h"
#include "xcbui.h"
#endif
#ifdef WAYLAND_FOUND
#include "wayland_public.h"
#include "waylandui.h"
#endif
#ifdef ENABLE_DBUS
#include "fcitx-utils/dbus/variant.h"
#include "dbus_public.h"
#include "portalsettingmonitor.h"
#endif
namespace fcitx::m3ui {
FCITX_DEFINE_LOG_CATEGORY(m3ui_logcategory, "m3ui");
using AccentColorDBusType = FCITX_STRING_TO_DBUS_TYPE("(ddd)");
M3UI::M3UI(Instance *instance) : instance_(instance) {
#ifdef ENABLE_DBUS
if (auto *dbusAddon = dbus()) {
dbus::VariantTypeRegistry::defaultRegistry()
.registerType<AccentColorDBusType>();
settingMonitor_ = std::make_unique<PortalSettingMonitor>(
*dbusAddon->call<IDBusModule::bus>());
}
#endif
reloadConfig();
#ifdef ENABLE_X11
if (auto *xcbAddon = xcb()) {
xcbCreatedCallback_ =
xcbAddon->call<IXCBModule::addConnectionCreatedCallback>(
[this](const std::string &name, xcb_connection_t *conn,
int screen, FocusGroup *) {
auto xcbui =
std::make_unique<XCBUI>(this, name, conn, screen);
uis_[xcbui->name()] = std::move(xcbui);
CLASSICUI_INFO()
<< "Created classicui for x11 display:" << name;
});
xcbClosedCallback_ =
xcbAddon->call<IXCBModule::addConnectionClosedCallback>(
[this](const std::string &name, xcb_connection_t *) {
uis_.erase("x11:" + name);
});
}
#endif
#ifdef WAYLAND_FOUND
if (auto *waylandAddon = wayland()) {
waylandCreatedCallback_ =
waylandAddon->call<IWaylandModule::addConnectionCreatedCallback>(
[this](const std::string &name, wl_display *display,
FocusGroup *) {
try {
auto waylandui =
std::make_unique<WaylandUI>(this, name, display);
uis_[waylandui->name()] = std::move(waylandui);
CLASSICUI_INFO()
<< "Created classicui for wayland display:" << name;
} catch (const std::runtime_error &) {
}
});
waylandClosedCallback_ =
waylandAddon->call<IWaylandModule::addConnectionClosedCallback>(
[this](const std::string &name, wl_display *) {
uis_.erase("wayland:" + name);
});
}
#endif
deferedReloadTheme_ =
instance_->eventLoop().addDeferEvent([this](EventSource *) {
reloadTheme();
return true;
});
deferedReloadTheme_->setEnabled(false);
// Since kimpanel may call classicui
persistentEventHandlers_.emplace_back(instance_->watchEvent(
EventType::FocusGroupFocusChanged, EventWatcherPhase::Default,
[this](Event &event) {
auto &focusEvent =
static_cast<FocusGroupFocusChangedEvent &>(event);
if (!focusEvent.newFocus()) {
if (auto *ui = uiForDisplay(focusEvent.group()->display())) {
ui->update(UserInterfaceComponent::InputPanel, nullptr);
}
}
}));
persistentEventHandlers_.emplace_back(instance_->watchEvent(
EventType::UIChanged, EventWatcherPhase::Default, [this](Event &) {
if (instance_->currentUI() == "kimpanel") {
deferedReloadTheme_->setOneShot();
} else if (instance_->currentUI() == "m3ui") {
deferedReloadTheme_->setOneShot();
}
}));
}
M3UI::~M3UI() {}
void M3UI::reloadConfig() {
readAsIni(config_, "conf/m3ui.conf");
reloadTheme();
}
void M3UI::reloadTheme() {
#ifdef ENABLE_DBUS
auto parseMessage = [this](const dbus::Variant &variant) {
if (variant.signature() == "u") {
auto color = variant.dataAs<uint32_t>();
auto oldIsDark = isDark_;
isDark_ = (color == 1);
if (oldIsDark != isDark_) {
CLASSICUI_DEBUG() << "XDG Portal AppearanceChanged "
"isDark"
<< isDark_;
deferedReloadTheme_->setOneShot();
}
}
};
if (dbus()) {
if (*config_.useDarkTheme) {
if (!darkModeEntry_ && settingMonitor_) {
darkModeEntry_ = settingMonitor_->watch(
"org.freedesktop.appearance", "color-scheme", parseMessage);
}
} else {
darkModeEntry_.reset();
}
}
#endif
bool hasPlasmaTheme =
instance_->currentUI() == "kimpanel" ||
(*config_.theme == PlasmaThemeName) ||
(*config_.useDarkTheme && *config_.themeDark == PlasmaThemeName);
if (hasPlasmaTheme) {
if (!plasmaThemeWatchdog_ && PlasmaThemeWatchdog::isAvailable()) {
try {
plasmaThemeWatchdog_ = std::make_unique<PlasmaThemeWatchdog>(
&instance_->eventLoop(), [this]() {
CLASSICUI_DEBUG() << "Reload plasma theme";
reloadTheme();
});
} catch (...) {
}
}
} else {
plasmaThemeWatchdog_.reset();
}
std::string_view themeName = (*config_.useDarkTheme && isDark_)
? *config_.themeDark
: *config_.theme;
if (instance_->currentUI() == "kimpanel") {
if (plasmaThemeWatchdog_) {
themeName = "plasma";
}
}
theme_.load(themeName);
#ifdef ENABLE_DBUS
auto parseAccentColor = [this](const dbus::Variant &variant) {
if (variant.signature() == "(ddd)") {
auto dbuscolor = variant.dataAs<AccentColorDBusType>();
Color color;
color.setAlphaF(1);
color.setRedF(std::get<0>(dbuscolor));
color.setGreenF(std::get<1>(dbuscolor));
color.setBlueF(std::get<2>(dbuscolor));
if (!accentColor_ || *accentColor_ != color) {
accentColor_ = color;
CLASSICUI_DEBUG() << "XDG Portal AccentColor changed "
"color: "
<< accentColor_;
deferedReloadTheme_->setOneShot();
}
}
};
if (dbus()) {
if (*config_.useAccentColor) {
if (!accentColorEntry_ && settingMonitor_) {
accentColorEntry_ =
settingMonitor_->watch("org.freedesktop.appearance",
"accent-color", parseAccentColor);
}
} else {
accentColorEntry_.reset();
accentColor_ = std::nullopt;
}
}
#endif
theme_.populateColor(accentColor_);
}
void M3UI::suspend() {
suspended_ = true;
for (auto &p : uis_) {
p.second->suspend();
}
if (auto *sni = notificationitem()) {
sni->call<INotificationItem::disable>();
}
eventHandlers_.clear();
}
const Configuration *M3UI::getConfig() const {
auto themeDirs = StandardPaths::global().locate(
StandardPathsType::PkgData, "themes",
[](const std::filesystem::path &path) {
std::error_code ec;
return std::filesystem::is_directory(path, ec);
});
std::map<std::string, std::string, std::less<>> themes;
for (const auto &[themeName, _] : themeDirs) {
auto file = StandardPaths::global().open(
StandardPathsType::PkgData,
std::filesystem::path("themes") / themeName / "theme.conf",
Theme::isSystemThemeName(themeName.string())
? StandardPathsMode::System
: StandardPathsMode::Default);
if (file.fd() < 0) {
continue;
}
RawConfig config;
readFromIni(config, file.fd());
ThemeConfig themeConfig;
themeConfig.load(config);
if (!themeConfig.metadata.value()
.name.value()
.defaultString()
.empty()) {
themes[themeName] =
themeConfig.metadata.value().name.value().match();
}
}
bool plasmaTheme = false;
if (PlasmaThemeWatchdog::isAvailable()) {
if (auto iter = themes.find(PlasmaThemeName); iter != themes.end()) {
themes.erase(iter);
}
themes.emplace(PlasmaThemeName, _("KDE Plasma (Experimental)"));
plasmaTheme = true;
}
config_.theme.annotation().setThemes({themes.begin(), themes.end()},
plasmaTheme);
config_.themeDark.annotation().setThemes({themes.begin(), themes.end()},
plasmaTheme);
return &config_;
}
UIInterface *M3UI::uiForEvent(Event &event) {
if (suspended_) {
return nullptr;
}
if (!event.isInputContextEvent()) {
return nullptr;
}
auto &icEvent = static_cast<InputContextEvent &>(event);
return uiForInputContext(icEvent.inputContext());
}
UIInterface *M3UI::uiForInputContext(InputContext *inputContext) {
if (suspended_ || !inputContext) {
return nullptr;
}
if (!inputContext->hasFocus()) {
return nullptr;
}
return uiForDisplay(inputContext->display());
}
UIInterface *M3UI::uiForDisplay(const std::string &display) {
auto iter = uis_.find(display);
if (iter == uis_.end()) {
return nullptr;
}
return iter->second.get();
}
void M3UI::resume() {
CLASSICUI_DEBUG() << "Resume M3UI";
suspended_ = false;
for (auto &p : uis_) {
p.second->resume();
}
if (auto *sni = notificationitem()) {
if (!sniHandler_) {
sniHandler_ =
sni->call<INotificationItem::watch>([this](bool enable) {
for (auto &p : uis_) {
p.second->setEnableTray(!enable);
}
});
}
sni->call<INotificationItem::enable>();
// Delay 1 sec to enable xembed tray icon.
deferedEnableTray_ = instance_->eventLoop().addTimeEvent(
CLOCK_MONOTONIC, now(CLOCK_MONOTONIC) + 1000000, 0,
[this](EventSource *, uint64_t) {
// If we are now suspended, just return.
if (suspended()) {
return true;
}
if (auto *sni = notificationitem()) {
auto registered =
sni->call<INotificationItem::registered>();
for (auto &p : uis_) {
p.second->setEnableTray(!registered);
}
}
deferedEnableTray_.reset();
return true;
});
} else {
for (auto &p : uis_) {
p.second->setEnableTray(true);
}
}
eventHandlers_.emplace_back(instance_->watchEvent(
EventType::InputContextCursorRectChanged, EventWatcherPhase::Default,
[this](Event &event) {
if (auto *ui = uiForEvent(event)) {
auto &icEvent = static_cast<InputContextEvent &>(event);
ui->updateCursor(icEvent.inputContext());
}
}));
eventHandlers_.emplace_back(instance_->watchEvent(
EventType::InputContextFocusIn, EventWatcherPhase::Default,
[this](Event &event) {
if (auto *ui = uiForEvent(event)) {
auto &icEvent = static_cast<InputContextEvent &>(event);
ui->updateCursor(icEvent.inputContext());
ui->updateCurrentInputMethod(icEvent.inputContext());
}
}));
eventHandlers_.emplace_back(instance_->watchEvent(
EventType::InputContextSwitchInputMethod, EventWatcherPhase::Default,
[this](Event &event) {
if (auto *ui = uiForEvent(event)) {
auto &icEvent = static_cast<InputContextEvent &>(event);
ui->updateCurrentInputMethod(icEvent.inputContext());
}
}));
eventHandlers_.emplace_back(instance_->watchEvent(
EventType::InputMethodGroupChanged, EventWatcherPhase::Default,
[this](Event &) {
instance_->inputContextManager().foreachFocused(
[this](InputContext *ic) {
if (auto *ui = uiForInputContext(ic)) {
ui->updateCurrentInputMethod(ic);
}
return true;
});
}));
eventHandlers_.emplace_back(instance_->watchEvent(
EventType::FocusGroupFocusChanged, EventWatcherPhase::Default,
[this](Event &event) {
auto &focusEvent =
static_cast<FocusGroupFocusChangedEvent &>(event);
if (!focusEvent.newFocus()) {
if (auto *ui = uiForDisplay(focusEvent.group()->display())) {
ui->update(UserInterfaceComponent::InputPanel, nullptr);
}
}
}));
}
void M3UI::update(UserInterfaceComponent component,
InputContext *inputContext) {
UIInterface *ui = nullptr;
if (inputContext->display().starts_with("wayland:") &&
!inputContext->frontendName().starts_with("wayland")) {
// If display is wayland, but frontend is not, then we can only do X11
// for now, though position is wrong. We don't know which is xwayland
// unfortunately, hopefully main display is X wayland.
// The position will be wrong anyway.
#ifdef ENABLE_X11
if (auto *xcbAddon = xcb()) {
auto mainX11Display = xcbAddon->call<IXCBModule::mainDisplay>();
if (!mainX11Display.empty()) {
if (auto *uiPtr = findValue(uis_, "x11:" + mainX11Display)) {
ui = uiPtr->get();
}
}
}
#endif
} else {
if (auto *uiPtr = findValue(uis_, inputContext->display())) {
ui = uiPtr->get();
}
}
CLASSICUI_DEBUG() << "Update component: " << static_cast<int>(component)
<< " for IC program:" << inputContext->program()
<< " frontend:" << inputContext->frontendName()
<< " display:" << inputContext->display()
<< " ui:" << (ui ? ui->name() : "(not available)");
if (ui) {
ui->update(component, inputContext);
if (component == UserInterfaceComponent::StatusArea) {
ui->updateCurrentInputMethod(inputContext);
}
}
}
class M3UIFactory : public AddonFactory {
public:
AddonInstance *create(AddonManager *manager) override {
return new M3UI(manager->instance());
}
};
const fcitx::Configuration *
M3UI::getSubConfig(const std::string &path) const {
if (!path.starts_with("theme/")) {
return nullptr;
}
auto name = path.substr(6);
if (!ThemeNameConstrain{}.check(name)) {
return nullptr;
}
subconfigTheme_.load(name);
return &subconfigTheme_;
}
void M3UI::setSubConfig(const std::string &path,
const fcitx::RawConfig &config) {
if (!path.starts_with("theme/")) {
return;
}
auto name = path.substr(6);
if (!ThemeNameConstrain{}.check(name)) {
return;
}
if (Theme::isSystemThemeName(name)) {
return;
}
auto &theme = name == theme_.name() ? theme_ : subconfigTheme_;
if (&theme == &subconfigTheme_) {
// Fill the system value.
getSubConfig(path);
} else {
deferedReloadTheme_->setOneShot();
}
theme.load(name, config);
safeSaveAsIni(theme, StandardPathsType::PkgData,
std::filesystem::path("themes") / name / "theme.conf");
}
std::vector<unsigned char> M3UI::labelIcon(const std::string &label,
unsigned int size) {
std::vector<unsigned char> data;
size_t stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, size);
data.resize(stride * size);
UniqueCPtr<cairo_surface_t, cairo_surface_destroy> image;
image.reset(cairo_image_surface_create_for_data(
data.data(), CAIRO_FORMAT_ARGB32, size, size, stride));
ThemeImage::drawTextIcon(image.get(), label, size, config_);
image.reset();
return data;
}
bool M3UI::preferTextIcon() const { return *config_.preferTextIcon; }
bool M3UI::showLayoutNameInIcon() const {
return *config_.showLayoutNameInIcon;
}
} // namespace fcitx::m3ui
FCITX_ADDON_FACTORY_V2(m3ui, fcitx::m3ui::M3UIFactory);