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 */