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>
This commit is contained in:
commit
0804634dc1
68 files changed
+11474
No files matched your search
@@ -0,0 +1,542 @@
|
||||
/*
|
||||
* 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);
|
||||
Reference in new issue
Block a user