From dbb755c976a5e787630f1acaaaeba631a98a5249 Mon Sep 17 00:00:00 2001 From: Wang Zichong Date: Thu, 18 Dec 2025 16:52:13 +0800 Subject: [PATCH] wip: sniproxy to traymanager1 --- .vscode/launch.json | 21 ++ .vscode/tasks.json | 19 + CMakeLists.txt | 27 +- fdoselectionmanager.cpp | 57 ++- fdoselectionmanager.h | 7 +- main.cpp | 8 +- org.deepin.dde.TrayManager1.xml | 38 ++ snidbus.cpp | 131 ------- snidbus.h | 41 --- sniproxy.cpp | 635 -------------------------------- sniproxy.h | 153 -------- traylist.h | 7 + traymanager1.cpp | 96 +++++ traymanager1.h | 83 +++++ traymanagerproxy.cpp | 302 +++++++++++++++ traymanagerproxy.h | 62 ++++ xcbutils.h | 2 +- 17 files changed, 697 insertions(+), 992 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 org.deepin.dde.TrayManager1.xml delete mode 100644 snidbus.cpp delete mode 100644 snidbus.h delete mode 100644 sniproxy.cpp delete mode 100644 sniproxy.h create mode 100644 traylist.h create mode 100644 traymanager1.cpp create mode 100644 traymanager1.h create mode 100644 traymanagerproxy.cpp create mode 100644 traymanagerproxy.h diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..da035eb --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Debug", + "type": "gdb", + "request": "launch", + "target": "./build/xembed-traymanager-proxy", + "cwd": "${workspaceRoot}", + "env": { + "DISPLAY": ":0", + "QT_LOGGING_RULES": "dde.*.debug=true;", + }, + "valuesFormatting": "prettyPrinters", + "preLaunchTask": "Build (Debug)" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..7c38afa --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,19 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Config", + "type": "shell", + "command": "cmake", + "args": ["-Bbuild", ".", "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", "-DCMAKE_BUILD_TYPE=Debug"] + }, + { + "label": "Build (Debug)", + "type": "shell", + "command": "cmake", + "args": ["--build", "build", "-j4"] + } + ] +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e67c97..135a220 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,10 @@ cmake_minimum_required(VERSION 3.16) -project(xembed-sni-proxy) +project(xembed-traymanager-proxy) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_CXX_STANDARD 20) +set(CMAKE_INCLUDE_CURRENT_DIR ON) # ensure adapter class can include related header find_package(Qt6 6.8 CONFIG REQUIRED COMPONENTS DBus) find_package(ECM REQUIRED NO_MODULE) @@ -41,33 +42,35 @@ set(XCB_LIBS set(XEMBED_SNI_PROXY_SOURCES main.cpp fdoselectionmanager.cpp fdoselectionmanager.h - snidbus.cpp snidbus.h - sniproxy.cpp + traymanager1.cpp traymanager1.h + traymanagerproxy.cpp traymanagerproxy.h xtestsender.cpp xtestsender.h ) -qt_add_dbus_adaptor(XEMBED_SNI_PROXY_SOURCES org.kde.StatusNotifierItem.xml - sniproxy.h SNIProxy) +set_source_files_properties( + ${CMAKE_CURRENT_SOURCE_DIR}/org.deepin.dde.TrayManager1.xml + PROPERTIES INCLUDE traylist.h + CLASSNAME TrayManager +) -set(statusnotifierwatcher_xml org.kde.StatusNotifierWatcher.xml) -qt_add_dbus_interface(XEMBED_SNI_PROXY_SOURCES ${statusnotifierwatcher_xml} statusnotifierwatcher_interface) +qt_add_dbus_adaptor(XEMBED_SNI_PROXY_SOURCES org.deepin.dde.TrayManager1.xml traymanager1.h TrayManager1) ecm_qt_declare_logging_category(XEMBED_SNI_PROXY_SOURCES HEADER debug.h IDENTIFIER SNIPROXY - CATEGORY_NAME kde.xembedsniproxy + CATEGORY_NAME dde.xembedsniproxy DEFAULT_SEVERITY Info DESCRIPTION "xembed sni proxy" EXPORT PLASMAWORKSPACE ) -add_executable(xembedsniproxy ${XEMBED_SNI_PROXY_SOURCES}) -set_property(TARGET xembedsniproxy PROPERTY AUTOMOC ON) +add_executable(xembed-traymanager-proxy ${XEMBED_SNI_PROXY_SOURCES}) +set_property(TARGET xembed-traymanager-proxy PROPERTY AUTOMOC ON) set_package_properties(XCB PROPERTIES TYPE REQUIRED) -target_link_libraries(xembedsniproxy +target_link_libraries(xembed-traymanager-proxy Qt::Core Qt::DBus KF6::WindowSystem @@ -75,7 +78,7 @@ target_link_libraries(xembedsniproxy X11::Xtst ) -install(TARGETS xembedsniproxy ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) +install(TARGETS xembed-traymanager-proxy ${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/fdoselectionmanager.cpp b/fdoselectionmanager.cpp index 768bda4..4912642 100644 --- a/fdoselectionmanager.cpp +++ b/fdoselectionmanager.cpp @@ -10,6 +10,7 @@ #include "debug.h" #include +#include #include @@ -18,8 +19,8 @@ #include #include -#include "../c_ptr.h" -#include "sniproxy.h" +#include "traymanager1.h" +#include "traymanagerproxy.h" #include "xcbutils.h" #define SYSTEM_TRAY_REQUEST_DOCK 0 @@ -31,7 +32,7 @@ FdoSelectionManager::FdoSelectionManager() , m_x11Interface(qGuiApp->nativeInterface()) , m_selectionOwner(new KSelectionOwner(Xcb::atoms->selectionAtom, -1, this)) { - qCDebug(SNIPROXY) << "starting"; + qDebug(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); @@ -132,18 +133,18 @@ bool FdoSelectionManager::nativeEventFilter(const QByteArray &eventType, void *m } } 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(); + const auto tmProxy = m_proxies.value(damagedWId); + if (tmProxy) { + tmProxy->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) { + const auto tmProxy = m_proxies.value(event->window); + if (tmProxy) { // 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); + tmProxy->resizeWindow(event->width, event->height); } } } @@ -160,7 +161,13 @@ void FdoSelectionManager::dock(xcb_window_t winId) } if (addDamageWatch(winId)) { - m_proxies[winId] = new SNIProxy(winId, this); + auto proxy = new TrayManagerProxy(winId, this); + m_proxies[winId] = proxy; + + // Register with TrayManager1 if available + if (m_trayManager) { + m_trayManager->registerIcon(winId, proxy); + } } } @@ -171,6 +178,12 @@ void FdoSelectionManager::undock(xcb_window_t winId) if (!m_proxies.contains(winId)) { return; } + + // Unregister from TrayManager1 if available + if (m_trayManager) { + m_trayManager->unregisterIcon(winId); + } + m_proxies[winId]->deleteLater(); m_proxies.remove(winId); } @@ -179,6 +192,7 @@ void FdoSelectionManager::onClaimedOwnership() { qCDebug(SNIPROXY) << "Manager selection claimed"; + initTrayManager(); setSystemTrayVisual(); } @@ -224,3 +238,26 @@ void FdoSelectionManager::setSystemTrayVisual() xcb_change_property(c, XCB_PROP_MODE_REPLACE, m_selectionOwner->ownerWindow(), Xcb::atoms->visualAtom, XCB_ATOM_VISUALID, 32, 1, &trayVisual); } + +void FdoSelectionManager::initTrayManager() +{ + // Create and register the TrayManager1 DBus interface + if (!m_trayManager) { + m_trayManager = new TrayManager1(this); + + // Export the object on DBus + QDBusConnection::sessionBus().registerObject( + QStringLiteral("/org/deepin/dde/TrayManager1"), + m_trayManager, + QDBusConnection::ExportAdaptors + ); + + // Request the service name + QDBusConnection::sessionBus().registerService( + QStringLiteral("org.deepin.dde.TrayManager1") + ); + + qCDebug(SNIPROXY) << "TrayManager1 DBus interface registered"; + } +} + diff --git a/fdoselectionmanager.h b/fdoselectionmanager.h index 51ea029..0af1f59 100644 --- a/fdoselectionmanager.h +++ b/fdoselectionmanager.h @@ -15,7 +15,8 @@ #include class KSelectionOwner; -class SNIProxy; +class TrayManagerProxy; +class TrayManager1; class FdoSelectionManager : public QObject, public QAbstractNativeEventFilter { @@ -39,12 +40,14 @@ private: void dock(xcb_window_t embed_win); void undock(xcb_window_t client); void setSystemTrayVisual(); + void initTrayManager(); QNativeInterface::QX11Application *m_x11Interface = nullptr; + TrayManager1 *m_trayManager = nullptr; uint8_t m_damageEventBase; QHash m_damageWatches; - QHash m_proxies; + QHash m_proxies; KSelectionOwner *m_selectionOwner; }; diff --git a/main.cpp b/main.cpp index aed5d70..e6f73d5 100644 --- a/main.cpp +++ b/main.cpp @@ -10,7 +10,6 @@ #include "fdoselectionmanager.h" #include "debug.h" -#include "snidbus.h" #include "xcbutils.h" #ifdef None @@ -44,18 +43,13 @@ int main(int argc, char **argv) QGuiApplication app(argc, argv); if (!KWindowSystem::isPlatformX11()) { - qFatal("xembed-sni-proxy is only useful XCB. Aborting"); + qFatal("xembed-traymanager-proxy requires X11. Aborting"); } app.setQuitOnLastWindowClosed(false); - qDBusRegisterMetaType(); - qDBusRegisterMetaType(); - qDBusRegisterMetaType(); - Xcb::atoms = new Xcb::Atoms(); - // KDBusService service(KDBusService::Unique); FdoSelectionManager manager; auto rc = app.exec(); diff --git a/org.deepin.dde.TrayManager1.xml b/org.deepin.dde.TrayManager1.xml new file mode 100644 index 0000000..bc5e617 --- /dev/null +++ b/org.deepin.dde.TrayManager1.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/snidbus.cpp b/snidbus.cpp deleted file mode 100644 index 74a3624..0000000 --- a/snidbus.cpp +++ /dev/null @@ -1,131 +0,0 @@ -/* - 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 deleted file mode 100644 index b814edd..0000000 --- a/snidbus.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - 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 deleted file mode 100644 index cec9b3f..0000000 --- a/sniproxy.cpp +++ /dev/null @@ -1,635 +0,0 @@ -/* - 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 deleted file mode 100644 index d474a39..0000000 --- a/sniproxy.h +++ /dev/null @@ -1,153 +0,0 @@ -/* - 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/traylist.h b/traylist.h new file mode 100644 index 0000000..9f0cd73 --- /dev/null +++ b/traylist.h @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +typedef QList TrayList; diff --git a/traymanager1.cpp b/traymanager1.cpp new file mode 100644 index 0000000..2e52017 --- /dev/null +++ b/traymanager1.cpp @@ -0,0 +1,96 @@ +/* + Deepin DDE TrayManager1 implementation + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "traymanager1.h" +#include "traymanagerproxy.h" +#include "debug.h" +#include "traymanager1adaptor.h" + +TrayManager1::TrayManager1(QObject *parent) + : QObject(parent) + , m_adaptor(new TrayManager1Adaptor(this)) +{ + qCDebug(SNIPROXY) << "TrayManager1 created"; +} + +TrayManager1::~TrayManager1() +{ + qCDebug(SNIPROXY) << "TrayManager1 destroyed"; +} + +void TrayManager1::registerIcon(xcb_window_t win, TrayManagerProxy *proxy) +{ + if (m_icons.contains(win)) { + qCWarning(SNIPROXY) << "Icon already registered:" << win; + return; + } + + m_icons[win] = proxy; + qCDebug(SNIPROXY) << "Icon registered:" << win << "name:" << proxy->name(); + + Q_EMIT Added(static_cast(win)); +} + +void TrayManager1::unregisterIcon(xcb_window_t win) +{ + if (!m_icons.contains(win)) { + qCWarning(SNIPROXY) << "Icon not found for removal:" << win; + return; + } + + m_icons.remove(win); + qCDebug(SNIPROXY) << "Icon unregistered:" << win; + + Q_EMIT Removed(static_cast(win)); +} + +void TrayManager1::notifyIconChanged(xcb_window_t win) +{ + if (!m_icons.contains(win)) { + return; + } + + qCDebug(SNIPROXY) << "Icon changed:" << win; + Q_EMIT Changed(static_cast(win)); +} + +TrayList TrayManager1::trayIcons() const +{ + qDebug() << "trayIcons:" << m_icons.keys(); + TrayList result; + for (xcb_window_t win : m_icons.keys()) { + result << static_cast(win); + } + return result; +} + +TrayManagerProxy *TrayManager1::iconProxy(xcb_window_t win) const +{ + return m_icons.value(win, nullptr); +} + +// DBus method implementations +bool TrayManager1::Manage() +{ + qCDebug(SNIPROXY) << "Manage() called via DBus"; + return true; +} + +QString TrayManager1::GetName(uint32_t win) +{ + auto proxy = m_icons.value(static_cast(win), nullptr); + if (proxy) { + return proxy->name(); + } + return QString(); +} + +void TrayManager1::EnableNotification(uint32_t win, bool enabled) +{ + auto proxy = m_icons.value(static_cast(win), nullptr); + if (proxy) { + qCDebug(SNIPROXY) << "EnableNotification for" << win << "=" << enabled; + } +} diff --git a/traymanager1.h b/traymanager1.h new file mode 100644 index 0000000..28a2cda --- /dev/null +++ b/traymanager1.h @@ -0,0 +1,83 @@ +/* + Deepin DDE TrayManager1 implementation + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +class TrayManagerProxy; + +typedef QList TrayList; + +class TrayManager1Adaptor; +/** + * @brief TrayManager1 implements the org.deepin.dde.TrayManager1 DBus interface + * + * This class manages all embedded X11 tray icons and exposes them via DBus. + * It maintains a list of TrayManagerProxy objects and emits signals when + * icons are added, removed, or changed. + */ +class TrayManager1 : public QObject, protected QDBusContext +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.deepin.dde.TrayManager1") + Q_PROPERTY(TrayList TrayIcons READ trayIcons) + +public: + explicit TrayManager1(QObject *parent = nullptr); + ~TrayManager1() override; + + /** + * @brief Register a new tray icon with the manager + * @param win Window ID of the embedded tray icon + * @param proxy Pointer to the TrayManagerProxy managing this icon + */ + void registerIcon(xcb_window_t win, TrayManagerProxy *proxy); + + /** + * @brief Unregister a tray icon + * @param win Window ID of the icon + */ + void unregisterIcon(xcb_window_t win); + + /** + * @brief Notify that an icon has changed + * @param win Window ID of the icon + */ + void notifyIconChanged(xcb_window_t win); + + /** + * @return List of all registered tray icon window IDs + */ + TrayList trayIcons() const; + + /** + * @return Pointer to TrayManagerProxy for the given window, or nullptr + */ + TrayManagerProxy *iconProxy(xcb_window_t win) const; + +public Q_SLOTS: + // DBus methods + bool Manage(); + QString GetName(uint32_t win); + void EnableNotification(uint32_t win, bool enabled); + +Q_SIGNALS: + // DBus signals + void Added(uint32_t id); + void Removed(uint32_t id); + void Changed(uint32_t id); + void Inited(); + +private: + TrayManager1Adaptor * m_adaptor; + QHash m_icons; +}; diff --git a/traymanagerproxy.cpp b/traymanagerproxy.cpp new file mode 100644 index 0000000..28705d4 --- /dev/null +++ b/traymanagerproxy.cpp @@ -0,0 +1,302 @@ +/* + Xembed Tray Manager Proxy - holds one embedded window + SPDX-FileCopyrightText: 2015 David Edmundson + SPDX-FileCopyrightText: 2019 Konrad Materka + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "traymanagerproxy.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "xcbutils.h" +#include "xtestsender.h" +#include "c_ptr.h" +#include +#include + + +#ifdef Status +typedef Status XStatus; +#undef Status +typedef XStatus Status; +#endif + +static uint16_t s_embedSize = 32; +static unsigned int XEMBED_VERSION = 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); +} + +TrayManagerProxy::TrayManagerProxy(xcb_window_t wid, QObject *parent) + : QObject(parent) + , m_x11Interface(qGuiApp->nativeInterface()) + , m_windowId(wid) + , m_injectMode(Direct) +{ + m_name = getWindowName(); + + 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; + values[1] = true; + values[2] = XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT; + xcb_create_window(c, + XCB_COPY_FROM_PARENT, + m_containerWid, + screen->root, + 0, 0, + s_embedSize, s_embedSize, + 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, + screen->root_visual, + mask, + values); + + setActiveForInput(false); + + NETWinInfo wm(c, m_containerWid, screen->root, NET::Properties(), NET::Properties2()); + wm.setOpacity(0); + + 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); + + 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 + 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 + auto waCookie = xcb_get_window_attributes(c, wid); + UniqueCPointer windowAttributes(xcb_get_window_attributes_reply(c, waCookie, nullptr)); + if (!checkWindowOrDescendantWantButtonEvents(wid)) { + m_injectMode = XTest; + } + + // First update after delay + QTimer::singleShot(500, this, &TrayManagerProxy::update); +} + +TrayManagerProxy::~TrayManagerProxy() +{ + auto c = m_x11Interface->connection(); + xcb_destroy_window(c, m_containerWid); + xcb_flush(c); +} + +QString TrayManagerProxy::getWindowName() const +{ + auto connection = m_x11Interface->connection(); + KWindowInfo info(m_windowId, NET::WMName | NET::WMIconName); + return info.name(); +} + +void TrayManagerProxy::update() +{ + QImage newImage = getImageNonComposite(); + + if (!newImage.isNull() && newImage != m_lastImage) { + m_lastImage = newImage; + // Icon image changed, could trigger update on TrayManager1 + // This would be handled at a higher level + } + + QTimer::singleShot(100, this, &TrayManagerProxy::update); +} + +void TrayManagerProxy::resizeWindow(const uint16_t width, const uint16_t height) const +{ + auto c = m_x11Interface->connection(); + const uint32_t values[] = {width, height}; + xcb_configure_window(c, m_windowId, XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, values); + xcb_flush(c); +} + +QSize TrayManagerProxy::calculateClientWindowSize() const +{ + auto c = m_x11Interface->connection(); + auto geoCookie = xcb_get_geometry(c, m_windowId); + UniqueCPointer geo(xcb_get_geometry_reply(c, geoCookie, nullptr)); + if (geo) { + return QSize(geo->width, geo->height); + } + return QSize(s_embedSize, s_embedSize); +} + +void TrayManagerProxy::sendClick(uint8_t mouseButton, int x, int y) +{ + if (m_injectMode == XTest) { + // Use XTest helper functions defined in xtestsender.h + sendXTestPressed(m_x11Interface->display(), mouseButton); + sendXTestReleased(m_x11Interface->display(), mouseButton); + return; + } + + auto c = m_x11Interface->connection(); + xcb_button_press_event_t pressEvent{}; + memset(&pressEvent, 0, sizeof(pressEvent)); + pressEvent.response_type = XCB_BUTTON_PRESS; + pressEvent.event = m_windowId; + pressEvent.time = XCB_CURRENT_TIME; + pressEvent.same_screen = 1; + pressEvent.root = DefaultRootWindow(m_x11Interface->display()); + pressEvent.root_x = x; + pressEvent.root_y = y; + pressEvent.event_x = static_cast(x); + pressEvent.event_y = static_cast(y); + pressEvent.child = 0; + pressEvent.state = 0; + pressEvent.detail = mouseButton; + + xcb_send_event(c, false, m_windowId, XCB_EVENT_MASK_BUTTON_PRESS, reinterpret_cast(&pressEvent)); + + xcb_button_release_event_t releaseEvent{}; + memset(&releaseEvent, 0, sizeof(releaseEvent)); + releaseEvent.response_type = XCB_BUTTON_RELEASE; + releaseEvent.event = m_windowId; + releaseEvent.time = XCB_CURRENT_TIME; + releaseEvent.same_screen = 1; + releaseEvent.root = DefaultRootWindow(m_x11Interface->display()); + releaseEvent.root_x = x; + releaseEvent.root_y = y; + releaseEvent.event_x = static_cast(x); + releaseEvent.event_y = static_cast(y); + releaseEvent.child = 0; + releaseEvent.state = 0; + releaseEvent.detail = mouseButton; + + xcb_send_event(c, false, m_windowId, XCB_EVENT_MASK_BUTTON_RELEASE, reinterpret_cast(&releaseEvent)); + xcb_flush(c); +} + +QImage TrayManagerProxy::getImageNonComposite() const +{ + auto c = m_x11Interface->connection(); + + auto geoCookie = xcb_get_geometry(c, m_windowId); + UniqueCPointer geo(xcb_get_geometry_reply(c, geoCookie, nullptr)); + if (!geo) { + return QImage(); + } + // Use xcb_image_get directly on the drawable. Use UINT32_MAX for plane mask. + xcb_image_t *xcbimg = xcb_image_get(c, m_windowId, 0, 0, geo->width, geo->height, UINT32_MAX, XCB_IMAGE_FORMAT_Z_PIXMAP); + if (!xcbimg) { + return QImage(); + } + + return convertFromNative(xcbimg); +} + +bool TrayManagerProxy::isTransparentImage(const QImage &image) const +{ + if (image.format() != QImage::Format_ARGB32) { + return false; + } + + const QRgb *data = reinterpret_cast(image.bits()); + for (int i = 0; i < image.width() * image.height(); ++i) { + if (qAlpha(data[i]) != 0) { + return false; + } + } + return true; +} + +QImage TrayManagerProxy::convertFromNative(xcb_image_t *xcbImage) const +{ + if (!xcbImage) { + return QImage(); + } + + QImage qimage(xcbImage->width, xcbImage->height, QImage::Format_ARGB32); + memcpy(qimage.bits(), xcbImage->data, qimage.sizeInBytes()); + + return qimage; +} + +QPoint TrayManagerProxy::calculateClickPoint() const +{ + return QPoint(s_embedSize / 2, s_embedSize / 2); +} + +void TrayManagerProxy::setActiveForInput(bool active) const +{ + auto c = m_x11Interface->connection(); + auto screen = xcb_setup_roots_iterator(xcb_get_setup(c)).data; + + xcb_rectangle_t rect = {0, 0, 0, 0}; + if (active) { + rect.width = s_embedSize; + rect.height = s_embedSize; + } + + xcb_shape_rectangles(c, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, XCB_CLIP_ORDERING_UNSORTED, m_containerWid, 0, 0, 1, &rect); + xcb_flush(c); +} diff --git a/traymanagerproxy.h b/traymanagerproxy.h new file mode 100644 index 0000000..8a6d8a7 --- /dev/null +++ b/traymanagerproxy.h @@ -0,0 +1,62 @@ +/* + Xembed Tray Manager Proxy - holds one embedded window + 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 + +class TrayManagerProxy : public QObject +{ + Q_OBJECT + +public: + explicit TrayManagerProxy(xcb_window_t wid, QObject *parent = nullptr); + ~TrayManagerProxy() override; + + void update(); + void resizeWindow(const uint16_t width, const uint16_t height) const; + + /** + * @return the window id of this item + */ + uint32_t windowId() const { return m_windowId; } + + /** + * @return the name/title of this item + */ + QString name() const { return m_name; } + +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; + QString getWindowName() const; + + QNativeInterface::QX11Application *m_x11Interface = nullptr; + xcb_window_t m_windowId; + xcb_window_t m_containerWid; + uint32_t m_damageId = 0; + InjectMode m_injectMode; + QString m_name; + QImage m_lastImage; +}; diff --git a/xcbutils.h b/xcbutils.h index 0b788b1..70f48c1 100644 --- a/xcbutils.h +++ b/xcbutils.h @@ -18,7 +18,7 @@ #include #include -#include "../c_ptr.h" +#include "c_ptr.h" #include /** XEMBED messages */