Git commit 2fc947526df5d7f62db79e7f224cc13b1aa15493 by David Edmundson. Committed on 03/11/2015 at 13:08. Pushed by davidedmundson into branch 'master'. Proxy Xembed icons to SNI The goal of this project is to make xembed system trays available in Plasma. This is to allow legacy apps (xchat, pidgin, tuxguitar) etc. system trays[1] available in Plasma which only supports StatusNotifierItem [2]. Ideally we also want this to work in an xwayland session, making X system tray icons available even when plasmashell only has a wayland connection. REVIEW: 125655 M +3 -0 CMakeLists.txt A +67 -0 xembed-sni-proxy/CMakeLists.txt A +30 -0 xembed-sni-proxy/Readme.md A +182 -0 xembed-sni-proxy/fdoselectionmanager.cpp [License: LGPL = (v2.1+)] A +62 -0 xembed-sni-proxy/fdoselectionmanager.h [License: LGPL (v= 2.1+)] A +72 -0 xembed-sni-proxy/main.cpp [License: LGPL (v2.1+)] A +96 -0 xembed-sni-proxy/org.kde.StatusNotifierItem.xml A +42 -0 xembed-sni-proxy/org.kde.StatusNotifierWatcher.xml A +148 -0 xembed-sni-proxy/snidbus.cpp [License: GPL (v2/3)] A +65 -0 xembed-sni-proxy/snidbus.h [License: GPL (v2/3)] A +376 -0 xembed-sni-proxy/sniproxy.cpp [License: LGPL (v2.1+)] A +152 -0 xembed-sni-proxy/sniproxy.h [License: LGPL (v2.1+)] A +126 -0 xembed-sni-proxy/xcbutils.h [License: GPL (v2)] A +7 -0 xembed-sni-proxy/xembedsniproxy.desktop http://commits.kde.org/plasma-workspace/2fc947526df5d7f62db79e7f224cc13b1aa= 15493 diff --git a/CMakeLists.txt b/CMakeLists.txt index 86d1531..5aacfb8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,6 +43,7 @@ include(WriteBasicConfigVersionFile) include(CheckIncludeFiles) include(FeatureSummary) include(ECMOptionalAddSubdirectory) +include(ECMQtDeclareLoggingCategory) = find_package(KF5Activities ${KF5_MIN_VERSION}) set_package_properties(KF5Activities PROPERTIES DESCRIPTION "management of= Plasma activities" @@ -153,5 +154,7 @@ add_subdirectory(phonon) add_subdirectory(solidautoeject) add_subdirectory(drkonqi) = +ecm_optional_add_subdirectory(xembed-sni-proxy) + add_subdirectory(soliduiserver) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_= PACKAGES) diff --git a/xembed-sni-proxy/CMakeLists.txt b/xembed-sni-proxy/CMakeLists.= txt new file mode 100644 index 0000000..1686340 --- /dev/null +++ b/xembed-sni-proxy/CMakeLists.txt @@ -0,0 +1,67 @@ +project(xembedsniproxy) + +add_definitions(-DQT_NO_CAST_TO_ASCII +-DQT_NO_CAST_FROM_ASCII +-DQT_NO_URL_CAST_FROM_STRING +-DQT_NO_CAST_FROM_BYTEARRAY) + +find_package(XCB + REQUIRED COMPONENTS + XCB + XFIXES + DAMAGE + COMPOSITE + RANDR + SHM + UTIL + IMAGE +) + +set(XCB_LIBS + XCB::XCB + XCB::XFIXES + XCB::DAMAGE + XCB::COMPOSITE + XCB::RANDR + XCB::SHM + XCB::UTIL + XCB::IMAGE +) + + + +set(XEMBED_SNI_PROXY_SOURCES + main.cpp + fdoselectionmanager.cpp + snidbus.cpp + sniproxy.cpp +) + +qt5_add_dbus_adaptor(XEMBED_SNI_PROXY_SOURCES org.kde.StatusNotifierItem.x= ml + sniproxy.h SNIProxy) + +set(statusnotifierwatcher_xml org.kde.StatusNotifierWatcher.xml) +qt5_add_dbus_interface(XEMBED_SNI_PROXY_SOURCES ${statusnotifierwatcher_xm= l} statusnotifierwatcher_interface) + +add_executable(xembedsniproxy ${XEMBED_SNI_PROXY_SOURCES}) + + + +set_package_properties(XCB PROPERTIES TYPE REQUIRED) + +ecm_qt_declare_logging_category(xembedsniproxy HEADER debug.h + IDENTIFIER SNIPROXY + CATEGORY_NAME kde.xembedsni= proxy + DEFAULT_SEVERITY Info) + +target_link_libraries(xembedsniproxy + Qt5::Core + Qt5::X11Extras + Qt5::DBus + KF5::WindowSystem + ${XCB_LIBS} +) + +install(TARGETS xembedsniproxy ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) +install(FILES xembedsniproxy.desktop DESTINATION ${KDE_INSTALL_AUTOSTARTDI= R}) + diff --git a/xembed-sni-proxy/Readme.md b/xembed-sni-proxy/Readme.md new file mode 100644 index 0000000..4201349 --- /dev/null +++ b/xembed-sni-proxy/Readme.md @@ -0,0 +1,30 @@ +##XEmbed SNI Proxy + +The goal of this project is to make xembed system trays available in Plasm= a. + +This is to allow legacy apps (xchat, pidgin, tuxguitar) etc. system trays[= 1] available in Plasma which only supports StatusNotifierItem [2]. + +Ideally we also want this to work in an xwayland session, making X system = tray icons available even when plasmashell only has a wayland connection. + +This project should be portable onto all other DEs that speak SNI. + +##How it works (in theory) + +* We register a window as a system tray container +* We render embeded windows composited offscreen +* We render contents into an image and send this over DBus via the SNI pro= tocol +* XDamage events trigger a repaint +* Activate and context menu events are replyed via X send event into the e= mbedded container as left and right clicks + +There are a few extra hacks in the real code to deal with some toolkits be= ing awkward. + +##Build instructions + + cmake . + make + sudo make install + +After building, run `xembedsniproxy`. + +[1] http://standards.freedesktop.org/systemtray-spec/systemtray-spec-lates= t.html +[2] http://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/ diff --git a/xembed-sni-proxy/fdoselectionmanager.cpp b/xembed-sni-proxy/fd= oselectionmanager.cpp new file mode 100644 index 0000000..02d2660 --- /dev/null +++ b/xembed-sni-proxy/fdoselectionmanager.cpp @@ -0,0 +1,182 @@ +/* + * Registers as a embed container + * Copyright (C) 2015 David Edmundson + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-13= 01 USA + * + */ +#include "fdoselectionmanager.h" + +#include "debug.h" + +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include "xcbutils.h" +#include "sniproxy.h" + +#define SYSTEM_TRAY_REQUEST_DOCK 0 +#define SYSTEM_TRAY_BEGIN_MESSAGE 1 +#define SYSTEM_TRAY_CANCEL_MESSAGE 2 + +FdoSelectionManager::FdoSelectionManager(): + QObject(), + m_selectionOwner(new KSelectionOwner(Xcb::atoms->selectionAtom, -1, th= is)) +{ + qCDebug(SNIPROXY) << "starting"; + + //load damage extension + xcb_connection_t *c =3D QX11Info::connection(); + xcb_prefetch_extension_data(c, &xcb_damage_id); + const auto *reply =3D xcb_get_extension_data(c, &xcb_damage_id); + if (reply->present) { + m_damageEventBase =3D reply->first_event; + xcb_damage_query_version_unchecked(c, XCB_DAMAGE_MAJOR_VERSION, XC= B_DAMAGE_MINOR_VERSION); + } else { + //no XDamage means + qCCritical(SNIPROXY) << "could not load damage extension. Quitting= "; + qApp->exit(-1); + } + + qApp->installNativeEventFilter(this); + + m_selectionOwner->claim(false); + connect(m_selectionOwner, &KSelectionOwner::claimedOwnership, this, &F= doSelectionManager::onClaimedOwnership); + connect(m_selectionOwner, &KSelectionOwner::failedToClaimOwnership, th= is, &FdoSelectionManager::onFailedToClaimOwnership); + connect(m_selectionOwner, &KSelectionOwner::lostOwnership, this, &FdoS= electionManager::onLostOwnership); +} + +FdoSelectionManager::~FdoSelectionManager() +{ + qCDebug(SNIPROXY) << "closing"; + m_selectionOwner->release(); +} + +void FdoSelectionManager::addDamageWatch(xcb_window_t client) +{ + qCDebug(SNIPROXY) << "adding damage watch for " << client; + + xcb_connection_t *c =3D QX11Info::connection(); + const auto attribsCookie =3D xcb_get_window_attributes_unchecked(c, cl= ient); + + const auto damageId =3D xcb_generate_id(c); + m_damageWatches[client] =3D damageId; + xcb_damage_create(c, damageId, client, XCB_DAMAGE_REPORT_LEVEL_NON_EMP= TY); + + QScopedPointer attr(xcb_get_window_attributes_reply(c, attribsCookie, Q_NULLPTR)); + uint32_t events =3D XCB_EVENT_MASK_STRUCTURE_NOTIFY; + if (!attr.isNull()) { + events =3D events | attr->your_event_mask; + } + // the event mask will not be removed again. We cannot track whether a= nother component also needs STRUCTURE_NOTIFY (e.g. KWindowSystem). + // if we would remove the event mask again, other areas will break. + xcb_change_window_attributes(c, client, XCB_CW_EVENT_MASK, &events); + +} + +bool FdoSelectionManager::nativeEventFilter(const QByteArray& eventType, v= oid* message, long int* result) +{ + Q_UNUSED(result); + + if (eventType !=3D "xcb_generic_event_t") { + return false; + } + + xcb_generic_event_t* ev =3D static_cast(message= ); + + const auto responseType =3D XCB_EVENT_RESPONSE_TYPE(ev); + if (responseType =3D=3D XCB_CLIENT_MESSAGE) { + const auto ce =3D reinterpret_cast(e= v); + if (ce->type =3D=3D Xcb::atoms->opcodeAtom) { + switch (ce->data.data32[1]) { + case SYSTEM_TRAY_REQUEST_DOCK: + dock(ce->data.data32[2]); + return true; + } + } + } else if (responseType =3D=3D XCB_UNMAP_NOTIFY) { + const auto unmappedWId =3D reinterpret_cast(ev)->window; + if (m_proxies[unmappedWId]) { + undock(unmappedWId); + } + } else if (responseType =3D=3D m_damageEventBase + XCB_DAMAGE_NOTIFY) { + const auto damagedWId =3D reinterpret_cast(ev)->drawable; + const auto sniProx =3D m_proxies[damagedWId]; + + Q_ASSERT(sniProx); + + if(sniProx) { + sniProx->update(); + xcb_damage_subtract(QX11Info::connection(), m_damageWatches[da= magedWId], XCB_NONE, XCB_NONE); + } + } + + return false; +} + +void FdoSelectionManager::dock(xcb_window_t winId) +{ + qCDebug(SNIPROXY) << "trying to dock window " << winId; + + if (m_proxies.contains(winId)) { + return; + } + + addDamageWatch(winId); + m_proxies[winId] =3D new SNIProxy(winId, this); +} + +void FdoSelectionManager::undock(xcb_window_t winId) +{ + qCDebug(SNIPROXY) << "trying to undock window " << winId; + + if (!m_proxies.contains(winId)) { + return; + } + m_proxies[winId]->deleteLater(); + m_proxies.remove(winId); +} + +void FdoSelectionManager::onClaimedOwnership() +{ + qCDebug(SNIPROXY) << "Manager selection claimed"; +} + +void FdoSelectionManager::onFailedToClaimOwnership() +{ + qCWarning(SNIPROXY) << "failed to claim ownership of Systray Manager"; + qApp->exit(-1); +} + +void FdoSelectionManager::onLostOwnership() +{ + qCWarning(SNIPROXY) << "lost ownership of Systray Manager"; + qApp->exit(-1); +} + + diff --git a/xembed-sni-proxy/fdoselectionmanager.h b/xembed-sni-proxy/fdos= electionmanager.h new file mode 100644 index 0000000..084152e --- /dev/null +++ b/xembed-sni-proxy/fdoselectionmanager.h @@ -0,0 +1,62 @@ +/* + * Registers as a embed container + * Copyright (C) 2015 David Edmundson + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-13= 01 USA + * + */ + +#ifndef FDOSELECTIONMANAGER_H +#define FDOSELECTIONMANAGER_H + +#include +#include +#include + +#include + +class KSelectionOwner; +class SNIProxy; + +class FdoSelectionManager : public QObject, public QAbstractNativeEventFil= ter +{ + Q_OBJECT + +public: + FdoSelectionManager(); + ~FdoSelectionManager(); + +protected: + bool nativeEventFilter(const QByteArray & eventType, void * message, l= ong * result) Q_DECL_OVERRIDE; + +private Q_SLOTS: + void onClaimedOwnership(); + void onFailedToClaimOwnership(); + void onLostOwnership(); + +private: + void addDamageWatch(xcb_window_t client); + void dock(xcb_window_t embed_win); + void undock(xcb_window_t client); + + uint8_t m_damageEventBase; + + QHash m_damageWatches; + QHash m_proxies; + KSelectionOwner *m_selectionOwner; +}; + + +#endif diff --git a/xembed-sni-proxy/main.cpp b/xembed-sni-proxy/main.cpp new file mode 100644 index 0000000..291fb10 --- /dev/null +++ b/xembed-sni-proxy/main.cpp @@ -0,0 +1,72 @@ +/* + * Main + * Copyright (C) 2015 David Edmundson + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-13= 01 USA + * + */ + +#include +#include + +#include "fdoselectionmanager.h" + +#include "debug.h" +#include "xcbutils.h" +#include "snidbus.h" + +#include + +namespace Xcb { + Xcb::Atoms* atoms; +} + +Q_LOGGING_CATEGORY(SNIPROXY, "kde.xembedsniproxy", QtDebugMsg) //change to= QtInfoMsg near release + +int main(int argc, char ** argv) +{ + //the whole point of this is to interact with X, if we are in any othe= r session, force trying to connect to X + //if the QPA can't load xcb, this app is useless anyway. + qputenv("QT_QPA_PLATFORM", "xcb"); + + QGuiApplication app(argc, argv); + + if (app.platformName() !=3D QLatin1String("xcb")) { + qFatal("xembed-sni-proxy is only useful XCB. Aborting"); + } + + auto disableSessionManagement =3D [](QSessionManager &sm) { + sm.setRestartHint(QSessionManager::RestartNever); + }; + QObject::connect(&app, &QGuiApplication::commitDataRequest, disableSes= sionManagement); + QObject::connect(&app, &QGuiApplication::saveStateRequest, disableSess= ionManagement); + + + app.setDesktopSettingsAware(false); + app.setQuitOnLastWindowClosed(false); + + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + Xcb::atoms =3D new Xcb::Atoms(); + + FdoSelectionManager manager; + + auto rc =3D app.exec(); + + delete Xcb::atoms; + return rc; +} \ No newline at end of file diff --git a/xembed-sni-proxy/org.kde.StatusNotifierItem.xml b/xembed-sni-p= roxy/org.kde.StatusNotifierItem.xml new file mode 100644 index 0000000..d378c74 --- /dev/null +++ b/xembed-sni-proxy/org.kde.StatusNotifierItem.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/xembed-sni-proxy/org.kde.StatusNotifierWatcher.xml b/xembed-sn= i-proxy/org.kde.StatusNotifierWatcher.xml new file mode 100644 index 0000000..2eb1a7a --- /dev/null +++ b/xembed-sni-proxy/org.kde.StatusNotifierWatcher.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/xembed-sni-proxy/snidbus.cpp b/xembed-sni-proxy/snidbus.cpp new file mode 100644 index 0000000..4ebb7a0 --- /dev/null +++ b/xembed-sni-proxy/snidbus.cpp @@ -0,0 +1,148 @@ +/* + * SNI DBus Serialisers + * Copyright 2015 David Edmundson + * Copyright 2009 by Marco Martin + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "snidbus.h" + +#include +#include + +//mostly copied from KStatusNotiferItemDbus.cpps from knotification + +KDbusImageStruct::KDbusImageStruct() +{ +} + +KDbusImageStruct::KDbusImageStruct(const QImage &image) +{ + width =3D image.size().width(); + height =3D image.size().height(); + if (image.format() =3D=3D QImage::Format_ARGB32) { + data =3D QByteArray((char *)image.bits(), image.byteCount()); + } else { + QImage image32 =3D image.convertToFormat(QImage::Format_ARGB32); + data =3D QByteArray((char *)image32.bits(), image32.byteCount()); + } + + //swap to network byte order if we are little endian + if (QSysInfo::ByteOrder =3D=3D QSysInfo::LittleEndian) { + quint32 *uintBuf =3D (quint32 *) data.data(); + for (uint i =3D 0; i < data.size() / sizeof(quint32); ++i) { + *uintBuf =3D qToBigEndian(*uintBuf); + ++uintBuf; + } + } +} + +// Marshall the ImageStruct data into a D-BUS argument +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageS= truct &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, KDbusImageS= truct &icon) +{ + qint32 width; + qint32 height; + QByteArray data; + + argument.beginStructure(); + argument >> width; + argument >> height; + argument >> data; + argument.endStructure(); + + icon.width =3D width; + icon.height =3D height; + icon.data =3D data; + + return argument; +} + +// Marshall the ImageVector data into a D-BUS argument +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageV= ector &iconVector) +{ + argument.beginArray(qMetaTypeId()); + for (int i =3D 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, KDbusImageV= ector &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 KDbusToolTi= pStruct &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, KDbusToolTi= pStruct &toolTip) +{ + QString icon; + KDbusImageVector image; + QString title; + QString subTitle; + + argument.beginStructure(); + argument >> icon; + argument >> image; + argument >> title; + argument >> subTitle; + argument.endStructure(); + + toolTip.icon =3D icon; + toolTip.image =3D image; + toolTip.title =3D title; + toolTip.subTitle =3D subTitle; + + return argument; +} \ No newline at end of file diff --git a/xembed-sni-proxy/snidbus.h b/xembed-sni-proxy/snidbus.h new file mode 100644 index 0000000..e8cda9c --- /dev/null +++ b/xembed-sni-proxy/snidbus.h @@ -0,0 +1,65 @@ +/* + * SNI Dbus serialisers + * Copyright 2015 David Edmundson + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef SNIDBUS_H +#define SNIDBUS_H + +#include +#include +#include +#include +#include + +//Custom message type for DBus +struct KDbusImageStruct { + KDbusImageStruct(); + KDbusImageStruct(const QImage &image); + int width; + int height; + QByteArray data; +}; + +typedef QVector KDbusImageVector; + +struct KDbusToolTipStruct { + QString icon; + KDbusImageVector image; + QString title; + QString subTitle; +}; + +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageS= truct &icon); +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusImageS= truct &icon); + +Q_DECLARE_METATYPE(KDbusImageStruct) + +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusImageV= ector &iconVector); +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusImageV= ector &iconVector); + +Q_DECLARE_METATYPE(KDbusImageVector) + +const QDBusArgument &operator<<(QDBusArgument &argument, const KDbusToolTi= pStruct &toolTip); +const QDBusArgument &operator>>(const QDBusArgument &argument, KDbusToolTi= pStruct &toolTip); + +Q_DECLARE_METATYPE(KDbusToolTipStruct) + +#endif // SNIDBUS_H diff --git a/xembed-sni-proxy/sniproxy.cpp b/xembed-sni-proxy/sniproxy.cpp new file mode 100644 index 0000000..b5cf3c3 --- /dev/null +++ b/xembed-sni-proxy/sniproxy.cpp @@ -0,0 +1,376 @@ +/* + * Holds one embedded window, registers as DBus entry + * Copyright (C) 2015 David Edmundson + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-13= 01 USA + * + */ + +#include "sniproxy.h" + +#include +#include +#include +#include + +#include "xcbutils.h" +#include "debug.h" + +#include +#include +#include +#include + +#include + +#include +#include + +#include "statusnotifieritemadaptor.h" +#include "statusnotifierwatcher_interface.h" + +#define SNI_WATCHER_SERVICE_NAME "org.kde.StatusNotifierWatcher" +#define SNI_WATCHER_PATH "/StatusNotifierWatcher" + +static uint16_t s_embedSize =3D 48; //max size of window to embed. We no l= onger resize the embedded window as Chromium acts stupidly. + +int SNIProxy::s_serviceCount =3D 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 =3D XCB_CLIENT_MESSAGE; + ev.window =3D towin; + ev.format =3D 32; + ev.data.data32[0] =3D XCB_CURRENT_TIME; + ev.data.data32[1] =3D message; + ev.data.data32[2] =3D d1; + ev.data.data32[3] =3D d2; + ev.data.data32[4] =3D d3; + ev.type =3D Xcb::atoms->xembedAtom; + xcb_send_event(QX11Info::connection(), false, towin, XCB_EVENT_MASK_NO= _EVENT, (char *) &ev); +} + +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 pat= h, 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, QStr= ingLiteral("XembedSniProxy%1").arg(s_serviceCount++))), + m_windowId(wid) +{ + //create new SNI + new StatusNotifierItemAdaptor(this); + m_dbus.registerObject(QStringLiteral("/StatusNotifierItem"), this); + + auto statusNotifierWatcher =3D new org::kde::StatusNotifierWatcher(QSt= ringLiteral(SNI_WATCHER_SERVICE_NAME), QStringLiteral(SNI_WATCHER_PATH), QD= BusConnection::sessionBus(), this); + auto reply =3D statusNotifierWatcher->RegisterStatusNotifierItem(m_dbu= s.baseService()); + reply.waitForFinished(); + if (reply.isError()) { + qCWarning(SNIPROXY) << "could not register SNI:" << reply.error().= message(); + } + + auto c =3D QX11Info::connection(); + + auto cookie =3D xcb_get_geometry(c, m_windowId); + QScopedPointer clientGeom(xcb_get_geometry_r= eply(c, cookie, Q_NULLPTR)); + + //create a container window + auto screen =3D xcb_setup_roots_iterator (xcb_get_setup (c)).data; + m_containerWid =3D xcb_generate_id(c); + uint32_t values[2]; + auto mask =3D XCB_CW_BACK_PIXEL | XCB_CW_OVERRIDE_REDIRECT; + values[0] =3D screen->black_pixel; //draw a solid background so the em= beded icon doesn't get garbage in it + values[1] =3D true; //bypass wM + xcb_create_window (c, /* connection */ + XCB_COPY_FROM_PARENT, /* depth */ + m_containerWid, /* window Id */ + screen->root, /* parent window */ + -500, 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 work= ing as GTK will check sendEvent locations to see if our window is in the ri= ght place. So even though our contents are drawn via compositing we still p= ut this window in the right place + + We can't composite it away anything parented owned by the root win= dow (apparently) + Stack Under works in the non composited case, but it doesn't seem = to work in kwin's composited case (probably need set relevant NETWM hint) + + As a last resort set opacity to 0 just to make sure this container= never appears + */ + +#ifndef VISUAL_DEBUG + const uint32_t stackBelowData[] =3D {XCB_STACK_MODE_BELOW}; + xcb_configure_window(c, m_containerWid, XCB_CONFIG_WINDOW_STACK_MODE, = stackBelowData); + + NETWinInfo wm(c, m_containerWid, screen->root, 0); + 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 reparente= d 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, m_containerWid, 0, 0); + + //move window we're embedding + const uint32_t windowMoveConfigVals[2] =3D { 0, 0 }; + + xcb_configure_window(c, wid, + XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y, + windowMoveConfigVals); + + + //if the window is a clearly stupid size resize to be something sensib= le + //this is needed as chormium and such when resized just fill the icon = with transparent space and only draw in the middle + //however spotify does need this as by default the window size is 900p= x wide. + //use an artbitrary heuristic to make sure icons are always sensible + if (clientGeom->width < 12 || clientGeom->width > s_embedSize || + clientGeom->height < 12 || clientGeom->height > s_embedSize) + { + const uint32_t windowMoveConfigVals[2] =3D { s_embedSize, s_embedS= ize }; + xcb_configure_window(c, wid, + XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDO= W_HEIGHT, + windowMoveConfigVals); + } + + //show the embedded window otherwise nothing happens + xcb_map_window(c, wid); + + xcb_clear_area(c, 0, wid, 0, 0, qMin(clientGeom->width, s_embedSize), = qMin(clientGeom->height, s_embedSize)); + + xcb_flush(c); + + //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() +{ + QDBusConnection::disconnectFromBus(m_dbus.name()); +} + +void SNIProxy::update() +{ + const QImage image =3D getImageNonComposite(); + + bool isTransparentImage =3D true; + + int sum =3D 0; + for (int x =3D 0; x < image.width(); ++x) { + for (int y =3D 0; y < image.height(); ++y) { + sum +=3D qAlpha(image.pixel(x, y)); + if (sum >=3D 255) { + // There is enough amount of opaque pixels. + isTransparentImage =3D false; + break; + } + } + } + + // 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) { + m_pixmap =3D QPixmap::fromImage(image); + emit NewIcon(); + } + else { + qCDebug(SNIPROXY) << "Skip transparent xembed icon"; + } +} + +void sni_cleanup_xcb_image(void *data) { + xcb_image_destroy(static_cast(data)); +} + +QImage SNIProxy::getImageNonComposite() +{ + auto c =3D QX11Info::connection(); + auto cookie =3D xcb_get_geometry(c, m_windowId); + QScopedPointer geom(xcb_get_geometry_reply(c= , cookie, Q_NULLPTR)); + + xcb_image_t *image =3D xcb_image_get(c, m_windowId, 0, 0, geom->width,= geom->height, 0xFFFFFF, XCB_IMAGE_FORMAT_Z_PIXMAP); + + QImage qimage(image->data, image->width, image->height, image->stride,= QImage::Format_ARGB32, sni_cleanup_xcb_image, image); + + return qimage; +} + +//____________properties__________ + +QString SNIProxy::Category() const +{ + return QStringLiteral("ApplicationStatus"); +} + +QString SNIProxy::Id() const +{ + return QString::number(m_windowId); +} + +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::ContextMenu(int x, int y) +{ + sendClick(XCB_BUTTON_INDEX_3, x, y); +} + +void SNIProxy::SecondaryActivate(int x, int y) +{ + sendClick(XCB_BUTTON_INDEX_2, x, y); +} + +void SNIProxy::Scroll(int delta, const QString& orientation) +{ + if (orientation =3D=3D 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 ma= tches 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) << "Sending click " << mouseButton << "to" << x << y; + + auto c =3D QX11Info::connection(); + + //set our window so the middle is where the mouse is + const uint32_t stackAboveData[] =3D {XCB_STACK_MODE_ABOVE}; + xcb_configure_window(c, m_containerWid, XCB_CONFIG_WINDOW_STACK_MODE, = stackAboveData); + + const uint32_t config_vals[4] =3D {x, y, s_embedSize, s_embedSize }; + xcb_configure_window(c, m_containerWid, + XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | X= CB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, + config_vals); + //mouse down + { + xcb_button_press_event_t* event =3D new xcb_button_press_event_t; + memset(event, 0x00, sizeof(xcb_button_press_event_t)); + event->response_type =3D XCB_BUTTON_PRESS; + event->event =3D m_windowId; + event->time =3D QX11Info::getTimestamp(); + event->same_screen =3D 1; + event->root =3D QX11Info::appRootWindow(); + event->root_x =3D x; + event->root_y =3D y; + event->event_x =3D 0; + event->event_y =3D 0; + event->child =3D 0; + event->state =3D 0; + event->detail =3D mouseButton; + + xcb_send_event(c, false, m_windowId, XCB_EVENT_MASK_BUTTON_PRESS, = (char *) event); + free(event); + } + + //mouse up + { + xcb_button_release_event_t* event =3D new xcb_button_release_event= _t; + memset(event, 0x00, sizeof(xcb_button_release_event_t)); + event->response_type =3D XCB_BUTTON_RELEASE; + event->event =3D m_windowId; + event->time =3D QX11Info::getTimestamp(); + event->same_screen =3D 1; + event->root =3D QX11Info::appRootWindow(); + event->root_x =3D x; + event->root_y =3D y; + event->event_x =3D 0; + event->event_y =3D 0; + event->child =3D 0; + event->state =3D 0; + event->detail =3D mouseButton; + + xcb_send_event(c, false, m_windowId, XCB_EVENT_MASK_BUTTON_RELEASE= , (char *) event); + free(event); + } +#ifndef VISUAL_DEBUG + const uint32_t stackBelowData[] =3D {XCB_STACK_MODE_BELOW}; + xcb_configure_window(c, m_containerWid, XCB_CONFIG_WINDOW_STACK_MODE, = stackBelowData); +#endif + } + + + + +// diff --git a/xembed-sni-proxy/sniproxy.h b/xembed-sni-proxy/sniproxy.h new file mode 100644 index 0000000..29aa56e --- /dev/null +++ b/xembed-sni-proxy/sniproxy.h @@ -0,0 +1,152 @@ +/* + * Holds one embedded window, registers as DBus entry + * Copyright (C) 2015 David Edmundson + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-13= 01 USA + * + */ + +#ifndef SNI_PROXY_H +#define SNI_PROXY_H + +#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: + SNIProxy(xcb_window_t wid, QObject *parent=3D0); + ~SNIProxy(); + + void update(); + + /** + * @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 s= hould 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: + void sendClick(uint8_t mouseButton, int x, int y); + QImage getImageNonComposite(); + + QDBusConnection m_dbus; + xcb_window_t m_windowId; + xcb_window_t m_containerWid; + static int s_serviceCount; + QPixmap m_pixmap; +}; + +#endif // SNIPROXY_H diff --git a/xembed-sni-proxy/xcbutils.h b/xembed-sni-proxy/xcbutils.h new file mode 100644 index 0000000..5e2b6dc --- /dev/null +++ b/xembed-sni-proxy/xcbutils.h @@ -0,0 +1,126 @@ +/******************************************************************** +Copyright (C) 2012, 2013 Martin Graesslin +Copyright (C) 2015 David Edmudson + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ + +#ifndef KWIN_XCB_UTILS_H +#define KWIN_XCB_UTILS_H + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +/** XEMBED messages */ +#define XEMBED_EMBEDDED_NOTIFY 0 +#define XEMBED_WINDOW_ACTIVATE 1 +#define XEMBED_WINDOW_DEACTIVATE 2 +#define XEMBED_REQUEST_FOCUS 3 +#define XEMBED_FOCUS_IN 4 +#define XEMBED_FOCUS_OUT 5 +#define XEMBED_FOCUS_NEXT 6 +#define XEMBED_FOCUS_PREV 7 + + +namespace Xcb { + +typedef xcb_window_t WindowId; + +template using ScopedCPointer =3D QScopedPointer; + +class Atom +{ +public: + explicit Atom(const QByteArray &name, bool onlyIfExists =3D false, xcb= _connection_t *c =3D QX11Info::connection()) + : m_connection(c) + , m_retrieved(false) + , m_cookie(xcb_intern_atom_unchecked(m_connection, onlyIfExists, n= ame.length(), name.constData())) + , m_atom(XCB_ATOM_NONE) + , m_name(name) + { + } + Atom() =3D delete; + Atom(const Atom &) =3D delete; + + ~Atom() { + if (!m_retrieved && m_cookie.sequence) { + xcb_discard_reply(m_connection, m_cookie.sequence); + } + } + + operator xcb_atom_t() const { + (const_cast(this))->getReply(); + return m_atom; + } + bool isValid() { + getReply(); + return m_atom !=3D XCB_ATOM_NONE; + } + bool isValid() const { + (const_cast(this))->getReply(); + return m_atom !=3D XCB_ATOM_NONE; + } + + inline const QByteArray &name() const { + return m_name; + } + +private: + void getReply() { + if (m_retrieved || !m_cookie.sequence) { + return; + } + ScopedCPointer reply(xcb_intern_atom_repl= y(m_connection, m_cookie, nullptr)); + if (!reply.isNull()) { + m_atom =3D reply->atom; + } + m_retrieved =3D true; + } + xcb_connection_t *m_connection; + bool m_retrieved; + xcb_intern_atom_cookie_t m_cookie; + xcb_atom_t m_atom; + QByteArray m_name; +}; + +class Atoms { +public: + Atoms() : + xembedAtom("_XEMBED"), + selectionAtom(xcb_atom_name_by_screen("_NET_SYSTEM_TRAY", QX11Info= ::appScreen())), + opcodeAtom("_NET_SYSTEM_TRAY_OPCODE"), + messageData("_NET_SYSTEM_TRAY_MESSAGE_DATA") + {} + + Atom xembedAtom; + Atom selectionAtom; + Atom opcodeAtom; + Atom messageData; + +}; + +extern Atoms* atoms; + +} // namespace Xcb + +#endif // KWIN_XCB_UTILS_H diff --git a/xembed-sni-proxy/xembedsniproxy.desktop b/xembed-sni-proxy/xem= bedsniproxy.desktop new file mode 100644 index 0000000..a583391 --- /dev/null +++ b/xembed-sni-proxy/xembedsniproxy.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Exec=3Dxembedsniproxy +Name=3DXembedSniProxy +Type=3DApplication +X-KDE-StartupNotify=3Dfalse +OnlyShowIn=3DKDE; +X-KDE-autostart-phase=3D0