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>
543 lines
18 KiB
C++
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);
|