commit 1940ef7547ac602cf916a1872e59a92472246739 Author: Wang Zichong Date: Thu Dec 18 16:41:37 2025 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a4fb4fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +.cache/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..9e67c97 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,81 @@ +cmake_minimum_required(VERSION 3.16) + +project(xembed-sni-proxy) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_CXX_STANDARD 20) + +find_package(Qt6 6.8 CONFIG REQUIRED COMPONENTS DBus) +find_package(ECM REQUIRED NO_MODULE) +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) +include(ECMQtDeclareLoggingCategory) +include(KDEInstallDirs) +include(ECMConfiguredInstall) + +find_package(KF6 6.6 REQUIRED COMPONENTS + WindowSystem) + +find_package(XCB + REQUIRED COMPONENTS + XCB + XFIXES + DAMAGE + COMPOSITE + RANDR + SHM + UTIL + IMAGE +) + +set(XCB_LIBS + XCB::XCB + XCB::XFIXES + XCB::DAMAGE + XCB::COMPOSITE + XCB::RANDR + XCB::SHM + XCB::UTIL + XCB::IMAGE +) + +set(XEMBED_SNI_PROXY_SOURCES + main.cpp + fdoselectionmanager.cpp fdoselectionmanager.h + snidbus.cpp snidbus.h + sniproxy.cpp + xtestsender.cpp xtestsender.h + ) + +qt_add_dbus_adaptor(XEMBED_SNI_PROXY_SOURCES org.kde.StatusNotifierItem.xml + sniproxy.h SNIProxy) + +set(statusnotifierwatcher_xml org.kde.StatusNotifierWatcher.xml) +qt_add_dbus_interface(XEMBED_SNI_PROXY_SOURCES ${statusnotifierwatcher_xml} statusnotifierwatcher_interface) + +ecm_qt_declare_logging_category(XEMBED_SNI_PROXY_SOURCES HEADER debug.h + IDENTIFIER SNIPROXY + CATEGORY_NAME kde.xembedsniproxy + DEFAULT_SEVERITY Info + DESCRIPTION "xembed sni proxy" + EXPORT PLASMAWORKSPACE + ) + +add_executable(xembedsniproxy ${XEMBED_SNI_PROXY_SOURCES}) +set_property(TARGET xembedsniproxy PROPERTY AUTOMOC ON) + + +set_package_properties(XCB PROPERTIES TYPE REQUIRED) + + +target_link_libraries(xembedsniproxy + Qt::Core + Qt::DBus + KF6::WindowSystem + ${XCB_LIBS} + X11::Xtst +) + +install(TARGETS xembedsniproxy ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) +install(FILES xembedsniproxy.desktop DESTINATION ${KDE_INSTALL_AUTOSTARTDIR}) + +ecm_install_configured_files(INPUT plasma-xembedsniproxy.service.in @ONLY DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}) diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..32885a5 --- /dev/null +++ b/Readme.md @@ -0,0 +1,34 @@ +## Note + +This project is modified from KDE's xembed-sni-proxy project, extracted from plasma-workspace. + +##XEmbed SNI Proxy + +The goal of this project is to make xembed system trays available in Plasma. + +This is to allow legacy apps (xchat, pidgin, tuxguitar) etc. system trays[1] available in Plasma which only supports StatusNotifierItem [2]. + +Ideally we also want this to work in an xwayland session, making X system tray icons available even when plasmashell only has a wayland connection. + +This project should be portable onto all other DEs that speak SNI. + +##How it works (in theory) + +* We register a window as a system tray container +* We render embedded windows composited offscreen +* We render contents into an image and send this over DBus via the SNI protocol +* XDamage events trigger a repaint +* Activate and context menu events are replyed via X send event into the embedded container as left and right clicks + +There are a few extra hacks in the real code to deal with some toolkits being awkward. + +##Build instructions + + cmake . + make + sudo make install + +After building, run `xembedsniproxy`. + +[1] http://standards.freedesktop.org/systemtray-spec/systemtray-spec-latest.html +[2] http://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/ diff --git a/c_ptr.h b/c_ptr.h new file mode 100644 index 0000000..0e263ee --- /dev/null +++ b/c_ptr.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ +#pragma once +#include + +struct CDeleter { + template + void operator()(T *ptr) + { + if (ptr) { + free(ptr); + } + } +}; + +template +using UniqueCPointer = std::unique_ptr; diff --git a/fdoselectionmanager.cpp b/fdoselectionmanager.cpp new file mode 100644 index 0000000..768bda4 --- /dev/null +++ b/fdoselectionmanager.cpp @@ -0,0 +1,226 @@ +/* + Registers as a embed container + SPDX-FileCopyrightText: 2015 David Edmundson + SPDX-FileCopyrightText: 2019 Konrad Materka + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ +#include "fdoselectionmanager.h" + +#include "debug.h" + +#include + +#include + +#include +#include +#include +#include + +#include "../c_ptr.h" +#include "sniproxy.h" +#include "xcbutils.h" + +#define SYSTEM_TRAY_REQUEST_DOCK 0 +#define SYSTEM_TRAY_BEGIN_MESSAGE 1 +#define SYSTEM_TRAY_CANCEL_MESSAGE 2 + +FdoSelectionManager::FdoSelectionManager() + : QObject() + , m_x11Interface(qGuiApp->nativeInterface()) + , m_selectionOwner(new KSelectionOwner(Xcb::atoms->selectionAtom, -1, this)) +{ + qCDebug(SNIPROXY) << "starting"; + + // we may end up calling QCoreApplication::quit() in this method, at which point we need the event loop running + QTimer::singleShot(0, this, &FdoSelectionManager::init); +} + +FdoSelectionManager::~FdoSelectionManager() +{ + qCDebug(SNIPROXY) << "closing"; + m_selectionOwner->release(); +} + +void FdoSelectionManager::init() +{ + // load damage extension + xcb_connection_t *c = m_x11Interface->connection(); + xcb_prefetch_extension_data(c, &xcb_damage_id); + const auto *reply = xcb_get_extension_data(c, &xcb_damage_id); + if (reply && reply->present) { + m_damageEventBase = reply->first_event; + xcb_damage_query_version_unchecked(c, XCB_DAMAGE_MAJOR_VERSION, XCB_DAMAGE_MINOR_VERSION); + } else { + // no XDamage means + qCCritical(SNIPROXY) << "could not load damage extension. Quitting"; + qApp->exit(-1); + } + + qApp->installNativeEventFilter(this); + + connect(m_selectionOwner, &KSelectionOwner::claimedOwnership, this, &FdoSelectionManager::onClaimedOwnership); + connect(m_selectionOwner, &KSelectionOwner::failedToClaimOwnership, this, &FdoSelectionManager::onFailedToClaimOwnership); + connect(m_selectionOwner, &KSelectionOwner::lostOwnership, this, &FdoSelectionManager::onLostOwnership); + m_selectionOwner->claim(false); +} + +bool FdoSelectionManager::addDamageWatch(xcb_window_t client) +{ + qCDebug(SNIPROXY) << "adding damage watch for " << client; + + xcb_connection_t *c = m_x11Interface->connection(); + const auto attribsCookie = xcb_get_window_attributes_unchecked(c, client); + + const auto damageId = xcb_generate_id(c); + m_damageWatches[client] = damageId; + xcb_damage_create(c, damageId, client, XCB_DAMAGE_REPORT_LEVEL_NON_EMPTY); + + xcb_generic_error_t *error = nullptr; + UniqueCPointer attr(xcb_get_window_attributes_reply(c, attribsCookie, &error)); + UniqueCPointer getAttrError(error); + uint32_t events = XCB_EVENT_MASK_STRUCTURE_NOTIFY; + if (attr) { + events = events | attr->your_event_mask; + } + // if window is already gone, there is no need to handle it. + if (getAttrError && getAttrError->error_code == XCB_WINDOW) { + return false; + } + // the event mask will not be removed again. We cannot track whether another component also needs STRUCTURE_NOTIFY (e.g. KWindowSystem). + // if we would remove the event mask again, other areas will break. + const auto changeAttrCookie = xcb_change_window_attributes_checked(c, client, XCB_CW_EVENT_MASK, &events); + UniqueCPointer changeAttrError(xcb_request_check(c, changeAttrCookie)); + // if window is gone by this point, it will be caught by eventFilter, so no need to check later errors. + if (changeAttrError && changeAttrError->error_code == XCB_WINDOW) { + return false; + } + + return true; +} + +bool FdoSelectionManager::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) +{ + Q_UNUSED(result) + + if (eventType != "xcb_generic_event_t") { + return false; + } + + auto *ev = static_cast(message); + + const auto responseType = XCB_EVENT_RESPONSE_TYPE(ev); + if (responseType == XCB_CLIENT_MESSAGE) { + const auto ce = reinterpret_cast(ev); + if (ce->type == Xcb::atoms->opcodeAtom) { + switch (ce->data.data32[1]) { + case SYSTEM_TRAY_REQUEST_DOCK: + dock(ce->data.data32[2]); + return true; + } + } + } else if (responseType == XCB_UNMAP_NOTIFY) { + const auto unmappedWId = reinterpret_cast(ev)->window; + if (m_proxies.contains(unmappedWId)) { + undock(unmappedWId); + } + } else if (responseType == XCB_DESTROY_NOTIFY) { + const auto destroyedWId = reinterpret_cast(ev)->window; + if (m_proxies.contains(destroyedWId)) { + undock(destroyedWId); + } + } else if (responseType == m_damageEventBase + XCB_DAMAGE_NOTIFY) { + const auto damagedWId = reinterpret_cast(ev)->drawable; + const auto sniProxy = m_proxies.value(damagedWId); + if (sniProxy) { + sniProxy->update(); + xcb_damage_subtract(m_x11Interface->connection(), m_damageWatches[damagedWId], XCB_NONE, XCB_NONE); + } + } else if (responseType == XCB_CONFIGURE_REQUEST) { + const auto event = reinterpret_cast(ev); + const auto sniProxy = m_proxies.value(event->window); + if (sniProxy) { + // The embedded window tries to move or resize. Ignore move, handle resize only. + if ((event->value_mask & XCB_CONFIG_WINDOW_WIDTH) || (event->value_mask & XCB_CONFIG_WINDOW_HEIGHT)) { + sniProxy->resizeWindow(event->width, event->height); + } + } + } + + return false; +} + +void FdoSelectionManager::dock(xcb_window_t winId) +{ + qCDebug(SNIPROXY) << "trying to dock window " << winId; + + if (m_proxies.contains(winId)) { + return; + } + + if (addDamageWatch(winId)) { + m_proxies[winId] = new SNIProxy(winId, this); + } +} + +void FdoSelectionManager::undock(xcb_window_t winId) +{ + qCDebug(SNIPROXY) << "trying to undock window " << winId; + + if (!m_proxies.contains(winId)) { + return; + } + m_proxies[winId]->deleteLater(); + m_proxies.remove(winId); +} + +void FdoSelectionManager::onClaimedOwnership() +{ + qCDebug(SNIPROXY) << "Manager selection claimed"; + + setSystemTrayVisual(); +} + +void FdoSelectionManager::onFailedToClaimOwnership() +{ + qCWarning(SNIPROXY) << "failed to claim ownership of Systray Manager"; + qApp->exit(-1); +} + +void FdoSelectionManager::onLostOwnership() +{ + qCWarning(SNIPROXY) << "lost ownership of Systray Manager"; + qApp->exit(-1); +} + +void FdoSelectionManager::setSystemTrayVisual() +{ + xcb_connection_t *c = m_x11Interface->connection(); + auto screen = xcb_setup_roots_iterator(xcb_get_setup(c)).data; + auto trayVisual = screen->root_visual; + xcb_depth_iterator_t depth_iterator = xcb_screen_allowed_depths_iterator(screen); + xcb_depth_t *depth = nullptr; + + while (depth_iterator.rem) { + if (depth_iterator.data->depth == 32) { + depth = depth_iterator.data; + break; + } + xcb_depth_next(&depth_iterator); + } + + if (depth) { + xcb_visualtype_iterator_t visualtype_iterator = xcb_depth_visuals_iterator(depth); + while (visualtype_iterator.rem) { + xcb_visualtype_t *visualtype = visualtype_iterator.data; + if (visualtype->_class == XCB_VISUAL_CLASS_TRUE_COLOR) { + trayVisual = visualtype->visual_id; + break; + } + xcb_visualtype_next(&visualtype_iterator); + } + } + + xcb_change_property(c, XCB_PROP_MODE_REPLACE, m_selectionOwner->ownerWindow(), Xcb::atoms->visualAtom, XCB_ATOM_VISUALID, 32, 1, &trayVisual); +} diff --git a/fdoselectionmanager.h b/fdoselectionmanager.h new file mode 100644 index 0000000..51ea029 --- /dev/null +++ b/fdoselectionmanager.h @@ -0,0 +1,50 @@ +/* + Registers as a embed container + SPDX-FileCopyrightText: 2015 David Edmundson + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +#include + +class KSelectionOwner; +class SNIProxy; + +class FdoSelectionManager : public QObject, public QAbstractNativeEventFilter +{ + Q_OBJECT + +public: + FdoSelectionManager(); + ~FdoSelectionManager() override; + +protected: + bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) override; + +private Q_SLOTS: + void onClaimedOwnership(); + void onFailedToClaimOwnership(); + void onLostOwnership(); + +private: + void init(); + bool addDamageWatch(xcb_window_t client); + void dock(xcb_window_t embed_win); + void undock(xcb_window_t client); + void setSystemTrayVisual(); + + QNativeInterface::QX11Application *m_x11Interface = nullptr; + + uint8_t m_damageEventBase; + + QHash m_damageWatches; + QHash m_proxies; + KSelectionOwner *m_selectionOwner; +}; diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..aed5d70 --- /dev/null +++ b/main.cpp @@ -0,0 +1,65 @@ +/* + Main + SPDX-FileCopyrightText: 2015 David Edmundson + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include + +#include "fdoselectionmanager.h" + +#include "debug.h" +#include "snidbus.h" +#include "xcbutils.h" + +#ifdef None +#ifndef FIXX11H_None +#define FIXX11H_None +inline constexpr XID XNone = None; +#undef None +inline constexpr XID None = XNone; +#endif +#undef None +#endif + +#include + +#include + +namespace Xcb +{ +Xcb::Atoms *atoms; +} + +int main(int argc, char **argv) +{ + // the whole point of this is to interact with X, if we are in any other session, force trying to connect to X + // if the QPA can't load xcb, this app is useless anyway. + qputenv("QT_QPA_PLATFORM", "xcb"); + + QGuiApplication::setDesktopSettingsAware(false); + QCoreApplication::setAttribute(Qt::AA_DisableSessionManager); + + QGuiApplication app(argc, argv); + + if (!KWindowSystem::isPlatformX11()) { + qFatal("xembed-sni-proxy is only useful XCB. Aborting"); + } + + app.setQuitOnLastWindowClosed(false); + + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + Xcb::atoms = new Xcb::Atoms(); + + // KDBusService service(KDBusService::Unique); + FdoSelectionManager manager; + + auto rc = app.exec(); + + delete Xcb::atoms; + return rc; +} diff --git a/org.kde.StatusNotifierItem.xml b/org.kde.StatusNotifierItem.xml new file mode 100644 index 0000000..0cd7edb --- /dev/null +++ b/org.kde.StatusNotifierItem.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.kde.StatusNotifierWatcher.xml b/org.kde.StatusNotifierWatcher.xml new file mode 100644 index 0000000..2eb1a7a --- /dev/null +++ b/org.kde.StatusNotifierWatcher.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plasma-xembedsniproxy.service.in b/plasma-xembedsniproxy.service.in new file mode 100644 index 0000000..61090fd --- /dev/null +++ b/plasma-xembedsniproxy.service.in @@ -0,0 +1,11 @@ +[Unit] +Description=Handle legacy xembed system tray icons +PartOf=graphical-session.target +After=plasma-core.target + +[Service] +ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/xembedsniproxy +Restart=on-failure +Type=simple +Slice=background.slice +TimeoutSec=5sec diff --git a/snidbus.cpp b/snidbus.cpp new file mode 100644 index 0000000..74a3624 --- /dev/null +++ b/snidbus.cpp @@ -0,0 +1,131 @@ +/* + SNI DBus Serialisers + SPDX-FileCopyrightText: 2015 David Edmundson + SPDX-FileCopyrightText: 2009 Marco Martin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "snidbus.h" + +#include +#include + +// mostly copied from KStatusNotiferItemDbus.cpps from knotification + +KDbusImageStruct::KDbusImageStruct() = default; + +KDbusImageStruct::KDbusImageStruct(const QImage &image) +{ + width = image.size().width(); + height = image.size().height(); + if (image.format() == QImage::Format_ARGB32) { + data = QByteArray((char *)image.bits(), image.sizeInBytes()); + } else { + QImage image32 = image.convertToFormat(QImage::Format_ARGB32); + data = QByteArray((char *)image32.bits(), image32.sizeInBytes()); + } + + // swap to network byte order if we are little endian + if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) { + auto *uintBuf = (quint32 *)data.data(); + for (uint i = 0; i < data.size() / sizeof(quint32); ++i) { + *uintBuf = qToBigEndian(*uintBuf); + ++uintBuf; + } + } +} + +// Marshall the ImageStruct data into a D-BUS argument +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageStruct &icon) +{ + argument.beginStructure(); + argument << icon.width; + argument << icon.height; + argument << icon.data; + argument.endStructure(); + return argument; +} + +// Retrieve the ImageStruct data from the D-BUS argument +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusImageStruct &icon) +{ + qint32 width; + qint32 height; + QByteArray data; + + argument.beginStructure(); + argument >> width; + argument >> height; + argument >> data; + argument.endStructure(); + + icon.width = width; + icon.height = height; + icon.data = data; + + return argument; +} + +// Marshall the ImageVector data into a D-BUS argument +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageVector &iconVector) +{ + argument.beginArray(qMetaTypeId()); + for (int i = 0; i < iconVector.size(); ++i) { + argument << iconVector[i]; + } + argument.endArray(); + return argument; +} + +// Retrieve the ImageVector data from the D-BUS argument +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusImageVector &iconVector) +{ + argument.beginArray(); + iconVector.clear(); + + while (!argument.atEnd()) { + KDbusImageStruct element; + argument >> element; + iconVector.append(element); + } + + argument.endArray(); + + return argument; +} + +// Marshall the ToolTipStruct data into a D-BUS argument +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusToolTipStruct &toolTip) +{ + argument.beginStructure(); + argument << toolTip.icon; + argument << toolTip.image; + argument << toolTip.title; + argument << toolTip.subTitle; + argument.endStructure(); + return argument; +} + +// Retrieve the ToolTipStruct data from the D-BUS argument +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusToolTipStruct &toolTip) +{ + QString icon; + KDbusImageVector image; + QString title; + QString subTitle; + + argument.beginStructure(); + argument >> icon; + argument >> image; + argument >> title; + argument >> subTitle; + argument.endStructure(); + + toolTip.icon = icon; + toolTip.image = image; + toolTip.title = title; + toolTip.subTitle = subTitle; + + return argument; +} diff --git a/snidbus.h b/snidbus.h new file mode 100644 index 0000000..b814edd --- /dev/null +++ b/snidbus.h @@ -0,0 +1,41 @@ +/* + SNI Dbus serialisers + Copyright 2015 David Edmundson + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include +#include +#include +#include +#include + +// Custom message type for DBus +struct KDbusImageStruct { + KDbusImageStruct(); + KDbusImageStruct(const QImage &image); + int width; + int height; + QByteArray data; +}; + +typedef QList KDbusImageVector; + +struct KDbusToolTipStruct { + QString icon; + KDbusImageVector image; + QString title; + QString subTitle; +}; + +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageStruct &icon); +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusImageStruct &icon); + +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageVector &iconVector); +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusImageVector &iconVector); + +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusToolTipStruct &toolTip); +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusToolTipStruct &toolTip); diff --git a/sniproxy.cpp b/sniproxy.cpp new file mode 100644 index 0000000..cec9b3f --- /dev/null +++ b/sniproxy.cpp @@ -0,0 +1,635 @@ +/* + Holds one embedded window, registers as DBus entry + SPDX-FileCopyrightText: 2015 David Edmundson + SPDX-FileCopyrightText: 2019 Konrad Materka + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "sniproxy.h" + +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include + +#include "statusnotifieritemadaptor.h" +#include "statusnotifierwatcher_interface.h" + +#include "../c_ptr.h" +#include "debug.h" +#include "xcbutils.h" +#include "xtestsender.h" +#include + +// #define VISUAL_DEBUG + +#define SNI_WATCHER_SERVICE_NAME "org.kde.StatusNotifierWatcher" +#define SNI_WATCHER_PATH "/StatusNotifierWatcher" + +#ifdef Status +typedef Status XStatus; +#undef Status +typedef XStatus Status; +#endif + +static uint16_t s_embedSize = 32; // max size of window to embed. We no longer resize the embedded window as Chromium acts stupidly. +static unsigned int XEMBED_VERSION = 0; + +int SNIProxy::s_serviceCount = 0; + +void xembed_message_send(xcb_window_t towin, long message, long d1, long d2, long d3) +{ + xcb_client_message_event_t ev; + + ev.response_type = XCB_CLIENT_MESSAGE; + ev.window = towin; + ev.format = 32; + ev.data.data32[0] = XCB_CURRENT_TIME; + ev.data.data32[1] = message; + ev.data.data32[2] = d1; + ev.data.data32[3] = d2; + ev.data.data32[4] = d3; + ev.type = Xcb::atoms->xembedAtom; + xcb_send_event(qGuiApp->nativeInterface()->connection(), false, towin, XCB_EVENT_MASK_NO_EVENT, (char *)&ev); +} + +static bool checkWindowOrDescendantWantButtonEvents(xcb_window_t window) +{ + auto connection = qGuiApp->nativeInterface()->connection(); + auto waCookie = xcb_get_window_attributes(connection, window); + UniqueCPointer windowAttributes(xcb_get_window_attributes_reply(connection, waCookie, nullptr)); + if (windowAttributes && windowAttributes->all_event_masks & XCB_EVENT_MASK_BUTTON_PRESS) { + return true; + } + if (windowAttributes && windowAttributes->do_not_propagate_mask & XCB_EVENT_MASK_BUTTON_PRESS) { + return false; + } + auto treeCookie = xcb_query_tree(connection, window); + UniqueCPointer tree(xcb_query_tree_reply(connection, treeCookie, nullptr)); + if (!tree) { + return false; + } + std::span children(xcb_query_tree_children(tree.get()), xcb_query_tree_children_length(tree.get())); + return std::ranges::any_of(children, &checkWindowOrDescendantWantButtonEvents); +} + +SNIProxy::SNIProxy(xcb_window_t wid, QObject *parent) + : QObject(parent) + , + // Work round a bug in our SNIWatcher with multiple SNIs per connection. + // there is an undocumented feature that you can register an SNI by path, however it doesn't detect an object on a service being removed, only the entire + // service closing instead lets use one DBus connection per SNI + m_dbus(QDBusConnection::connectToBus(QDBusConnection::SessionBus, QStringLiteral("XembedSniProxy%1").arg(s_serviceCount++))) + , m_x11Interface(qGuiApp->nativeInterface()) + , m_windowId(wid) + , m_injectMode(Direct) +{ + // create new SNI + new StatusNotifierItemAdaptor(this); + m_dbus.registerObject(QStringLiteral("/StatusNotifierItem"), this); + + auto statusNotifierWatcher = + new org::kde::StatusNotifierWatcher(QStringLiteral(SNI_WATCHER_SERVICE_NAME), QStringLiteral(SNI_WATCHER_PATH), QDBusConnection::sessionBus(), this); + auto reply = statusNotifierWatcher->RegisterStatusNotifierItem(m_dbus.baseService()); + reply.waitForFinished(); + if (reply.isError()) { + qCWarning(SNIPROXY) << "could not register SNI:" << reply.error().message(); + } + + auto c = m_x11Interface->connection(); + + // create a container window + auto screen = xcb_setup_roots_iterator(xcb_get_setup(c)).data; + m_containerWid = xcb_generate_id(c); + uint32_t values[3]; + uint32_t mask = XCB_CW_BACK_PIXEL | XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK; + values[0] = screen->black_pixel; // draw a solid background so the embedded icon doesn't get garbage in it + values[1] = true; // bypass wM + values[2] = XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT; + xcb_create_window(c, /* connection */ + XCB_COPY_FROM_PARENT, /* depth */ + m_containerWid, /* window Id */ + screen->root, /* parent window */ + 0, + 0, /* x, y */ + s_embedSize, + s_embedSize, /* width, height */ + 0, /* border_width */ + XCB_WINDOW_CLASS_INPUT_OUTPUT, /* class */ + screen->root_visual, /* visual */ + mask, + values); /* masks */ + + /* + We need the window to exist and be mapped otherwise the child won't render it's contents + + We also need it to exist in the right place to get the clicks working as GTK will check sendEvent locations to see if our window is in the right place. + So even though our contents are drawn via compositing we still put this window in the right place + + Set opacity to 0 just to make sure this container never appears + And set the input region to null so everything just clicks through + */ + + setActiveForInput(false); + +#ifndef VISUAL_DEBUG + + NETWinInfo wm(c, m_containerWid, screen->root, NET::Properties(), NET::Properties2()); + wm.setOpacity(0); +#endif + + xcb_flush(c); + + xcb_map_window(c, m_containerWid); + + xcb_reparent_window(c, wid, m_containerWid, 0, 0); + + /* + * Render the embedded window offscreen + */ + xcb_composite_redirect_window(c, wid, XCB_COMPOSITE_REDIRECT_MANUAL); + + /* we grab the window, but also make sure it's automatically reparented back + * to the root window if we should die. + */ + xcb_change_save_set(c, XCB_SET_MODE_INSERT, wid); + + // tell client we're embedding it + xembed_message_send(wid, XEMBED_EMBEDDED_NOTIFY, 0, m_containerWid, XEMBED_VERSION); + + // move window we're embedding + const uint32_t windowMoveConfigVals[2] = {0, 0}; + + xcb_configure_window(c, wid, XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y, windowMoveConfigVals); + + QSize clientWindowSize = calculateClientWindowSize(); + + // show the embedded window otherwise nothing happens + xcb_map_window(c, wid); + + xcb_clear_area(c, 0, wid, 0, 0, clientWindowSize.width(), clientWindowSize.height()); + + xcb_flush(c); + + // guess which input injection method to use + // we can either send an X event to the client or XTest + // some don't support direct X events (GTK3/4), and some don't support XTest because reasons + // note also some clients might not have the XTest extension. We may as well assume it does and just fail to send later. + + // we query if the client selected button presses in the event mask + // if the client does supports that we send directly, otherwise we'll use xtest + auto waCookie = xcb_get_window_attributes(c, wid); + UniqueCPointer windowAttributes(xcb_get_window_attributes_reply(c, waCookie, nullptr)); + if (!checkWindowOrDescendantWantButtonEvents(wid)) { + m_injectMode = XTest; + } + + // there's no damage event for the first paint, and sometimes it's not drawn immediately + // not ideal, but it works better than nothing + // test with xchat before changing + QTimer::singleShot(500, this, &SNIProxy::update); +} + +SNIProxy::~SNIProxy() +{ + xcb_destroy_window(m_x11Interface->connection(), m_containerWid); + QDBusConnection::disconnectFromBus(m_dbus.name()); +} + +void SNIProxy::update() +{ + QImage image = getImageNonComposite(); + if (image.isNull()) { + qCDebug(SNIPROXY) << "No xembed icon for" << m_windowId << Title(); + return; + } + + int w = image.width(); + int h = image.height(); + + m_pixmap = QPixmap::fromImage(std::move(image)); + if (w > s_embedSize || h > s_embedSize) { + qCDebug(SNIPROXY) << "Scaling pixmap of window" << m_windowId << Title() << "from w*h" << w << h; + m_pixmap = m_pixmap.scaled(s_embedSize, s_embedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + } + Q_EMIT NewIcon(); + Q_EMIT NewToolTip(); +} + +void SNIProxy::resizeWindow(const uint16_t width, const uint16_t height) const +{ + auto connection = m_x11Interface->connection(); + + uint16_t widthNormalized = std::min(width, s_embedSize); + uint16_t heighNormalized = std::min(height, s_embedSize); + + const uint32_t windowSizeConfigVals[2] = {widthNormalized, heighNormalized}; + xcb_configure_window(connection, m_windowId, XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, windowSizeConfigVals); + + xcb_flush(connection); +} + +QSize SNIProxy::calculateClientWindowSize() const +{ + auto c = m_x11Interface->connection(); + + auto cookie = xcb_get_geometry(c, m_windowId); + UniqueCPointer clientGeom(xcb_get_geometry_reply(c, cookie, nullptr)); + + QSize clientWindowSize; + if (clientGeom) { + clientWindowSize = QSize(clientGeom->width, clientGeom->height); + } + // if the window is a clearly stupid size resize to be something sensible + // this is needed as chromium and such when resized just fill the icon with transparent space and only draw in the middle + // however KeePass2 does need this as by default the window size is 273px wide and is not transparent + // use an arbitrary heuristic to make sure icons are always sensible + if (clientWindowSize.isEmpty() || clientWindowSize.width() > s_embedSize || clientWindowSize.height() > s_embedSize) { + qCDebug(SNIPROXY) << "Resizing window" << m_windowId << Title() << "from w*h" << clientWindowSize; + + resizeWindow(s_embedSize, s_embedSize); + + clientWindowSize = QSize(s_embedSize, s_embedSize); + } + + return clientWindowSize; +} + +void sni_cleanup_xcb_image(void *data) +{ + xcb_image_destroy(static_cast(data)); +} + +bool SNIProxy::isTransparentImage(const QImage &image) const +{ + int w = image.width(); + int h = image.height(); + + // check for the center and sub-center pixels first and avoid full image scan + if (!(qAlpha(image.pixel(w >> 1, h >> 1)) + qAlpha(image.pixel(w >> 2, h >> 2)) == 0)) + return false; + + // skip scan altogether if sub-center pixel found to be opaque + // and break out from the outer loop too on full scan + for (int x = 0; x < w; ++x) { + for (int y = 0; y < h; ++y) { + if (qAlpha(image.pixel(x, y))) { + // Found an opaque pixel. + return false; + } + } + } + + return true; +} + +QImage SNIProxy::getImageNonComposite() const +{ + auto c = m_x11Interface->connection(); + + QSize clientWindowSize = calculateClientWindowSize(); + + xcb_image_t *image = xcb_image_get(c, m_windowId, 0, 0, clientWindowSize.width(), clientWindowSize.height(), 0xFFFFFFFF, XCB_IMAGE_FORMAT_Z_PIXMAP); + + // Don't hook up cleanup yet, we may use a different QImage after all + QImage naiveConversion; + if (image) { + naiveConversion = QImage(image->data, image->width, image->height, QImage::Format_ARGB32); + } else { + qCDebug(SNIPROXY) << "Skip NULL image returned from xcb_image_get() for" << m_windowId << Title(); + return {}; + } + + if (isTransparentImage(naiveConversion)) { + QImage elaborateConversion = QImage(convertFromNative(image)); + + // Update icon only if it is at least partially opaque. + // This is just a workaround for X11 bug: xembed icon may suddenly + // become transparent for a one or few frames. Reproducible at least + // with WINE applications. + if (isTransparentImage(elaborateConversion)) { + qCDebug(SNIPROXY) << "Skip transparent xembed icon for" << m_windowId << Title(); + return {}; + } else + return elaborateConversion; + } else { + // Now we are sure we can eventually delete the xcb_image_t with this version + return {image->data, image->width, image->height, image->stride, QImage::Format_ARGB32, sni_cleanup_xcb_image, image}; + } +} + +QImage SNIProxy::convertFromNative(xcb_image_t *xcbImage) const +{ + QImage::Format format = QImage::Format_Invalid; + + switch (xcbImage->depth) { + case 1: + format = QImage::Format_MonoLSB; + break; + case 16: + format = QImage::Format_RGB16; + break; + case 24: + format = QImage::Format_RGB32; + break; + case 30: { + // Qt doesn't have a matching image format. We need to convert manually + auto *pixels = reinterpret_cast(xcbImage->data); + for (uint i = 0; i < (xcbImage->size / 4); i++) { + int r = (pixels[i] >> 22) & 0xff; + int g = (pixels[i] >> 12) & 0xff; + int b = (pixels[i] >> 2) & 0xff; + + pixels[i] = qRgba(r, g, b, 0xff); + } + // fall through, Qt format is still Format_ARGB32_Premultiplied + Q_FALLTHROUGH(); + } + case 32: + format = QImage::Format_ARGB32_Premultiplied; + break; + default: + return {}; // we don't know + } + + QImage image(xcbImage->data, xcbImage->width, xcbImage->height, xcbImage->stride, format, sni_cleanup_xcb_image, xcbImage); + + if (image.isNull()) { + return {}; + } + + if (format == QImage::Format_RGB32 && xcbImage->bpp == 32) { + QImage m = image.createHeuristicMask(); + QPixmap p = QPixmap::fromImage(std::move(image)); + p.setMask(QBitmap::fromImage(std::move(m))); + image = p.toImage(); + } + + // work around an abort in QImage::color + if (image.format() == QImage::Format_MonoLSB) { + image.setColorCount(2); + image.setColor(0, QColor(Qt::white).rgb()); + image.setColor(1, QColor(Qt::black).rgb()); + } + + return image; +} + +/* + Wine is using XWindow Shape Extension for transparent tray icons. + We need to find first clickable point starting from top-left. +*/ +QPoint SNIProxy::calculateClickPoint() const +{ + QSize clientSize = calculateClientWindowSize(); + QPoint clickPoint = QPoint(clientSize.width() / 2, clientSize.height() / 2); + + auto c = m_x11Interface->connection(); + + // request extent to check if shape has been set + xcb_shape_query_extents_cookie_t extentsCookie = xcb_shape_query_extents(c, m_windowId); + // at the same time make the request for rectangles (even if this request isn't needed) + xcb_shape_get_rectangles_cookie_t rectaglesCookie = xcb_shape_get_rectangles(c, m_windowId, XCB_SHAPE_SK_BOUNDING); + + UniqueCPointer extentsReply(xcb_shape_query_extents_reply(c, extentsCookie, nullptr)); + UniqueCPointer rectanglesReply(xcb_shape_get_rectangles_reply(c, rectaglesCookie, nullptr)); + + if (!extentsReply || !rectanglesReply || !extentsReply->bounding_shaped) { + return clickPoint; + } + + xcb_rectangle_t *rectangles = xcb_shape_get_rectangles_rectangles(rectanglesReply.get()); + if (!rectangles) { + return clickPoint; + } + + const QImage image = getImageNonComposite(); + + double minLength = sqrt(pow(image.height(), 2) + pow(image.width(), 2)); + const int nRectangles = xcb_shape_get_rectangles_rectangles_length(rectanglesReply.get()); + for (int i = 0; i < nRectangles; ++i) { + double length = sqrt(pow(rectangles[i].x, 2) + pow(rectangles[i].y, 2)); + if (length < minLength) { + minLength = length; + clickPoint = QPoint(rectangles[i].x, rectangles[i].y); + } + } + + qCDebug(SNIPROXY) << "Click point:" << clickPoint; + return clickPoint; +} + +void SNIProxy::setActiveForInput(bool active) const +{ + auto c = m_x11Interface->connection(); + if (active) { + xcb_rectangle_t rectangle; + rectangle.x = 0; + rectangle.y = 0; + rectangle.width = s_embedSize; + rectangle.height = s_embedSize; + xcb_shape_rectangles(c, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, 0, m_containerWid, 0, 0, 1, &rectangle); + + const uint32_t stackData[] = {XCB_STACK_MODE_ABOVE}; + xcb_configure_window(c, m_containerWid, XCB_CONFIG_WINDOW_STACK_MODE, stackData); + } else { + xcb_rectangle_t rectangle; + rectangle.x = 0; + rectangle.y = 0; + rectangle.width = 0; + rectangle.height = 0; + xcb_shape_rectangles(c, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, 0, m_containerWid, 0, 0, 1, &rectangle); + + const uint32_t stackData[] = {XCB_STACK_MODE_BELOW}; + xcb_configure_window(c, m_containerWid, XCB_CONFIG_WINDOW_STACK_MODE, stackData); + } +} + +//____________properties__________ + +QString SNIProxy::Category() const +{ + return QStringLiteral("ApplicationStatus"); +} + +QString SNIProxy::Id() const +{ + const auto title = Title(); + // we always need /some/ ID so if no window title exists, just use the winId. + if (title.isEmpty()) { + return QString::number(m_windowId); + } + return title; +} + +KDbusImageVector SNIProxy::IconPixmap() const +{ + KDbusImageStruct dbusImage(m_pixmap.toImage()); + return KDbusImageVector() << dbusImage; +} + +bool SNIProxy::ItemIsMenu() const +{ + return false; +} + +QString SNIProxy::Status() const +{ + return QStringLiteral("Active"); +} + +QString SNIProxy::Title() const +{ + KWindowInfo window(m_windowId, NET::WMName); + return window.name(); +} + +int SNIProxy::WindowId() const +{ + return m_windowId; +} + +//____________actions_____________ + +void SNIProxy::Activate(int x, int y) +{ + sendClick(XCB_BUTTON_INDEX_1, x, y); +} + +void SNIProxy::SecondaryActivate(int x, int y) +{ + sendClick(XCB_BUTTON_INDEX_2, x, y); +} + +void SNIProxy::ContextMenu(int x, int y) +{ + sendClick(XCB_BUTTON_INDEX_3, x, y); +} + +void SNIProxy::Scroll(int delta, const QString &orientation) +{ + if (orientation == QLatin1String("vertical")) { + sendClick(delta > 0 ? XCB_BUTTON_INDEX_4 : XCB_BUTTON_INDEX_5, 0, 0); + } else { + sendClick(delta > 0 ? 6 : 7, 0, 0); + } +} + +void SNIProxy::sendClick(uint8_t mouseButton, int x, int y) +{ + // it's best not to look at this code + // GTK doesn't like send_events and double checks the mouse position matches where the window is and is top level + // in order to solve this we move the embed container over to where the mouse is then replay the event using send_event + // if patching, test with xchat + xchat context menus + + // note x,y are not actually where the mouse is, but the plasmoid + // ideally we should make this match the plasmoid hit area + + qCDebug(SNIPROXY) << "Received click" << mouseButton << "with passed x*y" << x << y; + + auto c = m_x11Interface->connection(); + + auto cookieSize = xcb_get_geometry(c, m_windowId); + UniqueCPointer clientGeom(xcb_get_geometry_reply(c, cookieSize, nullptr)); + + if (!clientGeom) { + return; + } + + /*qCDebug(SNIPROXY) << "samescreen" << pointer->same_screen << endl + << "root x*y" << pointer->root_x << pointer->root_y << endl + << "win x*y" << pointer->win_x << pointer->win_y;*/ + + // move our window so the mouse is within its geometry + uint32_t configVals[2] = {0, 0}; + const QPoint clickPoint = calculateClickPoint(); + + if (mouseButton >= XCB_BUTTON_INDEX_4) { + // scroll event, take pointer position + auto cookie = xcb_query_pointer(c, m_windowId); + UniqueCPointer pointer(xcb_query_pointer_reply(c, cookie, nullptr)); + configVals[0] = pointer->root_x; + configVals[1] = pointer->root_y; + } else { + configVals[0] = static_cast(x - clickPoint.x()); + configVals[1] = static_cast(y - clickPoint.y()); + } + xcb_configure_window(c, m_containerWid, XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y, configVals); + + setActiveForInput(true); + + if (qgetenv("XDG_SESSION_TYPE") == "wayland") { + xcb_warp_pointer(c, XCB_NONE, m_windowId, 0, 0, 0, 0, clickPoint.x(), clickPoint.y()); + } + + // mouse down + if (m_injectMode == Direct) { + auto *event = new xcb_button_press_event_t; + memset(event, 0x00, sizeof(xcb_button_press_event_t)); + event->response_type = XCB_BUTTON_PRESS; + event->event = m_windowId; + event->time = XCB_CURRENT_TIME; + event->same_screen = 1; + event->root = DefaultRootWindow(m_x11Interface->display()); + event->root_x = x; + event->root_y = y; + event->event_x = static_cast(clickPoint.x()); + event->event_y = static_cast(clickPoint.y()); + event->child = 0; + event->state = 0; + event->detail = mouseButton; + + xcb_send_event(c, false, m_windowId, XCB_EVENT_MASK_BUTTON_PRESS, (char *)event); + delete event; + } else { + sendXTestPressed(m_x11Interface->display(), mouseButton); + } + + // mouse up + if (m_injectMode == Direct) { + auto *event = new xcb_button_release_event_t; + memset(event, 0x00, sizeof(xcb_button_release_event_t)); + event->response_type = XCB_BUTTON_RELEASE; + event->event = m_windowId; + event->time = XCB_CURRENT_TIME; + event->same_screen = 1; + event->root = DefaultRootWindow(m_x11Interface->display()); + event->root_x = x; + event->root_y = y; + event->event_x = static_cast(clickPoint.x()); + event->event_y = static_cast(clickPoint.y()); + event->child = 0; + event->state = 0; + event->detail = mouseButton; + + xcb_send_event(c, false, m_windowId, XCB_EVENT_MASK_BUTTON_RELEASE, (char *)event); + delete event; + } else { + sendXTestReleased(m_x11Interface->display(), mouseButton); + } + + if (m_injectMode == Direct) { + setActiveForInput(false); + } else { + // delayed because on xwayland with the new libei path it will go to XWayland + // then kwin, then back to X + // we need to delay slightly until that happens + if (qgetenv("XDG_SESSION_TYPE") == QByteArrayLiteral("wayland")) { + QTimer::singleShot(300, this, [this]() { + setActiveForInput(false); + }); + } else { + setActiveForInput(false); + } + } +} diff --git a/sniproxy.h b/sniproxy.h new file mode 100644 index 0000000..d474a39 --- /dev/null +++ b/sniproxy.h @@ -0,0 +1,153 @@ +/* + Holds one embedded window, registers as DBus entry + SPDX-FileCopyrightText: 2015 David Edmundson + SPDX-FileCopyrightText: 2019 Konrad Materka + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "snidbus.h" + +class SNIProxy : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString Category READ Category) + Q_PROPERTY(QString Id READ Id) + Q_PROPERTY(QString Title READ Title) + Q_PROPERTY(QString Status READ Status) + Q_PROPERTY(int WindowId READ WindowId) + Q_PROPERTY(bool ItemIsMenu READ ItemIsMenu) + Q_PROPERTY(KDbusImageVector IconPixmap READ IconPixmap) + +public: + explicit SNIProxy(xcb_window_t wid, QObject *parent = nullptr); + ~SNIProxy() override; + + void update(); + void resizeWindow(const uint16_t width, const uint16_t height) const; + + /** + * @return the category of the application associated to this item + * @see Category + */ + QString Category() const; + + /** + * @return the id of this item + */ + QString Id() const; + + /** + * @return the title of this item + */ + QString Title() const; + + /** + * @return The status of this item + * @see Status + */ + QString Status() const; + + /** + * @return The id of the main window of the application that controls the item + */ + int WindowId() const; + + /** + * @return The item only support the context menu, the visualization should prefer sending ContextMenu() instead of Activate() + */ + bool ItemIsMenu() const; + + /** + * @return a serialization of the icon data + */ + KDbusImageVector IconPixmap() const; + +public Q_SLOTS: + // interaction + /** + * Shows the context menu associated to this item + * at the desired screen position + */ + void ContextMenu(int x, int y); + + /** + * Shows the main widget and try to position it on top + * of the other windows, if the widget is already visible, hide it. + */ + void Activate(int x, int y); + + /** + * The user activated the item in an alternate way (for instance with middle mouse button, this depends from the systray implementation) + */ + void SecondaryActivate(int x, int y); + + /** + * Inform this item that the mouse wheel was used on its representation + */ + void Scroll(int delta, const QString &orientation); + +Q_SIGNALS: + /** + * Inform the systemtray that the own main icon has been changed, + * so should be reloaded + */ + void NewIcon(); + + /** + * Inform the systemtray that there is a new icon to be used as overlay + */ + void NewOverlayIcon(); + + /** + * Inform the systemtray that the requesting attention icon + * has been changed, so should be reloaded + */ + void NewAttentionIcon(); + + /** + * Inform the systemtray that something in the tooltip has been changed + */ + void NewToolTip(); + + /** + * Signal the new status when it has been changed + * @see Status + */ + void NewStatus(const QString &status); + +private: + enum InjectMode { + Direct, + XTest, + }; + + QSize calculateClientWindowSize() const; + void sendClick(uint8_t mouseButton, int x, int y); + QImage getImageNonComposite() const; + bool isTransparentImage(const QImage &image) const; + QImage convertFromNative(xcb_image_t *xcbImage) const; + QPoint calculateClickPoint() const; + void setActiveForInput(bool active) const; + + QDBusConnection m_dbus; + QNativeInterface::QX11Application *m_x11Interface = nullptr; + xcb_window_t m_windowId; + xcb_window_t m_containerWid; + static int s_serviceCount; + QPixmap m_pixmap; + InjectMode m_injectMode; +}; diff --git a/xcbutils.h b/xcbutils.h new file mode 100644 index 0000000..0b788b1 --- /dev/null +++ b/xcbutils.h @@ -0,0 +1,122 @@ +/* + SPDX-FileCopyrightText: 2012, 2013 Martin Graesslin + SPDX-FileCopyrightText: 2015 David Edmudson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "../c_ptr.h" +#include + +/** XEMBED messages */ +#define XEMBED_EMBEDDED_NOTIFY 0 +#define XEMBED_WINDOW_ACTIVATE 1 +#define XEMBED_WINDOW_DEACTIVATE 2 +#define XEMBED_REQUEST_FOCUS 3 +#define XEMBED_FOCUS_IN 4 +#define XEMBED_FOCUS_OUT 5 +#define XEMBED_FOCUS_NEXT 6 +#define XEMBED_FOCUS_PREV 7 + +namespace Xcb +{ +typedef xcb_window_t WindowId; + +class Atom +{ +public: + explicit Atom(const QByteArray &name, + bool onlyIfExists = false, + xcb_connection_t *c = qGuiApp->nativeInterface()->connection()) + : m_connection(c) + , m_retrieved(false) + , m_cookie(xcb_intern_atom_unchecked(m_connection, onlyIfExists, name.length(), name.constData())) + , m_atom(XCB_ATOM_NONE) + , m_name(name) + { + } + Atom() = delete; + Atom(const Atom &) = delete; + + ~Atom() + { + if (!m_retrieved && m_cookie.sequence) { + xcb_discard_reply(m_connection, m_cookie.sequence); + } + } + + operator xcb_atom_t() const + { + (const_cast(this))->getReply(); + return m_atom; + } + bool isValid() + { + getReply(); + return m_atom != XCB_ATOM_NONE; + } + bool isValid() const + { + (const_cast(this))->getReply(); + return m_atom != XCB_ATOM_NONE; + } + + inline const QByteArray &name() const + { + return m_name; + } + +private: + void getReply() + { + if (m_retrieved || !m_cookie.sequence) { + return; + } + UniqueCPointer reply(xcb_intern_atom_reply(m_connection, m_cookie, nullptr)); + if (reply) { + m_atom = reply->atom; + } + m_retrieved = true; + } + xcb_connection_t *m_connection; + bool m_retrieved; + xcb_intern_atom_cookie_t m_cookie; + xcb_atom_t m_atom; + QByteArray m_name; +}; + +class Atoms +{ +public: + Atoms() + : xembedAtom("_XEMBED") + , selectionAtom(xcb_atom_name_by_screen("_NET_SYSTEM_TRAY", DefaultScreen(qGuiApp->nativeInterface()->display()))) + , opcodeAtom("_NET_SYSTEM_TRAY_OPCODE") + , messageData("_NET_SYSTEM_TRAY_MESSAGE_DATA") + , visualAtom("_NET_SYSTEM_TRAY_VISUAL") + { + } + + Atom xembedAtom; + Atom selectionAtom; + Atom opcodeAtom; + Atom messageData; + Atom visualAtom; +}; + +extern Atoms *atoms; + +} // namespace Xcb diff --git a/xembedsniproxy.desktop b/xembedsniproxy.desktop new file mode 100644 index 0000000..e3a3914 --- /dev/null +++ b/xembedsniproxy.desktop @@ -0,0 +1,64 @@ +[Desktop Entry] +Exec=xembedsniproxy +Name=XembedSniProxy +Name[ar]=ميفاق وكيل sni مضمن +Name[ast]=XembedSniProxy +Name[az]=XembedSniProxy +Name[be]=XembedSniProxy +Name[bg]=XembedSniProxy +Name[ca]=XembedSniProxy +Name[ca@valencia]=XembedSniProxy +Name[cs]=XembedSniProxy +Name[da]=XembedSniProxy +Name[de]=XembedSniProxy +Name[el]=XembedSniProxy +Name[en_GB]=XembedSniProxy +Name[eo]=XembedSniProxy +Name[es]=XembedSniProxy +Name[et]=XembedSniProxy +Name[eu]=XembedSniProxy +Name[fi]=XembedSniProxy +Name[fr]=XembedSniProxy +Name[gl]=XembedSniProxy +Name[he]=XembedSniProxy +Name[hi]=एक्सएमबेड-एसएनआइ-प्रॉक्सी +Name[hu]=XembedSniProxy +Name[ia]=XembedSniProxy +Name[id]=XembedSniProxy +Name[is]=XembedSniProxy +Name[it]=XembedSniProxy +Name[ja]=XembedSniProxy +Name[ka]=XembedSniProxy +Name[ko]=XembedSniProxy +Name[lt]=XembedSniProxy +Name[lv]=XembedSniProxy +Name[ml]=എക്സ്എമ്പെഡ്സ്നിപ്രോക്സി +Name[nb]=XembedSniProxy +Name[nl]=XembedSniProxy +Name[nn]=XembedSniProxy +Name[pa]=XembedSniProxy +Name[pl]=XembedSniProxy +Name[pt]=XembedSniProxy +Name[pt_BR]=XembedSniProxy +Name[ro]=XembedSniProxy +Name[ru]=XembedSniProxy +Name[sa]=XembedSniProxy +Name[sk]=XembedSniProxy +Name[sl]=XembedSniProxy +Name[sr]=Иксембед‑сни‑прокси +Name[sr@ijekavian]=Иксембед‑сни‑прокси +Name[sr@ijekavianlatin]=XembedSniProxy +Name[sr@latin]=XembedSniProxy +Name[sv]=XembedSniProxy +Name[th]=XembedSniProxy +Name[tr]=XembedSniProxy +Name[uk]=XembedSniProxy +Name[vi]=XembedSniProxy +Name[zh_CN]=XembedSniProxy +Name[zh_TW]=XembedSniProxy +Type=Application +StartupNotify=false +NoDisplay=true +OnlyShowIn=KDE; +X-KDE-autostart-phase=0 +X-systemd-skip=true diff --git a/xtestsender.cpp b/xtestsender.cpp new file mode 100644 index 0000000..69833d4 --- /dev/null +++ b/xtestsender.cpp @@ -0,0 +1,19 @@ +/* Wrap XLIB code in a new file as it defines keywords that conflict with Qt + + SPDX-FileCopyrightText: 2017 David Edmundson + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "xtestsender.h" +#include + +void sendXTestPressed(Display *display, int button) +{ + XTestFakeButtonEvent(display, button, true, 0); +} + +void sendXTestReleased(Display *display, int button) +{ + XTestFakeButtonEvent(display, button, false, 0); +} diff --git a/xtestsender.h b/xtestsender.h new file mode 100644 index 0000000..50176bd --- /dev/null +++ b/xtestsender.h @@ -0,0 +1,12 @@ +/* Wrap XLIB code in a new file as it defines keywords that conflict with Qt + + SPDX-FileCopyrightText: 2017 David Edmundson + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ +#pragma once + +typedef struct _XDisplay Display; + +void sendXTestPressed(Display *display, int button); +void sendXTestReleased(Display *display, int button);