/* * SPDX-FileCopyrightText: 2016-2016 CSSlayer * * SPDX-License-Identifier: LGPL-2.1-or-later * */ #include "classicui.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 #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(); settingMonitor_ = std::make_unique( *dbusAddon->call()); } #endif reloadConfig(); #ifdef ENABLE_X11 if (auto *xcbAddon = xcb()) { xcbCreatedCallback_ = xcbAddon->call( [this](const std::string &name, xcb_connection_t *conn, int screen, FocusGroup *) { auto xcbui = std::make_unique(this, name, conn, screen); uis_[xcbui->name()] = std::move(xcbui); CLASSICUI_INFO() << "Created classicui for x11 display:" << name; }); xcbClosedCallback_ = xcbAddon->call( [this](const std::string &name, xcb_connection_t *) { uis_.erase("x11:" + name); }); } #endif #ifdef WAYLAND_FOUND if (auto *waylandAddon = wayland()) { waylandCreatedCallback_ = waylandAddon->call( [this](const std::string &name, wl_display *display, FocusGroup *) { try { auto waylandui = std::make_unique(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( [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(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(); 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( &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(); 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(); } 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> 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(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([this](bool enable) { for (auto &p : uis_) { p.second->setEnableTray(!enable); } }); } sni->call(); // 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(); 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(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(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(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(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(); 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(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 M3UI::labelIcon(const std::string &label, unsigned int size) { std::vector data; size_t stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, size); data.resize(stride * size); UniqueCPtr 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);