diff --git a/forest-panel/plugins/plugins.pro b/forest-panel/plugins/plugins.pro index 34c650a..8a45ac1 100644 --- a/forest-panel/plugins/plugins.pro +++ b/forest-panel/plugins/plugins.pro @@ -11,4 +11,5 @@ SUBDIRS += \ windowlist \ quicklaunch \ systray \ + systray-sni \ deskswitch diff --git a/forest-panel/plugins/systray-old/systray.cpp b/forest-panel/plugins/systray-old/systray.cpp deleted file mode 100755 index 4bcb20b..0000000 --- a/forest-panel/plugins/systray-old/systray.cpp +++ /dev/null @@ -1,279 +0,0 @@ -/* BEGIN_COMMON_COPYRIGHT_HEADER - * (c)GPL3+ - * - * Copyright: 2020 Nicholas Yoder - * Authors: - * Nicholas Yoder - * - * Modified from elokab systray plugin - * - * This program or 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 3 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-1301 USA - * - * END_COMMON_COPYRIGHT_HEADER */ - -#include "systray.h" -#include -#include -#include -#include -#include - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -typedef uchar* UCHARP; - -SysTray::SysTray(QWidget* parent): QWidget(parent), m_valid(false), m_trayId(0), m_damageEvent(0), m_damageError(0), m_iconSize(22, 22) -{ -} - -SysTray::~SysTray() -{ - stopTray(); -} - -void SysTray::setupPlug(int, QBoxLayout *layout, QList) -{ - basehlayout = new QHBoxLayout(this); - basehlayout->setSpacing(2); - basehlayout->setMargin(0); - - // Init the selection later just to ensure that no signals are sent until - // after construction is done and the creating object has a chance to connect. - QTimer::singleShot(0, this, SLOT(startTray())); - - layout->addWidget(this); - basehlayout->setDirection(layout->direction()); -} - -//x11 event filter - event passed down from forest reimplementation of virtual x11EventFilter function of QApplication -void SysTray::x11EventFilter(XEvent* event) -{ - TrayIcon* icon; - - switch (event->type) - { - case ClientMessage: - clientMessageEvent(&(event->xclient)); - break; - - case DestroyNotify: - icon = findIcon(event->xany.window); - if (icon) - { - m_iconsList.removeAll(icon); - delete icon; - realign(); - } - break; - - default: - if (event->type == m_damageEvent + XDamageNotify) - { - XDamageNotifyEvent* dmg = reinterpret_cast(event); - icon = findIcon(dmg->drawable); - if (icon) - icon->update(); - } - break; - } -} - -//called from x11EventFilter~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -void SysTray::clientMessageEvent(XClientMessageEvent* e) -{ - unsigned long opcode; - opcode = Window(e->data.l[1]); - Window id; - - switch (opcode) - { - case 0: - id = Window(e->data.l[2]); - if (id) - addIcon(id); - break; - - case 1: - case 2: - qDebug() << "we don't show baloons messages."; - break; - - default: - if (opcode == atom("_NET_SYSTEM_TRAY_MESSAGE_DATA")) - qDebug() << "message from dockapp:" << e->data.b; - break; - } -} - -TrayIcon* SysTray::findIcon(Window id) -{ - foreach(TrayIcon* icon, m_iconsList) - { - if (icon->iconId() == id || icon->windowId() == id) - return icon; - } - return nullptr; -} - -void SysTray::realign() -{ - foreach(TrayIcon* icon, m_iconsList) - basehlayout->removeWidget(icon); - - foreach(TrayIcon* icon, m_iconsList) - basehlayout->addWidget(icon); -} - -VisualID SysTray::getVisual() -{ - VisualID visualId = 0; - Display* dsp = QX11Info::display(); - - XVisualInfo templ; - templ.screen=QX11Info::appScreen(); - templ.depth=32; - templ.c_class=TrueColor; - - int nvi; - XVisualInfo* xvi = XGetVisualInfo(dsp, VisualScreenMask|VisualDepthMask|VisualClassMask, &templ, &nvi); - - if (xvi) - { - int i; - XRenderPictFormat* format; - for (i = 0; i < nvi; i++) - { - format = XRenderFindVisualFormat(dsp, xvi[i].visual); - if (format && - format->type == PictTypeDirect && - format->direct.alphaMask) - { - visualId = xvi[i].visualid; - break; - } - } - XFree(xvi); - } - - return visualId; -} - -//freedesktop systray specification~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -void SysTray::startTray() -{ - Display* dsp = QX11Info::display(); - Window root = QX11Info::appRootWindow(); - - QString s = QString("_NET_SYSTEM_TRAY_S%1").arg(DefaultScreen(dsp)); - Atom _NET_SYSTEM_TRAY_S = atom(s.toAscii()); - - if (XGetSelectionOwner(dsp, _NET_SYSTEM_TRAY_S) != None) - { - qWarning() << "Another systray is running"; - m_valid = false; - return; - } - - // init systray protocol - m_trayId = XCreateSimpleWindow(dsp, root, -1, -1, 1, 1, 0, 0, 0); - - XSetSelectionOwner(dsp, _NET_SYSTEM_TRAY_S, m_trayId, CurrentTime); - if (XGetSelectionOwner(dsp, _NET_SYSTEM_TRAY_S) != m_trayId) - { - qWarning() << "Can't get systray manager"; - stopTray(); - m_valid = false; - return; - } - - int orientation =0; - XChangeProperty(dsp, m_trayId, atom("_NET_SYSTEM_TRAY_ORIENTATION"), XA_CARDINAL, 32, PropModeReplace, UCHARP(&orientation), 1); - - // ** Visual ******************************** - VisualID visualId = getVisual(); - if (visualId) - XChangeProperty(QX11Info::display(), m_trayId, atom("_NET_SYSTEM_TRAY_VISUAL"), XA_VISUALID, 32, PropModeReplace, UCHARP(&visualId), 1); - // ****************************************** - - XClientMessageEvent event; - event.type = ClientMessage; - event.window = root; - event.message_type = atom("MANAGER"); - event.format = 32; - event.data.l[0] = CurrentTime; - event.data.l[1] = long(_NET_SYSTEM_TRAY_S); - event.data.l[2] = long(m_trayId); - event.data.l[3] = 0; - event.data.l[4] = 0; - - XSendEvent(dsp, root, False, StructureNotifyMask, (XEvent*)&event); - XDamageQueryExtension(QX11Info::display(), &m_damageEvent, &m_damageError); - - qDebug() << "Systray started"; - m_valid = true; -} - -void SysTray::stopTray() -{ - qDeleteAll(m_iconsList); - if (m_trayId) - { - XDestroyWindow(QX11Info::display(), m_trayId); - m_trayId = 0; - } - m_valid = false; -} - -void SysTray::addIcon(Window winId) -{ - TrayIcon* icon = new TrayIcon(winId, this); - if (!icon->isValid()) - { - delete icon; - return; - } - - icon->setIconSize(m_iconSize); - m_iconsList.append(icon); - realign(); -} - -Atom SysTray::atom(const char* atomName) -{ - static QHash hash; - - if (hash.contains(atomName)) - return hash.value(atomName); - - Atom atom = XInternAtom(QX11Info::display(), atomName, false); - hash[atomName] = atom; - return atom; -} - -Q_EXPORT_PLUGIN2(forest-panel-system-tray-plugin, SysTray) diff --git a/forest-panel/plugins/systray-old/systray.h b/forest-panel/plugins/systray-old/systray.h deleted file mode 100755 index 3d49799..0000000 --- a/forest-panel/plugins/systray-old/systray.h +++ /dev/null @@ -1,89 +0,0 @@ -/* BEGIN_COMMON_COPYRIGHT_HEADER - * (c)GPL3+ - * - * Copyright: 2020 Nicholas Yoder - * Authors: - * Nicholas Yoder - * - * Modified from elokab systray plugin - * - * This program or 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 3 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-1301 USA - * - * END_COMMON_COPYRIGHT_HEADER */ - -#ifndef SYSTRAY_H -#define SYSTRAY_H - -#include "panelpluginterface.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "trayicon.h" - -#include -#include -#include - -class SysTray: public QWidget, panelpluginterface -{ - Q_OBJECT - Q_PLUGIN_METADATA(IID "forest.panel.systray.plugin") - Q_INTERFACES(panelpluginterface) - -public: - explicit SysTray(QWidget* parent = nullptr); - ~SysTray(); - - //begin plugininterface - void setupPlug(QBoxLayout *layout, QHash info, QList itemlist); - void XcbEventFilter(xcb_generic_event_t* event); - QHash getpluginfo(); - //end plugininterface - -private slots: - void startTray(); - void stopTray(); - void realign(); - TrayIcon* findIcon(Window trayId); - -private: - VisualID getVisual(); - void clientMessageEvent(XClientMessageEvent* e); - void addIcon(Window id); - Atom atom(const char* atomName); - - bool m_valid; - Window m_trayId; - QList m_iconsList; - int m_damageEvent; - int m_damageError; - QSize m_iconSize; - QHBoxLayout *basehlayout; -}; -#endif // SYSTRAY_H diff --git a/forest-panel/plugins/systray-old/systray.pro b/forest-panel/plugins/systray-old/systray.pro deleted file mode 100755 index d7e6238..0000000 --- a/forest-panel/plugins/systray-old/systray.pro +++ /dev/null @@ -1,40 +0,0 @@ -QT += widgets x11extras - -TARGET = systray -TEMPLATE = lib -DEFINES += PLUG_LIBRARY - -DESTDIR = ../../../usr/lib/forest/panel - -INCLUDEPATH += ../../library -INCLUDEPATH += ../../../library - -CONFIG += c++11 \ - plugin - -LIBS += $(SUBLIBS) -lX11 -xrender -lXcomposite -lXdamage - -# The following define makes your compiler emit warnings if you use -# any Qt feature that has been marked deprecated (the exact warnings -# depend on your compiler). Please consult the documentation of the -# deprecated API in order to know how to port your code away from it. -DEFINES += QT_DEPRECATED_WARNINGS - -# You can also make your code fail to compile if it uses deprecated APIs. -# In order to do so, uncomment the following line. -# You can also select to disable deprecated APIs only up to a certain version of Qt. -#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 - -SOURCES += \ - systray.cpp \ - trayicon.cpp - -HEADERS += \ - systray.h \ - trayicon.h - -# Default rules for deployment. -target.path = /usr/lib/forest/panel - -INSTALLS += target - diff --git a/forest-panel/plugins/systray-old/trayicon.cpp b/forest-panel/plugins/systray-old/trayicon.cpp deleted file mode 100755 index 56b8633..0000000 --- a/forest-panel/plugins/systray-old/trayicon.cpp +++ /dev/null @@ -1,238 +0,0 @@ -/* BEGIN_COMMON_COPYRIGHT_HEADER - * (c)GPL3+ - * - * Copyright: 2020 Nicholas Yoder - * Authors: - * Nicholas Yoder - * - * Modified from elokab systray plugin - * - * This program or 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 3 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-1301 USA - * - * END_COMMON_COPYRIGHT_HEADER */ - -#include "trayicon.h" - -#include -#include -#include -#include -#include - -#include -#include -#include - -typedef uchar* UCHARP; - -static bool xError; - -int windowErrorHandler(Display *d, XErrorEvent *e) -{ - xError = true; - if (e->error_code != BadWindow) - { - char str[1024]; - XGetErrorText(d, e->error_code, str, 1024); - qWarning() << "Error handler" << e->error_code << str; - } - return 0; -} - -//TrayIcon constructor~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -TrayIcon::TrayIcon(Window iconId, QWidget* parent): QWidget(parent), m_iconId(iconId), m_windowId(0), m_iconSize(22, 22) -{ - setFixedWidth(26); - m_iconValid = init(); -} - -bool TrayIcon::init() -{ - Display* dsp = QX11Info::display(); - - XWindowAttributes attr; - if (! XGetWindowAttributes(dsp, m_iconId, &attr)) - return false; - unsigned long mask = 0; - XSetWindowAttributes set_attr; - - Visual* visual = attr.visual; - set_attr.colormap = attr.colormap; - set_attr.background_pixel = 0; - set_attr.border_pixel = 0; - mask = CWColormap|CWBackPixel|CWBorderPixel; - - m_windowId = XCreateWindow(dsp, this->winId(), 0, 0, uint(m_iconSize.width()), uint(m_iconSize.height()), 0, attr.depth, InputOutput, visual, mask, &set_attr); - - xError = false; - XErrorHandler old; - old = XSetErrorHandler(windowErrorHandler); - XReparentWindow(dsp, m_iconId, m_windowId, 0, 0); - XSync(dsp, false); - XSetErrorHandler(old); - - if (xError) - { - qWarning() << "* Not icon_swallow *"; - XDestroyWindow(dsp, m_windowId); - return false; - } - - Atom acttype; - int actfmt; - unsigned long nbitem, bytes; - unsigned char *data = nullptr; - int ret; - ret = XGetWindowProperty(dsp, m_iconId, atom("_XEMBED_INFO"), 0, 2, false, atom("_XEMBED_INFO"), &acttype, &actfmt, &nbitem, &bytes, &data); - if (ret == Success) - { - if (data) - XFree(data); - } - else - { - qWarning() << "TrayIcon: xembed error"; - XDestroyWindow(dsp, m_windowId); - return false; - } - - XEvent e; - e.xclient.type = ClientMessage; - e.xclient.serial = 0; - e.xclient.send_event = True; - e.xclient.message_type = atom("_XEMBED"); - e.xclient.window = m_iconId; - e.xclient.format = 32; - e.xclient.data.l[0] = CurrentTime; - e.xclient.data.l[1] = 0; - e.xclient.data.l[2] = 0; - e.xclient.data.l[3] = long(m_windowId); - e.xclient.data.l[4] = 0; - XSendEvent(dsp, m_iconId, false, 0xFFFFFF, &e); - - XSelectInput(dsp, m_iconId, StructureNotifyMask); - m_damage = XDamageCreate(dsp, m_iconId, XDamageReportRawRectangles); - XCompositeRedirectWindow(dsp, m_windowId, CompositeRedirectManual); - XMapWindow(dsp, m_iconId); - XMapRaised(dsp, m_windowId); - XResizeWindow(QX11Info::display(),m_windowId, uint(m_iconSize.width()), uint(m_iconSize.height())); - XResizeWindow(QX11Info::display(),m_iconId, uint(m_iconSize.width()), uint(m_iconSize.height())); - - return true; -} - -TrayIcon::~TrayIcon() -{ - Display* dsp = QX11Info::display(); - XSelectInput(dsp, m_iconId, NoEventMask); - - //mDamage - if (m_damage) - XDamageDestroy(dsp, m_damage); - - //reparent to root - xError = false; - XErrorHandler old = XSetErrorHandler(windowErrorHandler); - XUnmapWindow(dsp, m_iconId); - XReparentWindow(dsp, m_iconId, QX11Info::appRootWindow(), 0, 0); - XDestroyWindow(dsp, m_windowId); - XSync(dsp, False); - XSetErrorHandler(old); -} - -QSize TrayIcon::sizeHint() const -{ - QMargins margins = contentsMargins(); - return QSize(margins.left() + m_iconSize.width() + margins.right(), margins.top() + m_iconSize.height() + margins.bottom()); -} - -void TrayIcon::setIconSize(const QSize &iconSize) -{ - m_iconSize = iconSize; - - if (m_windowId) - XResizeWindow(QX11Info::display(),m_windowId, uint(m_iconSize.width()), uint(m_iconSize.height())); - - if (m_iconId) - XResizeWindow(QX11Info::display(), m_iconId, uint(m_iconSize.width()), uint(m_iconSize.height())); -} - -void TrayIcon::resizeEvent(QResizeEvent *) -{ - QRect rect = iconGeometry(); - XMoveWindow(QX11Info::display(), m_windowId, rect.left(), rect.top()); -} - -void TrayIcon::paintEvent(QPaintEvent *event) -{ - draw(event); -} - -QRect TrayIcon::iconGeometry() -{ - QRect res = QRect(QPoint(0, 0), m_iconSize); - res.moveCenter(QRect(0, 0, width(), height()).center()); - return res; -} - -void TrayIcon::draw(QPaintEvent* /*event*/) -{ - Display* dsp = QX11Info::display(); - - XWindowAttributes attr; - if (!XGetWindowAttributes(dsp, m_iconId, &attr)) - { - qWarning() << "Paint error"; - return; - } - - XImage* ximage = XGetImage(dsp, m_iconId, 0, 0, uint(attr.width), uint(attr.height), AllPlanes, ZPixmap); - if (!ximage) - { - qWarning() << " * Error image is NULL"; - return; - } - - QImage image = QImage(UCHARP(ximage->data), ximage->width, ximage->height, ximage->bytes_per_line, QImage::Format_ARGB32_Premultiplied); - - //Draw QImage ........................... - QPainter painter(this); - - QRect iconRect = iconGeometry(); - - if (image.size() != iconRect.size()) - { - image = image.scaled(iconRect.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); - QRect r = image.rect(); - r.moveCenter(iconRect.center()); - iconRect = r; - } - painter.drawImage(iconRect, image); - - XDestroyImage(ximage); -} - -Atom TrayIcon::atom(const char* atomName) -{ - static QHash hash; - - if (hash.contains(atomName)) - return hash.value(atomName); - - Atom atom = XInternAtom(QX11Info::display(), atomName, false); - hash[atomName] = atom; - return atom; -} diff --git a/forest-panel/plugins/systray-old/trayicon.h b/forest-panel/plugins/systray-old/trayicon.h deleted file mode 100755 index 890da87..0000000 --- a/forest-panel/plugins/systray-old/trayicon.h +++ /dev/null @@ -1,75 +0,0 @@ -/* BEGIN_COMMON_COPYRIGHT_HEADER - * (c)GPL3+ - * - * Copyright: 2020 Nicholas Yoder - * Authors: - * Nicholas Yoder - * - * Modified from elokab systray plugin - * - * This program or 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 3 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-1301 USA - * - * END_COMMON_COPYRIGHT_HEADER */ - -#ifndef TRAYICON_H -#define TRAYICON_H - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -class TrayIcon: public QWidget -{ - Q_OBJECT - -public: - TrayIcon(Window iconId, QWidget* parent); - virtual ~TrayIcon(); - -public slots: - Window iconId() { return m_iconId; } - Window windowId() { return m_windowId; } - bool isValid() const { return m_iconValid; } - QSize iconSize() const { return m_iconSize; } - void setIconSize(const QSize &iconSize); - QSize sizeHint() const; - -protected: - void resizeEvent(QResizeEvent *); - void paintEvent(QPaintEvent *event); - void draw(QPaintEvent* event); - -private: - bool init(); - Atom atom(const char* atomName); - - QRect iconGeometry(); - Window m_iconId; - Window m_windowId; - bool m_iconValid; - QSize m_iconSize; - Damage m_damage; - -}; -#endif // TRAYICON_H diff --git a/forest-panel/plugins/systray-sni/systray-sni.pro b/forest-panel/plugins/systray-sni/systray-sni.pro new file mode 100644 index 0000000..14e7603 --- /dev/null +++ b/forest-panel/plugins/systray-sni/systray-sni.pro @@ -0,0 +1,58 @@ +QT += core gui dbus widgets x11extras + +TARGET = systray-sni +TEMPLATE = lib +DEFINES += PLUG_LIBRARY + +DESTDIR = ../../../usr/lib/forest/panel + +CONFIG += plugin + +LIBS += $(SUBLIBS) -lxcb-composite -lxcb-image -lxcb-damage -lxcb-shape + +INCLUDEPATH = ../../library +INCLUDEPATH += ../../../library +INCLUDEPATH += ../../../library/xcbutills + +LIBS += -L/usr/lib/x86_64-linux-gnu/ -lKF5WindowSystem +INCLUDEPATH += /usr/include/KF5/KWindowSystem +DEPENDPATH += /usr/include/KF5/KWindowSystem + +# The following define makes your compiler emit warnings if you use +# any feature of Qt which has been marked as deprecated (the exact warnings +# depend on your compiler). Please consult the documentation of the +# deprecated API in order to know how to port your code away from it. +DEFINES += QT_DEPRECATED_WARNINGS + +# You can also make your code fail to compile if you use deprecated APIs. +# In order to do so, uncomment the following line. +# You can also select to disable deprecated APIs only up to a certain version of Qt. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +CONFIG += c++11 + +SOURCES += \ + systray.cpp \ + trayicon.cpp \ + ../../../library/xcbutills/xcbutills.cpp + +HEADERS += \ + systray.h \ + trayicon.h \ + ../../library/panelbutton.h \ + ../../library/buttonrender.h \ + ../../../library/xcbutills/xcbutills.h + +# Default rules for deployment. +target.path = /usr/lib/forest/panel + +INSTALLS += target + +#unix:!macx: LIBS += -L$$OUT_PWD/../../../library/xcbutills/ -lxcbutills + +#INCLUDEPATH += $$PWD/../../../library/xcbutills +#DEPENDPATH += $$PWD/../../../library/xcbutills + +#unix:!macx: PRE_TARGETDEPS += $$OUT_PWD/../../../library/xcbutills/libxcbutills.a + + diff --git a/forest-panel/plugins/systray-sni/systray.cpp b/forest-panel/plugins/systray-sni/systray.cpp new file mode 100644 index 0000000..96cfefc --- /dev/null +++ b/forest-panel/plugins/systray-sni/systray.cpp @@ -0,0 +1,249 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL3+ + * + * Copyright: 2022 Nicholas Yoder + * + * This program or 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 3 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-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + +#include "systray.h" + +#include + +#include +#include "trayicon.h" + +#include "xcbutills/xcbutills.h" + +#include +#include +#include + +#define SYSTEM_TRAY_REQUEST_DOCK 0 + +systray::systray(){ +} + +systray::~systray(){ +} + +void systray::setupPlug(QBoxLayout *layout, QList itemlist){ + Q_UNUSED(itemlist); + + mainLayout = new QHBoxLayout(this); + mainLayout->setMargin(0); + mainLayout->setSpacing(0); + layout->addWidget(this); + + //Use KSelectionOwner to get ownership of the systray atom for the current screen + QString trayAtomName = QString("_NET_SYSTEM_TRAY_S%1").arg(QX11Info::appScreen()); + tSelectionOwner = new KSelectionOwner(Xcbutills::atom(trayAtomName), -1, this); + + QTimer::singleShot(500, this, &systray::init); +} + +void systray::init(){ + //load damage extension + xcb_connection_t *c = QX11Info::connection(); + xcb_prefetch_extension_data(c, &xcb_damage_id); + const auto *reply = xcb_get_extension_data(c, &xcb_damage_id); + if (reply && reply->present) { + damageEventBase = reply->first_event; + xcb_damage_query_version_unchecked(c, XCB_DAMAGE_MAJOR_VERSION, XCB_DAMAGE_MINOR_VERSION); + } else { + qDebug() << "Systray: Error - Could not load damage extension."; + } + + connect(tSelectionOwner, &KSelectionOwner::claimedOwnership, this, &systray::onClaimedOwnership); + connect(tSelectionOwner, &KSelectionOwner::failedToClaimOwnership, this, &systray::onFailedToClaimOwnership); + connect(tSelectionOwner, &KSelectionOwner::lostOwnership, this, &systray::onLostOwnership); + tSelectionOwner->claim(false); +} + +void systray::XcbEventFilter(xcb_generic_event_t *ev){ + if (!damageEventBase || !valid) return; + + const auto responseType = XCB_EVENT_RESPONSE_TYPE(ev); + if (responseType == XCB_CLIENT_MESSAGE) { + const auto ce = reinterpret_cast(ev); + if (ce->type == Xcbutills::atom("_NET_SYSTEM_TRAY_OPCODE")) { + switch (ce->data.data32[1]) { + case SYSTEM_TRAY_REQUEST_DOCK: + dock(ce->data.data32[2]); + return; + } + } + } else if (responseType == XCB_UNMAP_NOTIFY) { + const auto unmappedWId = reinterpret_cast(ev)->window; + if (tIcons.contains(unmappedWId)) { + undock(unmappedWId); + } + } else if (responseType == XCB_DESTROY_NOTIFY) { + const auto destroyedWId = reinterpret_cast(ev)->window; + if (tIcons.contains(destroyedWId)) { + undock(destroyedWId); + } + } else if (responseType == damageEventBase + XCB_DAMAGE_NOTIFY) { + const auto damagedWId = reinterpret_cast(ev)->drawable; + const auto tIcon = tIcons.value(damagedWId); + if (tIcon) { + tIcon->update(); + xcb_damage_subtract(QX11Info::connection(), tDamageWatches[damagedWId], XCB_NONE, XCB_NONE); + } + } else if (responseType == XCB_CONFIGURE_REQUEST) { + const auto event = reinterpret_cast(ev); + const auto tIcon = tIcons.value(event->window); + if (tIcon) { + // 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)) { + tIcon->resizeWindow(event->width, event->height); + } + } + } else if (responseType == XCB_VISIBILITY_NOTIFY) { + const auto event = reinterpret_cast(ev); + // it's possible that something showed our container window, we have to hide it + // workaround for BUG 357443: when KWin is restarted, container window is shown on top + if (event->state == XCB_VISIBILITY_UNOBSCURED) { + for (auto tIcon : tIcons.values()) { + tIcon->hideContainerWindow(event->window); + } + } + } +} + +QHash systray::getpluginfo(){ + QHash info; + info["name"] = "System Tray SNI"; + info["needsXcbEvents"] = "true"; + return info; +} + +void systray::dock(xcb_window_t winId){ + qDebug() << "Systray: Docking window " << winId; + + if (tIcons.contains(winId)) + return; + + if (addDamageWatch(winId)) { + trayicon *tIcon = new trayicon(winId); + tIcons[winId] = tIcon; + mainLayout->addWidget(tIcon); + } +} + +void systray::undock(xcb_window_t winId){ + qDebug() << "Systray: Undocking window " << winId; + + if (!tIcons.contains(winId)) + return; + + trayicon *tIcon = tIcons[winId]; + mainLayout->removeWidget(tIcon); + tIcon->deleteLater(); + tIcons.remove(winId); +} + +bool systray::addDamageWatch(xcb_window_t client){ + qDebug() << "Systray: Adding damage watch for " << client; + + xcb_connection_t *c = QX11Info::connection(); + const auto attribsCookie = xcb_get_window_attributes_unchecked(c, client); + + const auto damageId = xcb_generate_id(c); + tDamageWatches[client] = damageId; + xcb_damage_create(c, damageId, client, XCB_DAMAGE_REPORT_LEVEL_NON_EMPTY); + + xcb_generic_error_t *error = nullptr; + QScopedPointer attr(xcb_get_window_attributes_reply(c, attribsCookie, &error)); + QScopedPointer getAttrError(error); + uint32_t events = XCB_EVENT_MASK_STRUCTURE_NOTIFY; + if (!attr.isNull()) { + events = events | attr->your_event_mask; + } + // if window is already gone, there is no need to handle it. + if (getAttrError && getAttrError->error_code == XCB_WINDOW) { + return false; + } + // the event mask will not be removed again. We cannot track whether another component also needs STRUCTURE_NOTIFY (e.g. KWindowSystem). + // if we would remove the event mask again, other areas will break. + const auto changeAttrCookie = xcb_change_window_attributes_checked(c, client, XCB_CW_EVENT_MASK, &events); + QScopedPointer changeAttrError(xcb_request_check(c, changeAttrCookie)); + // if window is gone by this point, it will be caught by eventFilter, so no need to check later errors. + if (changeAttrError && changeAttrError->error_code == XCB_WINDOW) { + return false; + } + + return true; +} + + +void systray::onClaimedOwnership() +{ + qDebug() << "Systray: Claimed ownership of Systray Manager"; + setSystemTrayVisual(); + valid = true; +} + +void systray::onFailedToClaimOwnership() +{ + qWarning() << "Systray: Failed to claim ownership of Systray Manager"; + valid = false; +} + +void systray::onLostOwnership() +{ + qWarning() << "Systray: Lost ownership of Systray Manager"; + valid = false; +} + +void systray::setSystemTrayVisual() +{ + xcb_connection_t *c = QX11Info::connection(); + auto screen = xcb_setup_roots_iterator(xcb_get_setup(c)).data; + auto trayVisual = screen->root_visual; + xcb_depth_iterator_t depth_iterator = xcb_screen_allowed_depths_iterator(screen); + xcb_depth_t *depth = nullptr; + + while (depth_iterator.rem) { + if (depth_iterator.data->depth == 32) { + depth = depth_iterator.data; + break; + } + xcb_depth_next(&depth_iterator); + } + + if (depth) { + xcb_visualtype_iterator_t visualtype_iterator = xcb_depth_visuals_iterator(depth); + while (visualtype_iterator.rem) { + xcb_visualtype_t *visualtype = visualtype_iterator.data; + if (visualtype->_class == XCB_VISUAL_CLASS_TRUE_COLOR) { + trayVisual = visualtype->visual_id; + break; + } + xcb_visualtype_next(&visualtype_iterator); + } + } + + xcb_change_property(c, + XCB_PROP_MODE_REPLACE, + tSelectionOwner->ownerWindow(), + Xcbutills::atom("_NET_SYSTEM_TRAY_VISUAL"), + XCB_ATOM_VISUALID, + 32, + 1, + &trayVisual); +} diff --git a/forest-panel/plugins/systray-sni/systray.h b/forest-panel/plugins/systray-sni/systray.h new file mode 100644 index 0000000..86c9424 --- /dev/null +++ b/forest-panel/plugins/systray-sni/systray.h @@ -0,0 +1,78 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL3+ + * + * Copyright: 2021 Nicholas Yoder + * + * This program or 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 3 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-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + +#ifndef SYSTRAY_H +#define SYSTRAY_H + +#include +#include + +#include "panelpluginterface.h" + +#include + +class KSelectionOwner; +class trayicon; + +class systray : public QWidget, panelpluginterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "forest.panel.systray-sni.plugin") + Q_INTERFACES(panelpluginterface) + +public: + systray(); + ~systray(); + + //begin plugininterface + void setupPlug(QBoxLayout *layout, QList itemlist); + void closePlug(){close(); deleteLater();} + void XcbEventFilter(xcb_generic_event_t *ev); + QHash getpluginfo(); + //end plugininterface + +private: + void init(); + + void dock(xcb_window_t winId); + void undock(xcb_window_t winId); + bool addDamageWatch(xcb_window_t client); + +private slots: + void onClaimedOwnership(); + void onFailedToClaimOwnership(); + void onLostOwnership(); + void setSystemTrayVisual(); + +private: + QHBoxLayout *mainLayout = nullptr; + + uint8_t damageEventBase = 0; + QHash tIcons; + QHash tDamageWatches; + KSelectionOwner *tSelectionOwner; + + bool valid = false; + +}; + +#endif // SYSTRAY_H diff --git a/forest-panel/plugins/systray-sni/trayicon.cpp b/forest-panel/plugins/systray-sni/trayicon.cpp new file mode 100644 index 0000000..203548a --- /dev/null +++ b/forest-panel/plugins/systray-sni/trayicon.cpp @@ -0,0 +1,633 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL3+ + * + * Copyright: 2022 Nicholas Yoder + * + * This program or 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 3 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-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + +#include "trayicon.h" + +#include +#include + +#include "xcbutills.h" + +#include +#include +#include + +#include + +#include + +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; + +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 = Xcbutills::atom("_XEMBED"); + xcb_send_event(QX11Info::connection(), false, towin, XCB_EVENT_MASK_NO_EVENT, (char *) &ev); +} + +trayicon::trayicon(xcb_window_t wid): iWindowId(wid), sendingClickEvent(false), iInjectMode(Direct){ + setupIconButton(QIcon::fromTheme("unknown")); + connect(this, &trayicon::mouseReleased, this, &trayicon::handleClick); + init(); +} + +trayicon::~trayicon(){ + auto c = QX11Info::connection(); + xcb_destroy_window(c, iContainerWid); +} + +void trayicon::init(){ + auto c = QX11Info::connection(); + + //create a container window + auto screen = xcb_setup_roots_iterator (xcb_get_setup (c)).data; + iContainerWid = 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_VISIBILITY_CHANGE | // receive visibility change, to handle KWin restart #357443 + // Redirect and handle structure (size, position) requests from the embedded window. + 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 */ + iContainerWid, /* 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 + + We can't composite it away anything parented owned by the root window (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 + */ + + + stackContainerWindow(XCB_STACK_MODE_BELOW); + + NETWinInfo wm(c, iContainerWid, screen->root, NET::Properties(), NET::Properties2()); + wm.setOpacity(0); + + + xcb_flush(c); + + xcb_map_window(c, iContainerWid); + + xcb_reparent_window(c, iWindowId, + iContainerWid, + 0, 0); + + /* + * Render the embedded window offscreen + */ + xcb_composite_redirect_window(c, iWindowId, 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, iWindowId); + + //tell client we're embedding it + xembed_message_send(iWindowId, XEMBED_EMBEDDED_NOTIFY, 0, iContainerWid, XEMBED_VERSION); + + //move window we're embedding + const uint32_t windowMoveConfigVals[2] = { 0, 0 }; + + xcb_configure_window(c, iWindowId, + XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y, + windowMoveConfigVals); + + QSize clientWindowSize = calculateClientWindowSize(); + + //show the embedded window otherwise nothing happens + xcb_map_window(c, iWindowId); + + xcb_clear_area(c, 0, iWindowId, 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, iWindowId); + QScopedPointer windowAttributes(xcb_get_window_attributes_reply(c, waCookie, nullptr)); + if (windowAttributes && ! (windowAttributes->all_event_masks & XCB_EVENT_MASK_BUTTON_PRESS)) { + iInjectMode = 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, &trayicon::updateIcon); +} + +void trayicon::updateIcon(){ + const QImage image = getImageNonComposite(); + if (image.isNull()) { + qDebug() << "No xembed icon for" << iWindowId << Title(); + return; + } + + int w = image.width(); + int h = image.height(); + + iPixmap = QPixmap::fromImage(image); + if (w > s_embedSize || h > s_embedSize) { + qDebug() << "Scaling pixmap of window" << iWindowId << Title() << "from w*h" << w << h; + iPixmap = iPixmap.scaled(s_embedSize, s_embedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + } + + setIcon(QIcon(iPixmap)); + //Q_EMIT NewIcon(); + //Q_EMIT NewToolTip(); +} + +void trayicon::resizeWindow(const uint16_t width, const uint16_t height) const +{ + auto connection = QX11Info::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, iWindowId, + XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, + windowSizeConfigVals); + + xcb_flush(connection); +} + +void trayicon::hideContainerWindow(xcb_window_t windowId) const +{ + if (iContainerWid == windowId && !sendingClickEvent) { + qDebug() << "Container window visible, stack below"; + stackContainerWindow(XCB_STACK_MODE_BELOW); + } +} + +QSize trayicon::calculateClientWindowSize() const +{ + auto c = QX11Info::connection(); + + auto cookie = xcb_get_geometry(c, iWindowId); + QScopedPointer + 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 artbitrary heuristic to make sure icons are always sensible + if (clientWindowSize.isEmpty() || clientWindowSize.width() > s_embedSize || clientWindowSize.height() > s_embedSize) { + qDebug() << "Resizing window" << iWindowId << Title() << "from w*h" << clientWindowSize; + + resizeWindow(s_embedSize, s_embedSize); + + clientWindowSize = QSize(s_embedSize, s_embedSize); + } + + return clientWindowSize; +} + + +void ti_cleanup_xcb_image(void *data) { + xcb_image_destroy(static_cast(data)); +} + +bool trayicon::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 trayicon::getImageNonComposite() const +{ + auto c = QX11Info::connection(); + + QSize clientWindowSize = calculateClientWindowSize(); + + xcb_image_t *image = xcb_image_get(c, iWindowId, 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 { + qDebug() << "Skip NULL image returned from xcb_image_get() for" << iWindowId << Title(); + return QImage(); + } + + 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)) { + qDebug() << "Skip transparent xembed icon for" << iWindowId << Title(); + return QImage(); + } else + return elaborateConversion; + } else { + // Now we are sure we can eventually delete the xcb_image_t with this version + return QImage(image->data, image->width, image->height, image->stride, QImage::Format_ARGB32, ti_cleanup_xcb_image, image); + } +} + +QImage trayicon::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 + quint32 *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 QImage(); // we don't know + } + + QImage image(xcbImage->data, xcbImage->width, xcbImage->height, xcbImage->stride, format, ti_cleanup_xcb_image, xcbImage); + + if (image.isNull()) { + return QImage(); + } + + if (format == QImage::Format_RGB32 && xcbImage->bpp == 32) + { + QImage m = image.createHeuristicMask(); + QBitmap mask(QPixmap::fromImage(m)); + QPixmap p = QPixmap::fromImage(image); + p.setMask(mask); + 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 trayicon::calculateClickPoint() const +{ + QPoint clickPoint = QPoint(0, 0); + + auto c = QX11Info::connection(); + + // request extent to check if shape has been set + xcb_shape_query_extents_cookie_t extentsCookie = xcb_shape_query_extents(c, iWindowId); + // 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, iWindowId, XCB_SHAPE_SK_BOUNDING); + + QScopedPointer + extentsReply(xcb_shape_query_extents_reply(c, extentsCookie, nullptr)); + QScopedPointer + 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); + } + } + + qDebug() << "Click point:" << clickPoint; + return clickPoint; +} + +void trayicon::stackContainerWindow(const uint32_t stackMode) const{ + auto c = QX11Info::connection(); + const uint32_t stackData[] = {stackMode}; + xcb_configure_window(c, iContainerWid, XCB_CONFIG_WINDOW_STACK_MODE, stackData); +} + +QString trayicon::Title() const{ + return Xcbutills::getWindowTitle(iWindowId); +} + +// Actions ========================= +void trayicon::handleClick(QMouseEvent *event){ + int x = QCursor::pos().x(); + int y = QCursor::pos().y(); + + if (event->button() == Qt::LeftButton) + sendClick(XCB_BUTTON_INDEX_1, x, y); + else if (event->button() == Qt::MiddleButton) + sendClick(XCB_BUTTON_INDEX_2, x, y); + else if (event->button() == Qt::RightButton) + sendClick(XCB_BUTTON_INDEX_3, x, y); +} + +void trayicon::wheelEvent(QWheelEvent *event){ + Scroll(event->angleDelta().x(), "vertical"); +} + +void trayicon::enterEvent(QEvent *){ + auto c = QX11Info::connection(); + + int x = QCursor::pos().x(); + int y = QCursor::pos().y(); + QPoint local_pos = mapFromGlobal(QPoint(x,y)); + + //stackContainerWindow(XCB_STACK_MODE_ABOVE); + + xcb_enter_notify_event_t* event = new xcb_enter_notify_event_t; + memset(event, 0x00, sizeof (xcb_enter_notify_event_t)); + event->response_type = XCB_ENTER_NOTIFY; + event->event = iWindowId; + event->time = XCB_CURRENT_TIME; + event->root = QX11Info::appRootWindow(); + event->root_x = x; + event->root_y = y; + event->event_x = static_cast(local_pos.x()); + event->event_y = static_cast(local_pos.y()); + event->child = 0; + event->state = 0; + event->detail = 0; + + xcb_send_event(c, false, iWindowId, XCB_EVENT_MASK_ENTER_WINDOW, (char *) event); + delete event; + + //stackContainerWindow(XCB_STACK_MODE_BELOW); +} + +void trayicon::leaveEvent(QEvent *){ + auto c = QX11Info::connection(); + + int x = QCursor::pos().x(); + int y = QCursor::pos().y(); + QPoint local_pos = mapFromGlobal(QPoint(x,y)); + + //stackContainerWindow(XCB_STACK_MODE_ABOVE); + + xcb_leave_notify_event_t* event = new xcb_enter_notify_event_t; + memset(event, 0x00, sizeof (xcb_enter_notify_event_t)); + event->response_type = XCB_LEAVE_NOTIFY; + event->event = iWindowId; + event->time = XCB_CURRENT_TIME; + event->root = QX11Info::appRootWindow(); + event->root_x = x; + event->root_y = y; + event->event_x = static_cast(local_pos.x()); + event->event_y = static_cast(local_pos.y()); + event->child = 0; + event->state = 0; + event->detail = 0; + + xcb_send_event(c, false, iWindowId, XCB_EVENT_MASK_LEAVE_WINDOW, (char *) event); + delete event; + + //stackContainerWindow(XCB_STACK_MODE_BELOW); +} + +/* +void trayicon::mouseMoveEvent(QMouseEvent *event){ + + int x = QCursor::pos().x(); + int y = QCursor::pos().y(); + + auto c = QX11Info::connection(); + + stackContainerWindow(XCB_STACK_MODE_ABOVE); + + xcb_motion_notify_event_t* x_event = new xcb_motion_notify_event_t; + memset(x_event, 0x00, sizeof (xcb_motion_notify_event_t)); + x_event->response_type = XCB_MOTION_NOTIFY; + x_event->event = iWindowId; + x_event->time = XCB_CURRENT_TIME; + x_event->same_screen = 1; + x_event->root = QX11Info::appRootWindow(); + x_event->root_x = x; + x_event->root_y = y; + x_event->event_x = static_cast(event->x()); + x_event->event_y = static_cast(event->y()); + x_event->child = 0; + x_event->state = 0; + x_event->detail = 0; + + xcb_send_event(c, false, iWindowId, XCB_EVENT_MASK_POINTER_MOTION, (char *) x_event); + delete x_event; + + stackContainerWindow(XCB_STACK_MODE_BELOW); +}*/ + +void trayicon::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 trayicon::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 + + qDebug() << "Received click" << mouseButton << "with passed x*y" << x << y; + sendingClickEvent = true; + + auto c = QX11Info::connection(); + + auto cookieSize = xcb_get_geometry(c, iWindowId); + QScopedPointer + clientGeom(xcb_get_geometry_reply(c, cookieSize, nullptr)); + + if (!clientGeom) { + return; + } + + auto cookie = xcb_query_pointer(c, iWindowId); + QScopedPointer + pointer(xcb_query_pointer_reply(c, cookie, nullptr)); + /*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 + configVals[0] = pointer->root_x; + configVals[1] = pointer->root_y; + } else { + if (pointer->root_x > x + clientGeom->width) + configVals[0] = pointer->root_x - clientGeom->width + 1; + else + configVals[0] = static_cast(x - clickPoint.x()); + if (pointer->root_y > y + clientGeom->height) + configVals[1] = pointer->root_y - clientGeom->height + 1; + else + configVals[1] = static_cast(y - clickPoint.y()); + } + xcb_configure_window(c, iContainerWid, XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y, configVals); + + //pull window up + //stackContainerWindow(XCB_STACK_MODE_ABOVE); + + + //mouse down + if (iInjectMode == Direct) { + xcb_button_press_event_t* event = new xcb_button_press_event_t; + memset(event, 0x00, sizeof(xcb_button_press_event_t)); + event->response_type = XCB_BUTTON_PRESS; + event->event = iWindowId; + event->time = QX11Info::getTimestamp(); + event->same_screen = 1; + event->root = QX11Info::appRootWindow(); + 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, iWindowId, XCB_EVENT_MASK_BUTTON_PRESS, (char *) event); + delete event; + } else { +// sendXTestPressed(QX11Info::display(), mouseButton); + } + + + //mouse up + if (iInjectMode == Direct){ + xcb_button_release_event_t* event = new xcb_button_release_event_t; + memset(event, 0x00, sizeof(xcb_button_release_event_t)); + event->response_type = XCB_BUTTON_RELEASE; + event->event = iWindowId; + event->time = QX11Info::getTimestamp(); + event->same_screen = 1; + event->root = QX11Info::appRootWindow(); + 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, iWindowId, XCB_EVENT_MASK_BUTTON_RELEASE, (char *) event); + delete event; + } else { +// sendXTestReleased(QX11Info::display(), mouseButton); + } + + //stackContainerWindow(XCB_STACK_MODE_BELOW); + + + sendingClickEvent = false; + qDebug() << "Sent Click"; +} diff --git a/forest-panel/plugins/systray-sni/trayicon.h b/forest-panel/plugins/systray-sni/trayicon.h new file mode 100644 index 0000000..8aee1b2 --- /dev/null +++ b/forest-panel/plugins/systray-sni/trayicon.h @@ -0,0 +1,91 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL3+ + * + * Copyright: 2021 Nicholas Yoder + * + * This program or 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 3 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-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + +#ifndef TRAYICON_H +#define TRAYICON_H + +#include "panelbutton.h" + +#include +#include + +class trayicon: public panelbutton +{ + Q_OBJECT + +public: + explicit trayicon(xcb_window_t wid); + ~trayicon() override; + + void resizeWindow(const uint16_t width, const uint16_t height) const; + void hideContainerWindow(xcb_window_t windowId) const; + + QString Title() const; + +private slots: + void handleClick(QMouseEvent *event); + + // 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); + +protected: + void wheelEvent(QWheelEvent *event) override; + void enterEvent(QEvent *) override; + void leaveEvent(QEvent *) override; + //void mouseMoveEvent(QMouseEvent *event) override; + +private: + enum InjectMode { + Direct, + XTest + }; + + void init(); + void updateIcon(); + + 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 stackContainerWindow(const uint32_t stackMode) const; + + xcb_window_t iWindowId; + xcb_window_t iContainerWid; + + QPixmap iPixmap; + bool sendingClickEvent; + InjectMode iInjectMode; + +}; + +#endif // TRAYICON_H diff --git a/forest-panel/plugins/systray/systray.cpp b/forest-panel/plugins/systray/systray.cpp index 63511da..4c1a0b9 100644 --- a/forest-panel/plugins/systray/systray.cpp +++ b/forest-panel/plugins/systray/systray.cpp @@ -1,7 +1,7 @@ /* BEGIN_COMMON_COPYRIGHT_HEADER * (c)LGPL3+ * - * Copyright: 2022 Nicholas Yoder + * Copyright: 2021 Nicholas Yoder * * This program or library is free software; you can redistribute it * and/or modify it under the terms of the GNU Lesser General Public @@ -22,228 +22,257 @@ #include "systray.h" -#include - -#include -#include "trayicon.h" - -#include "xcbutills/xcbutills.h" - -#include -#include #include +#include #define SYSTEM_TRAY_REQUEST_DOCK 0 +#define SYSTEM_TRAY_BEGIN_MESSAGE 1 +#define SYSTEM_TRAY_CANCEL_MESSAGE 2 +#define XEMBED_EMBEDDED_NOTIFY 0 +#define XEMBED_MAPPED (1 << 0) -systray::systray(){ +systray::systray() +{ } -systray::~systray(){ +systray::~systray() +{ + stoptray(); } -void systray::setupPlug(QBoxLayout *layout, QList itemlist){ +void systray::setupPlug(QBoxLayout *layout, QList itemlist) +{ Q_UNUSED(itemlist); - mainLayout = new QHBoxLayout(this); - mainLayout->setMargin(0); - mainLayout->setSpacing(0); - layout->addWidget(this); + mDisplay = QX11Info::display(); + _NET_SYSTEM_TRAY_OPCODE = Xcbutills::atom("_NET_SYSTEM_TRAY_OPCODE"); - //Use KSelectionOwner to get ownership of the systray atom for the current screen - QString trayAtomName = QString("_NET_SYSTEM_TRAY_S%1").arg(QX11Info::appScreen()); - tSelectionOwner = new KSelectionOwner(Xcbutills::atom(trayAtomName), -1, this); + traylayout = new QHBoxLayout(this); + traylayout->setMargin(0); + traylayout->setSpacing(0); + layout->addWidget(this); - QTimer::singleShot(500, this, &systray::init); + iconsize = QSize(22,22); + QTimer::singleShot(0, this, &systray::starttray); } -void systray::init(){ - //load damage extension - xcb_connection_t *c = QX11Info::connection(); - xcb_prefetch_extension_data(c, &xcb_damage_id); - const auto *reply = xcb_get_extension_data(c, &xcb_damage_id); - if (reply && reply->present) { - damageEventBase = reply->first_event; - xcb_damage_query_version_unchecked(c, XCB_DAMAGE_MAJOR_VERSION, XCB_DAMAGE_MINOR_VERSION); - } else { - qDebug() << "Systray: Error - Could not load damage extension."; - } +void systray::XcbEventFilter(xcb_generic_event_t *event) +{ + TrayIcon* icon; + int event_type = event->response_type & ~0x80; - connect(tSelectionOwner, &KSelectionOwner::claimedOwnership, this, &systray::onClaimedOwnership); - connect(tSelectionOwner, &KSelectionOwner::failedToClaimOwnership, this, &systray::onFailedToClaimOwnership); - connect(tSelectionOwner, &KSelectionOwner::lostOwnership, this, &systray::onLostOwnership); - tSelectionOwner->claim(false); -} + switch (event_type) + { + case ClientMessage: + clientMessageEvent(event); + break; -void systray::XcbEventFilter(xcb_generic_event_t *ev){ - if (!damageEventBase || !valid) return; - - const auto responseType = XCB_EVENT_RESPONSE_TYPE(ev); - if (responseType == XCB_CLIENT_MESSAGE) { - const auto ce = reinterpret_cast(ev); - if (ce->type == Xcbutills::atom("_NET_SYSTEM_TRAY_OPCODE")) { - switch (ce->data.data32[1]) { - case SYSTEM_TRAY_REQUEST_DOCK: - dock(ce->data.data32[2]); - return; - } - } - } else if (responseType == XCB_UNMAP_NOTIFY) { - const auto unmappedWId = reinterpret_cast(ev)->window; - if (tIcons.contains(unmappedWId)) { - undock(unmappedWId); - } - } else if (responseType == XCB_DESTROY_NOTIFY) { - const auto destroyedWId = reinterpret_cast(ev)->window; - if (tIcons.contains(destroyedWId)) { - undock(destroyedWId); - } - } else if (responseType == damageEventBase + XCB_DAMAGE_NOTIFY) { - const auto damagedWId = reinterpret_cast(ev)->drawable; - const auto tIcon = tIcons.value(damagedWId); - if (tIcon) { - tIcon->update(); - xcb_damage_subtract(QX11Info::connection(), tDamageWatches[damagedWId], XCB_NONE, XCB_NONE); - } - } else if (responseType == XCB_CONFIGURE_REQUEST) { - const auto event = reinterpret_cast(ev); - const auto tIcon = tIcons.value(event->window); - if (tIcon) { - // 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)) { - tIcon->resizeWindow(event->width, event->height); +// case ConfigureNotify: +// icon = findIcon(event->xconfigure.window); +// if (icon) +// icon->configureEvent(&(event->xconfigure)); +// break; + + case DestroyNotify: { + unsigned long event_window; + event_window = reinterpret_cast(event)->window; + icon = findIcon(event_window); + if (icon) + { + icon->windowDestroyed(event_window); + mIcons.removeAll(icon); + delete icon; } + break; } - } else if (responseType == XCB_VISIBILITY_NOTIFY) { - const auto event = reinterpret_cast(ev); - // it's possible that something showed our container window, we have to hide it - // workaround for BUG 357443: when KWin is restarted, container window is shown on top - if (event->state == XCB_VISIBILITY_UNOBSCURED) { - for (auto tIcon : tIcons.values()) { - tIcon->hideContainerWindow(event->window); + default: + if (event_type == mDamageEvent + XDamageNotify) + { + xcb_damage_notify_event_t* dmg = reinterpret_cast(event); + icon = findIcon(dmg->drawable); + if (icon) + icon->updateicon(); } - } + break; } } -QHash systray::getpluginfo(){ +QHash systray::getpluginfo() +{ QHash info; info["name"] = "System Tray"; info["needsXcbEvents"] = "true"; return info; } -void systray::dock(xcb_window_t winId){ - qDebug() << "Systray: Docking window " << winId; - - if (tIcons.contains(winId)) +void systray::starttray() +{ + Display* dsp = mDisplay; + Window root = QX11Info::appRootWindow(); + QString s = QString("_NET_SYSTEM_TRAY_S%1").arg(DefaultScreen(dsp)); + Atom _NET_SYSTEM_TRAY_S = Xcbutills::atom(s.toLatin1()); + + if (XGetSelectionOwner(dsp, _NET_SYSTEM_TRAY_S) != None) + { + qWarning() << "Another systray is running"; + //mValid = false; return; - - if (addDamageWatch(winId)) { - trayicon *tIcon = new trayicon(winId); - tIcons[winId] = tIcon; - mainLayout->addWidget(tIcon); } -} -void systray::undock(xcb_window_t winId){ - qDebug() << "Systray: Undocking window " << winId; + // init systray protocol + mTrayId = XCreateSimpleWindow(dsp, root, -1, -1, 1, 1, 0, 0, 0); - if (!tIcons.contains(winId)) + XSetSelectionOwner(dsp, _NET_SYSTEM_TRAY_S, mTrayId, CurrentTime); + if (XGetSelectionOwner(dsp, _NET_SYSTEM_TRAY_S) != mTrayId) + { + qWarning() << "Can't get systray manager"; + stoptray(); + //mValid = false; return; + } - trayicon *tIcon = tIcons[winId]; - mainLayout->removeWidget(tIcon); - tIcon->deleteLater(); - tIcons.remove(winId); -} + int orientation = 0; //0 = horizontal, 1 = vertical + XChangeProperty(dsp, mTrayId, Xcbutills::atom("_NET_SYSTEM_TRAY_ORIENTATION"), XA_CARDINAL, 32, PropModeReplace, (unsigned char*)&orientation, 1); -bool systray::addDamageWatch(xcb_window_t client){ - qDebug() << "Systray: Adding damage watch for " << client; + // ** Visual ******************************** + VisualID visualId = getVisual(); + if (visualId) + { + XChangeProperty(mDisplay, mTrayId, Xcbutills::atom("_NET_SYSTEM_TRAY_VISUAL"), XA_VISUALID, 32, PropModeReplace, (unsigned char*)&visualId, 1); + } + // ****************************************** - xcb_connection_t *c = QX11Info::connection(); - const auto attribsCookie = xcb_get_window_attributes_unchecked(c, client); + setIconSize(iconsize); - const auto damageId = xcb_generate_id(c); - tDamageWatches[client] = damageId; - xcb_damage_create(c, damageId, client, XCB_DAMAGE_REPORT_LEVEL_NON_EMPTY); + XClientMessageEvent ev; + ev.type = ClientMessage; + ev.window = root; + ev.message_type = Xcbutills::atom("MANAGER"); + ev.format = 32; + ev.data.l[0] = CurrentTime; + ev.data.l[1] = long(_NET_SYSTEM_TRAY_S); + ev.data.l[2] = long(mTrayId); + ev.data.l[3] = 0; + ev.data.l[4] = 0; + XSendEvent(dsp, root, False, StructureNotifyMask, (XEvent*)&ev); - xcb_generic_error_t *error = nullptr; - QScopedPointer attr(xcb_get_window_attributes_reply(c, attribsCookie, &error)); - QScopedPointer getAttrError(error); - uint32_t events = XCB_EVENT_MASK_STRUCTURE_NOTIFY; - if (!attr.isNull()) { - events = events | attr->your_event_mask; - } - // if window is already gone, there is no need to handle it. - if (getAttrError && getAttrError->error_code == XCB_WINDOW) { - return false; - } - // the event mask will not be removed again. We cannot track whether another component also needs STRUCTURE_NOTIFY (e.g. KWindowSystem). - // if we would remove the event mask again, other areas will break. - const auto changeAttrCookie = xcb_change_window_attributes_checked(c, client, XCB_CW_EVENT_MASK, &events); - QScopedPointer changeAttrError(xcb_request_check(c, changeAttrCookie)); - // if window is gone by this point, it will be caught by eventFilter, so no need to check later errors. - if (changeAttrError && changeAttrError->error_code == XCB_WINDOW) { - return false; - } + XDamageQueryExtension(mDisplay, &mDamageEvent, &mDamageError); - return true; + qDebug() << "Systray started"; } - -void systray::onClaimedOwnership() +void systray::stoptray() { - qDebug() << "Systray: Claimed ownership of Systray Manager"; - setSystemTrayVisual(); - valid = true; + for (auto & icon : mIcons) + disconnect(icon, &QObject::destroyed, this, &systray::onIconDestroyed); + qDeleteAll(mIcons); + if (mTrayId) + { + XDestroyWindow(mDisplay, mTrayId); + mTrayId = 0; + } + //mValid = false; } -void systray::onFailedToClaimOwnership() +void systray::onIconDestroyed(QObject * icon) { - qWarning() << "Systray: Failed to claim ownership of Systray Manager"; - valid = false; + //in the time QOjbect::destroyed is emitted, the child destructor + //is already finished, so the qobject_cast to child will return nullptr in all cases + mIcons.removeAll(static_cast(icon)); } -void systray::onLostOwnership() +void systray::addIcon(Window winId) { - qWarning() << "Systray: Lost ownership of Systray Manager"; - valid = false; + // decline to add an icon for a window we already manage + TrayIcon *icon = findIcon(winId); + if(icon) + return; + + icon = new TrayIcon(winId, iconsize); + mIcons.append(icon); + traylayout->addWidget(icon); + connect(icon, &QObject::destroyed, this, &systray::onIconDestroyed); } -void systray::setSystemTrayVisual() +VisualID systray::getVisual() { - xcb_connection_t *c = QX11Info::connection(); - auto screen = xcb_setup_roots_iterator(xcb_get_setup(c)).data; - auto trayVisual = screen->root_visual; - xcb_depth_iterator_t depth_iterator = xcb_screen_allowed_depths_iterator(screen); - xcb_depth_t *depth = nullptr; - - while (depth_iterator.rem) { - if (depth_iterator.data->depth == 32) { - depth = depth_iterator.data; - break; - } - xcb_depth_next(&depth_iterator); - } - - if (depth) { - xcb_visualtype_iterator_t visualtype_iterator = xcb_depth_visuals_iterator(depth); - while (visualtype_iterator.rem) { - xcb_visualtype_t *visualtype = visualtype_iterator.data; - if (visualtype->_class == XCB_VISUAL_CLASS_TRUE_COLOR) { - trayVisual = visualtype->visual_id; + VisualID visualId = 0; + Display* dsp = mDisplay; + + XVisualInfo templ; + templ.screen=QX11Info::appScreen(); + templ.depth=32; + templ.c_class=TrueColor; + + int nvi; + XVisualInfo* xvi = XGetVisualInfo(dsp, VisualScreenMask|VisualDepthMask|VisualClassMask, &templ, &nvi); + + if (xvi) + { + int i; + XRenderPictFormat* format; + for (i = 0; i < nvi; i++) + { + format = XRenderFindVisualFormat(dsp, xvi[i].visual); + if (format && format->type == PictTypeDirect && format->direct.alphaMask) + { + visualId = xvi[i].visualid; break; } - xcb_visualtype_next(&visualtype_iterator); } + XFree(xvi); + } + + return visualId; +} + +void systray::setIconSize(QSize icosize) +{ + iconsize = icosize; + unsigned long size = ulong(qMin(iconsize.width(), iconsize.height())); + XChangeProperty(mDisplay, mTrayId, Xcbutills::atom("_NET_SYSTEM_TRAY_ICON_SIZE"), XA_CARDINAL, 32, PropModeReplace, (unsigned char*)&size, 1); +} + +TrayIcon* systray::findIcon(Window id) +{ + for(TrayIcon* icon : qAsConst(mIcons)) + { + if (icon->iconId() == id || icon->windowId() == id) + return icon; } + return nullptr; +} + +void systray::clientMessageEvent(xcb_generic_event_t *e) +{ + unsigned long opcode; + unsigned long message_type; + Window id; + xcb_client_message_event_t* event = reinterpret_cast(e); + uint32_t* data32 = event->data.data32; + message_type = event->type; + opcode = data32[1]; + if(message_type != _NET_SYSTEM_TRAY_OPCODE) + return; + + switch (opcode) + { + case SYSTEM_TRAY_REQUEST_DOCK: + id = data32[2]; + if (id) + addIcon(id); + break; - xcb_change_property(c, - XCB_PROP_MODE_REPLACE, - tSelectionOwner->ownerWindow(), - Xcbutills::atom("_NET_SYSTEM_TRAY_VISUAL"), - XCB_ATOM_VISUALID, - 32, - 1, - &trayVisual); + case SYSTEM_TRAY_BEGIN_MESSAGE: + case SYSTEM_TRAY_CANCEL_MESSAGE: + qDebug() << "we don't show balloon messages."; + break; + + default: +// if (opcode == xfitMan().atom("_NET_SYSTEM_TRAY_MESSAGE_DATA")) +// qDebug() << "message from dockapp:" << e->data.b; +// else +// qDebug() << "SYSTEM_TRAY : unknown message type" << opcode; + break; + } } diff --git a/forest-panel/plugins/systray/systray.h b/forest-panel/plugins/systray/systray.h index b94feea..6c5ec4f 100644 --- a/forest-panel/plugins/systray/systray.h +++ b/forest-panel/plugins/systray/systray.h @@ -26,12 +26,25 @@ #include #include +//#include "panelbutton.h" #include "panelpluginterface.h" +#include "trayicon.h" +#include "xcbutills/xcbutills.h" +#include + +#include +#include +#include +#include +#include +#include #include +#include + +#undef Bool // defined as int in X11/Xlib.h -class KSelectionOwner; -class trayicon; +typedef long unsigned int luint; class systray : public QWidget, panelpluginterface { @@ -45,34 +58,33 @@ class systray : public QWidget, panelpluginterface //begin plugininterface void setupPlug(QBoxLayout *layout, QList itemlist); - void closePlug(){close(); deleteLater();} - void XcbEventFilter(xcb_generic_event_t *ev); + void closePlug(){this->close(); deleteLater();} + void XcbEventFilter(xcb_generic_event_t *event); QHash getpluginfo(); //end plugininterface -private: - void init(); - - void dock(xcb_window_t winId); - void undock(xcb_window_t winId); - bool addDamageWatch(xcb_window_t client); - private slots: - void onClaimedOwnership(); - void onFailedToClaimOwnership(); - void onLostOwnership(); - void setSystemTrayVisual(); + void starttray(); + void stoptray(); + void onIconDestroyed(QObject * icon); + void clientMessageEvent(xcb_generic_event_t *e); + int clientMessage(WId _wid, Atom _msg, luint data0, luint data1 = 0, luint data2 = 0, luint data3 = 0, luint data4 = 0) const; + void addIcon(Window id); + TrayIcon* findIcon(Window trayId); private: - QHBoxLayout *mainLayout = nullptr; - - uint8_t damageEventBase = 0; - QHash tIcons; - QHash tDamageWatches; - KSelectionOwner *tSelectionOwner; - - bool valid = false; - + VisualID getVisual(); + void setIconSize(QSize icosize); + + QList mIcons; + + QSize iconsize; + Display* mDisplay; + Window mTrayId = 0; + int mDamageEvent = 0; + int mDamageError = 0; + Atom _NET_SYSTEM_TRAY_OPCODE; + QHBoxLayout *traylayout; }; #endif // SYSTRAY_H diff --git a/forest-panel/plugins/systray/systray.pro b/forest-panel/plugins/systray/systray.pro index ce35d5b..021a9de 100644 --- a/forest-panel/plugins/systray/systray.pro +++ b/forest-panel/plugins/systray/systray.pro @@ -8,14 +8,12 @@ DESTDIR = ../../../usr/lib/forest/panel CONFIG += plugin -LIBS += $(SUBLIBS) -lxcb-composite -lxcb-image -lxcb-damage -lxcb-shape +LIBS += $(SUBLIBS) -lXrender -lxcb-composite INCLUDEPATH = ../../library INCLUDEPATH += ../../../library -INCLUDEPATH += ../../../library/xcbutills LIBS += -L/usr/lib/x86_64-linux-gnu/ -lKF5WindowSystem - INCLUDEPATH += /usr/include/KF5/KWindowSystem DEPENDPATH += /usr/include/KF5/KWindowSystem @@ -55,5 +53,3 @@ INSTALLS += target #DEPENDPATH += $$PWD/../../../library/xcbutills #unix:!macx: PRE_TARGETDEPS += $$OUT_PWD/../../../library/xcbutills/libxcbutills.a - - diff --git a/forest-panel/plugins/systray/trayicon.cpp b/forest-panel/plugins/systray/trayicon.cpp index 7412530..7b9231f 100644 --- a/forest-panel/plugins/systray/trayicon.cpp +++ b/forest-panel/plugins/systray/trayicon.cpp @@ -1,7 +1,7 @@ /* BEGIN_COMMON_COPYRIGHT_HEADER * (c)LGPL3+ * - * Copyright: 2022 Nicholas Yoder + * Copyright: 2021 Nicholas Yoder * * This program or library is free software; you can redistribute it * and/or modify it under the terms of the GNU Lesser General Public @@ -20,520 +20,270 @@ * * END_COMMON_COPYRIGHT_HEADER */ -#include "trayicon.h" - -#include -#include - -#include "xcbutills.h" - -#include -#include +// Warning: order of include is important. +#include +#include +#include +#include #include +#include +#include +#include -#include - -#include - -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; - -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 = Xcbutills::atom("_XEMBED"); - xcb_send_event(QX11Info::connection(), false, towin, XCB_EVENT_MASK_NO_EVENT, (char *) &ev); -} - -trayicon::trayicon(xcb_window_t wid): iWindowId(wid), sendingClickEvent(false), iInjectMode(Direct){ - setupIconButton(QIcon::fromTheme("unknown")); - connect(this, &trayicon::mouseReleased, this, &trayicon::handleClick); - init(); -} - -trayicon::~trayicon(){ - auto c = QX11Info::connection(); - xcb_destroy_window(c, iContainerWid); -} - -void trayicon::init(){ - auto c = QX11Info::connection(); - - //create a container window - auto screen = xcb_setup_roots_iterator (xcb_get_setup (c)).data; - iContainerWid = 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_VISIBILITY_CHANGE | // receive visibility change, to handle KWin restart #357443 - // Redirect and handle structure (size, position) requests from the embedded window. - 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 */ - iContainerWid, /* 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 - - We can't composite it away anything parented owned by the root window (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 - */ - - - stackContainerWindow(XCB_STACK_MODE_BELOW); - - NETWinInfo wm(c, iContainerWid, screen->root, NET::Properties(), NET::Properties2()); - wm.setOpacity(0); - - - xcb_flush(c); - - xcb_map_window(c, iContainerWid); - - xcb_reparent_window(c, iWindowId, - iContainerWid, - 0, 0); - - /* - * Render the embedded window offscreen - */ - xcb_composite_redirect_window(c, iWindowId, 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, iWindowId); - - //tell client we're embedding it - xembed_message_send(iWindowId, XEMBED_EMBEDDED_NOTIFY, 0, iContainerWid, XEMBED_VERSION); - - //move window we're embedding - const uint32_t windowMoveConfigVals[2] = { 0, 0 }; - - xcb_configure_window(c, iWindowId, - XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y, - windowMoveConfigVals); - - QSize clientWindowSize = calculateClientWindowSize(); - - //show the embedded window otherwise nothing happens - xcb_map_window(c, iWindowId); - - xcb_clear_area(c, 0, iWindowId, 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, iWindowId); - QScopedPointer windowAttributes(xcb_get_window_attributes_reply(c, waCookie, nullptr)); - if (windowAttributes && ! (windowAttributes->all_event_masks & XCB_EVENT_MASK_BUTTON_PRESS)) { - iInjectMode = 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, &trayicon::updateIcon); -} - -void trayicon::updateIcon(){ - const QImage image = getImageNonComposite(); - if (image.isNull()) { - qDebug() << "No xembed icon for" << iWindowId << Title(); - return; - } +#include "trayicon.h" +#include "xcbutills/xcbutills.h" - int w = image.width(); - int h = image.height(); +#include +#include +#include +#include +#include - iPixmap = QPixmap::fromImage(image); - if (w > s_embedSize || h > s_embedSize) { - qDebug() << "Scaling pixmap of window" << iWindowId << Title() << "from w*h" << w << h; - iPixmap = iPixmap.scaled(s_embedSize, s_embedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); - } +#include - setIcon(QIcon(iPixmap)); - //Q_EMIT NewIcon(); - //Q_EMIT NewToolTip(); -} +#define XEMBED_EMBEDDED_NOTIFY 0 +static bool xError; -void trayicon::resizeWindow(const uint16_t width, const uint16_t height) const +int windowErrorHandler(Display *d, XErrorEvent *e) { - auto connection = QX11Info::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, iWindowId, - XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, - windowSizeConfigVals); - - xcb_flush(connection); + xError = true; + if (e->error_code != BadWindow) { + char str[1024]; + XGetErrorText(d, e->error_code, str, 1024); + qWarning() << "SystemTray: " << e->error_code << str; + } + return 0; } -void trayicon::hideContainerWindow(xcb_window_t windowId) const +TrayIcon::TrayIcon(Window iconId, QSize const & iconSize) : mIconId(iconId), mWindowId(0), mIconSize(iconSize), mDamage(0), mDisplay(QX11Info::display()) { - if (iContainerWid == windowId && !sendingClickEvent) { - qDebug() << "Container window visible, stack below"; - stackContainerWindow(XCB_STACK_MODE_BELOW); - } + // NOTE: + // it's a good idea to save the return value of QX11Info::display(). + // In Qt 5, this API is slower and has some limitations which can trigger crashes. + // The XDisplay value is actally stored in QScreen object of the primary screen rather than + // in a global variable. So when the parimary QScreen is being deleted and becomes invalid, + // QX11Info::display() will fail and cause crash. Storing this value improves the efficiency and + // also prevent potential crashes caused by this bug. + + ///setObjectName("TrayIcon"); + ///setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + // NOTE: + // see https://github.com/lxqt/lxqt/issues/945 + // workaround: delayed init because of weird behaviour of some icons/windows (claws-mail) + // (upon starting the app the window for receiving clicks wasn't correctly sized + // no matter what we've done) + + //connect(t, &QTimer::timeout, this, &TrayIcon::tryleave); + + QTimer::singleShot(200, this, &TrayIcon::init); } -QSize trayicon::calculateClientWindowSize() const +void TrayIcon::init() { - auto c = QX11Info::connection(); + Display* dsp = mDisplay; - auto cookie = xcb_get_geometry(c, iWindowId); - QScopedPointer - clientGeom(xcb_get_geometry_reply(c, cookie, nullptr)); - - QSize clientWindowSize; - if (clientGeom) { - clientWindowSize = QSize(clientGeom->width, clientGeom->height); + XWindowAttributes attr; + if (! XGetWindowAttributes(dsp, mIconId, &attr)){ + deleteLater(); + return; } - //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 artbitrary heuristic to make sure icons are always sensible - if (clientWindowSize.isEmpty() || clientWindowSize.width() > s_embedSize || clientWindowSize.height() > s_embedSize) { - qDebug() << "Resizing window" << iWindowId << Title() << "from w*h" << clientWindowSize; - - resizeWindow(s_embedSize, s_embedSize); - clientWindowSize = QSize(s_embedSize, s_embedSize); + unsigned long mask = 0; + XSetWindowAttributes set_attr; + + Visual* visual = attr.visual; + set_attr.colormap = attr.colormap; + set_attr.background_pixel = 0; + set_attr.border_pixel = 0; + mask = CWColormap|CWBackPixel|CWBorderPixel; + + const QRect icon_geom = iconGeometry(); + mWindowId = XCreateWindow(dsp, this->winId(), icon_geom.x(), icon_geom.y(), uint(icon_geom.width() * metric(PdmDevicePixelRatio)), + uint(icon_geom.height() * metric(PdmDevicePixelRatio)), 0, attr.depth, InputOutput, visual, mask, &set_attr); + + xError = false; + XErrorHandler old; + old = XSetErrorHandler(windowErrorHandler); + XReparentWindow(dsp, mIconId, mWindowId, 0, 0); + XSync(dsp, false); + XSetErrorHandler(old); + + if (xError){ + qWarning() << "SystemTray: Not icon_swallow"; + XDestroyWindow(dsp, mWindowId); + mWindowId = 0; + deleteLater(); + return; } - return clientWindowSize; -} - -void sni_cleanup_xcb_image(void *data) { - xcb_image_destroy(static_cast(data)); -} - -bool trayicon::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; - } + { + Atom acttype; + int actfmt; + unsigned long nbitem, bytes; + unsigned char *data = nullptr; + int ret; + + ret = XGetWindowProperty(dsp, mIconId, Xcbutills::atom("_XEMBED_INFO"), + 0, 2, false, Xcbutills::atom("_XEMBED_INFO"), + &acttype, &actfmt, &nbitem, &bytes, &data); + if (ret == Success) + { + if (data) + XFree(data); + } + else + { + qWarning() << "SystemTray: xembed error"; + XDestroyWindow(dsp, mWindowId); + deleteLater(); + return; } } - return true; -} - -QImage trayicon::getImageNonComposite() const -{ - auto c = QX11Info::connection(); + { + /*XEvent e; + e.xclient.type = ClientMessage; + e.xclient.serial = 0; + e.xclient.send_event = True; + e.xclient.message_type = Xcbutills::atom("_XEMBED"); + e.xclient.window = mIconId; + e.xclient.format = 32; + e.xclient.data.l[0] = CurrentTime; + e.xclient.data.l[1] = XEMBED_EMBEDDED_NOTIFY; + e.xclient.data.l[2] = 0; + e.xclient.data.l[3] = long(mWindowId); + e.xclient.data.l[4] = 0; + XSendEvent(dsp, mIconId, false, 0xFFFFFF, &e); + */ + + xcb_client_message_event_t event; + event.response_type = XCB_CLIENT_MESSAGE; + event.format = 32; + event.sequence = 0; + event.window = mIconId; + event.type = Xcbutills::atom("_XEMBED"); + event.data.data32[0] = CurrentTime; + event.data.data32[1] = XEMBED_EMBEDDED_NOTIFY; + event.data.data32[2] = 0; + event.data.data32[3] = long(mWindowId); + event.data.data32[4] = 0; + uint sendevent_mask = XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY; + xcb_send_event(QX11Info::connection(), false, mIconId, sendevent_mask, (const char *) &event); + } - QSize clientWindowSize = calculateClientWindowSize(); + XSelectInput(dsp, mIconId, StructureNotifyMask); + mDamage = XDamageCreate(dsp, mIconId, XDamageReportRawRectangles); + //XCompositeRedirectWindow(dsp, mWindowId, CompositeRedirectManual); + xcb_composite_redirect_window(QX11Info::connection(), xcb_window_t(mWindowId), CompositeRedirectManual); - xcb_image_t *image = xcb_image_get(c, iWindowId, 0, 0, clientWindowSize.width(), clientWindowSize.height(), 0xFFFFFFFF, XCB_IMAGE_FORMAT_Z_PIXMAP); + //XMapWindow(dsp, mIconId); + xcb_map_window(QX11Info::connection(), xcb_window_t(mIconId)); + //XMapRaised(dsp, mWindowId); + xcb_map_window_checked(QX11Info::connection(), xcb_window_t(mWindowId)); - // 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 { - qDebug() << "Skip NULL image returned from xcb_image_get() for" << iWindowId << Title(); - return QImage(); - } + const QSize req_size{mIconSize * metric(PdmDevicePixelRatio)}; + //XResizeWindow(dsp, mIconId, uint(req_size.width()), uint(req_size.height())); + XResizeWindow(dsp, mIconId, uint(req_size.width()), uint(req_size.height())); - 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)) { - qDebug() << "Skip transparent xembed icon for" << iWindowId << Title(); - return QImage(); - } else - return elaborateConversion; - } else { - // Now we are sure we can eventually delete the xcb_image_t with this version - return QImage(image->data, image->width, image->height, image->stride, QImage::Format_ARGB32, sni_cleanup_xcb_image, image); - } + this->updateicon(); } -QImage trayicon::convertFromNative(xcb_image_t *xcbImage) const +TrayIcon::~TrayIcon() { - 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 - quint32 *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 QImage(); // we don't know - } + Display* dsp = mDisplay; + XSelectInput(dsp, mIconId, NoEventMask); - QImage image(xcbImage->data, xcbImage->width, xcbImage->height, xcbImage->stride, format, sni_cleanup_xcb_image, xcbImage); + if (mDamage) + XDamageDestroy(dsp, mDamage); - if (image.isNull()) { - return QImage(); - } + // reparent to root + xError = false; + XErrorHandler old = XSetErrorHandler(windowErrorHandler); - if (format == QImage::Format_RGB32 && xcbImage->bpp == 32) - { - QImage m = image.createHeuristicMask(); - QBitmap mask(QPixmap::fromImage(m)); - QPixmap p = QPixmap::fromImage(image); - p.setMask(mask); - image = p.toImage(); - } + XUnmapWindow(dsp, mIconId); + XReparentWindow(dsp, mIconId, QX11Info::appRootWindow(), 0, 0); - // 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; + if (mWindowId) + XDestroyWindow(dsp, mWindowId); + XSync(dsp, False); + XSetErrorHandler(old); } - -/* - Wine is using XWindow Shape Extension for transparent tray icons. - We need to find first clickable point starting from top-left. -*/ -QPoint trayicon::calculateClickPoint() const +QSize TrayIcon::sizeHint() const { - QPoint clickPoint = QPoint(0, 0); - - auto c = QX11Info::connection(); - - // request extent to check if shape has been set - xcb_shape_query_extents_cookie_t extentsCookie = xcb_shape_query_extents(c, iWindowId); - // 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, iWindowId, XCB_SHAPE_SK_BOUNDING); - - QScopedPointer - extentsReply(xcb_shape_query_extents_reply(c, extentsCookie, nullptr)); - QScopedPointer - 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(); + QMargins margins = contentsMargins(); + return QSize(margins.left() + mIconSize.width() + margins.right(), margins.top() + mIconSize.height() + margins.bottom()); +} - 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); - } - } +void TrayIcon::setIconSize(QSize iconSize) +{ + mIconSize = iconSize; - qDebug() << "Click point:" << clickPoint; - return clickPoint; -} + const QSize req_size{mIconSize * metric(PdmDevicePixelRatio)}; + if (mWindowId) + Xcbutills::resizeWindow(xcb_window_t(mWindowId), req_size.width(), req_size.height()); -void trayicon::stackContainerWindow(const uint32_t stackMode) const{ - auto c = QX11Info::connection(); - const uint32_t stackData[] = {stackMode}; - xcb_configure_window(c, iContainerWid, XCB_CONFIG_WINDOW_STACK_MODE, stackData); + if (mIconId) + Xcbutills::resizeWindow(xcb_window_t(mIconId), req_size.width(), req_size.height()); } -QString trayicon::Title() const{ - return Xcbutills::getWindowTitle(iWindowId); -} +QRect TrayIcon::iconGeometry() +{ + QRect res = QRect(QPoint(0, 0), mIconSize); -// Actions ========================= -//TODO: handle scroll -void trayicon::handleClick(QMouseEvent *event){ - int x = event->pos().x(); - int y = event->pos().y(); - - if (event->button() == Qt::LeftButton) - sendClick(XCB_BUTTON_INDEX_1, x, y); - else if (event->button() == Qt::MiddleButton) - sendClick(XCB_BUTTON_INDEX_2, x, y); - else if (event->button() == Qt::RightButton) - sendClick(XCB_BUTTON_INDEX_3, x, y); + res.moveCenter(QRect(0, 0, width(), height()).center()); + return res; } -void trayicon::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 TrayIcon::updateicon() +{ + update(); } -void trayicon::sendClick(uint8_t mouseButton, int x, int y) +void TrayIcon::paintEvent(QPaintEvent* event) { - //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 - - qDebug() << "Received click" << mouseButton << "with passed x*y" << x << y; - sendingClickEvent = true; - - auto c = QX11Info::connection(); + //panelbutton::paintEvent(event); - auto cookieSize = xcb_get_geometry(c, iWindowId); - QScopedPointer - clientGeom(xcb_get_geometry_reply(c, cookieSize, nullptr)); - - if (!clientGeom) { + Display* dsp = mDisplay; + XWindowAttributes attr; + if (!XGetWindowAttributes(dsp, mIconId, &attr)) + { + qWarning() << "systray: Paint error"; return; } - auto cookie = xcb_query_pointer(c, iWindowId); - QScopedPointer - pointer(xcb_query_pointer_reply(c, cookie, nullptr)); - /*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 - configVals[0] = pointer->root_x; - configVals[1] = pointer->root_y; - } else { - if (pointer->root_x > x + clientGeom->width) - configVals[0] = pointer->root_x - clientGeom->width + 1; - else - configVals[0] = static_cast(x - clickPoint.x()); - if (pointer->root_y > y + clientGeom->height) - configVals[1] = pointer->root_y - clientGeom->height + 1; - else - configVals[1] = static_cast(y - clickPoint.y()); + XImage* ximage = XGetImage(dsp, mIconId, 0, 0, uint(attr.width), uint(attr.height), AllPlanes, ZPixmap); + if(ximage) + { + iconimage = QImage((const uchar*) ximage->data, ximage->width, ximage->height, ximage->bytes_per_line, QImage::Format_ARGB32_Premultiplied); } - xcb_configure_window(c, iContainerWid, XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y, configVals); - - //pull window up - stackContainerWindow(XCB_STACK_MODE_ABOVE); - - //mouse down - if (iInjectMode == Direct) { - xcb_button_press_event_t* event = new xcb_button_press_event_t; - memset(event, 0x00, sizeof(xcb_button_press_event_t)); - event->response_type = XCB_BUTTON_PRESS; - event->event = iWindowId; - event->time = QX11Info::getTimestamp(); - event->same_screen = 1; - event->root = QX11Info::appRootWindow(); - 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, iWindowId, XCB_EVENT_MASK_BUTTON_PRESS, (char *) event); - delete event; - } else { -// sendXTestPressed(QX11Info::display(), mouseButton); + else { + iconimage = qApp->primaryScreen()->grabWindow(mIconId, 0,0, attr.width, attr.height).toImage(); } - //mouse up - if (iInjectMode == Direct) + + QPainter painter(this); + QRect iconRect = iconGeometry(); + if (iconimage.size() != iconRect.size()) { - xcb_button_release_event_t* event = new xcb_button_release_event_t; - memset(event, 0x00, sizeof(xcb_button_release_event_t)); - event->response_type = XCB_BUTTON_RELEASE; - event->event = iWindowId; - event->time = QX11Info::getTimestamp(); - event->same_screen = 1; - event->root = QX11Info::appRootWindow(); - 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, iWindowId, XCB_EVENT_MASK_BUTTON_RELEASE, (char *) event); - delete event; - } else { -// sendXTestReleased(QX11Info::display(), mouseButton); + iconimage = iconimage.scaled(iconRect.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); + QRect r = iconimage.rect(); + r.moveCenter(iconRect.center()); + iconRect = r; } + painter.drawImage(iconRect, iconimage); +} - stackContainerWindow(XCB_STACK_MODE_BELOW); - +void TrayIcon::windowDestroyed(Window w) +{ + //damage is destroyed if it's parent window was destroyed + if (mIconId == w) + mDamage = 0; +} - sendingClickEvent = false; +bool TrayIcon::isXCompositeAvailable() +{ + int eventBase, errorBase; + return XCompositeQueryExtension(QX11Info::display(), &eventBase, &errorBase ); } diff --git a/forest-panel/plugins/systray/trayicon.h b/forest-panel/plugins/systray/trayicon.h index c831795..47869aa 100644 --- a/forest-panel/plugins/systray/trayicon.h +++ b/forest-panel/plugins/systray/trayicon.h @@ -23,65 +23,72 @@ #ifndef TRAYICON_H #define TRAYICON_H +#include +#include #include "panelbutton.h" -#include -#include +#include +#include -class trayicon: public panelbutton +#define TRAY_ICON_SIZE_DEFAULT 24 + +class QWidget; +class LXQtPanel; + +class TrayIcon: public panelbutton { Q_OBJECT + Q_PROPERTY(QSize iconSize READ iconSize WRITE setIconSize) public: - explicit trayicon(xcb_window_t wid); - ~trayicon() override; - - void resizeWindow(const uint16_t width, const uint16_t height) const; - void hideContainerWindow(xcb_window_t windowId) const; - - QString Title() const; - -private slots: - void handleClick(QMouseEvent *event); + TrayIcon(Window iconId, QSize const & iconSize); + virtual ~TrayIcon(); - // Shows the context menu associated to this item at the desired screen position - void ContextMenu(int x, int y); + Window iconId() { return mIconId; } + Window windowId() { return mWindowId; } + void windowDestroyed(Window w); - //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); + QSize iconSize() const { return mIconSize; } + void setIconSize(QSize iconSize); + void updateicon(); - // 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); + QSize sizeHint() const; protected: + void paintEvent(QPaintEvent*event); + void enterEvent(QEvent *){ + //setMouseOver(true); + //emit enterevent(); + //t->start(100); + } + + void leaveEvent(QEvent *){ + //tryleave(); + } + + +//private slots: + /*void tryleave(){ + if (!geometry().contains(mapFromGlobal(cursor().pos()))){ + setMouseOver(false); + emit leaveevent(); + t->stop(); + } + }*/ private: - enum InjectMode { - Direct, - XTest - }; - void init(); - void updateIcon(); - - 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 stackContainerWindow(const uint32_t stackMode) const; - - xcb_window_t iWindowId; - xcb_window_t iContainerWid; - QPixmap iPixmap; - bool sendingClickEvent; - InjectMode iInjectMode; + //QTimer *t = new QTimer; + QRect iconGeometry(); + Window mIconId; + Window mWindowId; + QSize mIconSize; + Damage mDamage; + Display* mDisplay; + QImage iconimage; + static bool isXCompositeAvailable(); }; #endif // TRAYICON_H diff --git a/forest-panel/plugins/windowlist/button.cpp b/forest-panel/plugins/windowlist/button.cpp deleted file mode 100755 index 5ad0127..0000000 --- a/forest-panel/plugins/windowlist/button.cpp +++ /dev/null @@ -1,248 +0,0 @@ -/* BEGIN_COMMON_COPYRIGHT_HEADER - * (c)LGPL3+ - * - * Copyright: 2021 Nicholas Yoder - * - * This program or 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 3 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-1301 USA - * - * END_COMMON_COPYRIGHT_HEADER */ - -#include "button.h" - -#include - -button::button(xcb_window_t window, QIcon icon, QString type, QString text){ - bttype = type; - bttext = text; - btwindow = window; - - if (bttype == "icononly"){ - setupIconButton(icon, 16); - } - else if (bttype == "tab"){//take this out or make it work again - setupIconAndTextButton(bttext, icon, 16); - /*if (hlayout){ - closebutton *cbt = new closebutton; - cbt->setFixedSize(18,18); - hlayout->addWidget(cbt); - connect(cbt, &closebutton::clicked, this, &button::closewindow); - }*/ - } - else{ - setupIconAndTextButton(bttext, icon, 16); - } - - pmenu = new popupmenu(this, CenteredOnWidget); - - pmenuitem *item = new pmenuitem("Raise", QIcon::fromTheme("arrow-up")); - connect(item, &pmenuitem::clicked, this, &button::raisewindow); - pmenu->additem(item); - pmenuitem *item2 = new pmenuitem("Maximize", QIcon::fromTheme("arrow-up-double")); - connect(item2, &pmenuitem::clicked, this, &button::maximizewindow); - pmenu->additem(item2); - pmenuitem *item3 = new pmenuitem("Demaximize", QIcon::fromTheme("arrow-down")); - connect(item3, &pmenuitem::clicked, this, &button::demaximizewindow); - pmenu->additem(item3); - pmenuitem *item4 = new pmenuitem("Minimize", QIcon::fromTheme("arrow-down-double")); - connect(item4, &pmenuitem::clicked, this, &button::minimizewindow); - pmenu->additem(item4); - pmenuitem *item5 = new pmenuitem("Close", QIcon::fromTheme("application-exit")); - connect(item5, &pmenuitem::clicked, this, &button::closewindow); - pmenu->additem(item5); - - connect(this, SIGNAL(clicked(QMouseEvent*)), this, SLOT(slotclicked(QMouseEvent*))); - - /*QVBoxLayout *popupvlayout = new QVBoxLayout; - popupvlayout->setMargin(0); - popupvlayout->setSpacing(3); - - QHBoxLayout *hlayout = new QHBoxLayout; - hlayout->setMargin(0); - //wintitlelabel = new QLabel; - //hlayout->addWidget(wintitlelabel); - hlayout->addStretch(1); - closebutton *cbt = new closebutton; - cbt->setFixedSize(18,18); - hlayout->addWidget(cbt); - popupvlayout->addLayout(hlayout); - - scrshotlabel = new QLabel; - popupvlayout->addWidget(scrshotlabel); - pbox = new popup(popupvlayout, this, CenteredOnWidget); - connect(cbt, &closebutton::clicked, pbox, &popup::closepopup); - connect(cbt, &closebutton::clicked, this, &button::closewindow);*/ - connect(this, &button::enterevent, this, &button::handleEnterEvent); - connect(this, &button::leaveevent, this, &button::handleLeaveEvent); -} - -button::~button(){ - pmenu->deleteLater(); -} - -void button::mousePressEvent(QMouseEvent *event){ - if (event->button() == Qt::LeftButton){ - mousepressed = true; - startdragpos = event->pos(); - } -} - -void button::mouseReleaseEvent(QMouseEvent *event){ - if (event->button() == Qt::LeftButton){ - if (mousepressed){ - emit clicked(event); - mousepressed = false; - } - } - else{ - emit clicked(event); - } - -} - -void button::mouseMoveEvent(QMouseEvent *event){ - if (mousepressed){ - if ((event->pos() - startdragpos).manhattanLength() > 20){ - demaximizewindow(); - - //Move window so top of window (title bar) is on screen - KWindowInfo info(btwindow, NET::WMGeometry); - QRect screengeo = qApp->primaryScreen()->geometry(); - QRect windowgeo = info.geometry(); - int x = (windowgeo.width() > screengeo.width()) ? 50 : screengeo.width()/2 - windowgeo.width()/2; - int y = (windowgeo.height() > screengeo.height()) ? 50 : screengeo.height()/2 - windowgeo.height()/2; - Xcbutills::moveWindow(btwindow, x, y); - - mousepressed = false; - } - } -} - -void button::updatedata(){ //get rid of this somehow... - if (this->property("buttontype") != "Icon"){ - KWindowInfo info(btwindow, NET::WMVisibleName | NET::WMName); - QString newtext = info.visibleName().isEmpty() ? info.name() : info.visibleName(); - - if (bttext != newtext){ - bttext = newtext; - setText(bttext); - setIcon(Xcbutills::getWindowIcon(btwindow)); - } - } -} - -void button::slotclicked(QMouseEvent *event){ - if (event->button() == Qt::LeftButton){ - setDown(true); - raisewindow(); - } - else if (event->button() == Qt::RightButton){ - QSize sizeHint = pmenu->popupw->sizeHint(); - if (width() > sizeHint.width()) - pmenu->popupw->setFixedSize(width(), sizeHint.height()); - else - pmenu->popupw->setFixedSize(sizeHint.width(), sizeHint.height()); - - pmenu->show(); - } -} - -void button::sethighlight(xcb_window_t win){ - if (win == btwindow) setDown(true); - else setDown(false); -} - -void button::raisewindow(){ - Xcbutills::raiseWindow(btwindow); -} - -void button::maximizewindow(){ - Xcbutills::maximizeWindow(btwindow); -} - -void button::minimizewindow(){ - Xcbutills::minimizeWindow(btwindow); -} - -void button::closewindow(){ - Xcbutills::closeWindow(btwindow); -} - -void button::demaximizewindow(){ - Xcbutills::demaximizeWindow(btwindow); - //QTimer::singleShot(100, this, SLOT(resizeifneeded())); -} - -void button::resizeifneeded(){ - /*QRect wingeo = Xcbutills::getwindowGeometry(btwindow); - QRect parentgeo = this->parentWidget()->parentWidget()->geometry(); - if (wingeo.intersects(parentgeo)) - { - Xcbutills::moveWindow(btwindow, 50,50); - QRect desktopgeo = qApp->desktop()->availableGeometry(); - Xcbutills::resizeWindow(btwindow, desktopgeo.width() - 100, desktopgeo.height() - 100); - }*/ -} - -void button::handleEnterEvent(){ - emit mouseEnter(this); - /*if (openptimer) - delete openptimer; - - openptimer = new QTimer; - openptimer->setSingleShot(true); - connect(openptimer, &QTimer::timeout, this, &button::showpopup); - openptimer->start(500);*/ -} - -void button::handleLeaveEvent(){ - emit mouseLeave(this); - /*if (openptimer) - { - openptimer->stop(); - delete openptimer; - openptimer = nullptr; - }*/ -} - -/*void button::showpopup() -{ - QPixmap pix = QPixmap::fromImage(Xcbutills::getWindowImage(btwindow)); - scrshotlabel->setPixmap(pix.scaled(200,200, Qt::KeepAspectRatio, Qt::SmoothTransformation)); - - //wintitlelabel->setText(Xcbutills::getWindowTitle(btwindow)); - pbox->showpopup(); - - closeptimer = new QTimer; - connect(closeptimer, &QTimer::timeout, this, &button::closepopup); - closeptimer->start(200); -} - -void button::closepopup() -{ - QRect r(0,0, width(), height()); - - if (!(r.contains(this->mapFromGlobal(QCursor::pos())) || pbox->geometry().contains(QCursor::pos()))) - { - pbox->closepopup(); - - if (closeptimer) - { - closeptimer->stop(); - delete closeptimer; - closeptimer = nullptr; - } - } -}*/ diff --git a/forest-panel/plugins/windowlist/button.h b/forest-panel/plugins/windowlist/button.h deleted file mode 100755 index 0d05b62..0000000 --- a/forest-panel/plugins/windowlist/button.h +++ /dev/null @@ -1,107 +0,0 @@ -/* BEGIN_COMMON_COPYRIGHT_HEADER - * (c)LGPL3+ - * - * Copyright: 2021 Nicholas Yoder - * - * This program or 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 3 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-1301 USA - * - * END_COMMON_COPYRIGHT_HEADER */ - -#ifndef BUTTON_H -#define BUTTON_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "panelbutton.h" -#include "popupmenu.h" - -#include "xcbutills/xcbutills.h" - -class button : public panelbutton -{ - Q_OBJECT - -public: - button(xcb_window_t window, QIcon icon, QString type, QString text); - ~button(); - -public slots: - void sethighlight(xcb_window_t win); - void slotclicked(QMouseEvent *event); - void updatedata(); - xcb_window_t xcbwindow(){return btwindow;} - -signals: - void clicked(QMouseEvent *event); - void sigclicked(int bnum); - void readytoclose(); - void sigpressed(int bnum); - - void mouseEnter(button *bt); - void mouseLeave(button *bt); - -protected: - void mousePressEvent(QMouseEvent *event); - void mouseReleaseEvent(QMouseEvent *event); - void mouseMoveEvent(QMouseEvent *event); - -private slots: - void raisewindow(); - void maximizewindow(); - void minimizewindow(); - void closewindow(); - void demaximizewindow(); - void resizeifneeded(); - - void handleEnterEvent(); - void handleLeaveEvent(); - //void showpopup(); - //void closepopup(); - -private: - bool mousepressed = false; - bool mouseOver = false; - - QString bttext; - QString bttype; - xcb_window_t btwindow; - int btfontsize; - - QPoint startdragpos; - - popupmenu *pmenu = nullptr; - - //QTimer *openptimer = nullptr; - //QTimer *closeptimer = nullptr; - //popup *pbox = nullptr; - //QLabel *scrshotlabel = nullptr; - //QLabel *wintitlelabel = nullptr; - //QVBoxLayout *popupvlayout = nullptr; -}; - -#endif // BUTTON_H diff --git a/forest-panel/plugins/windowlist/imagepopup.cpp b/forest-panel/plugins/windowlist/imagepopup.cpp index 203fddf..6481b25 100644 --- a/forest-panel/plugins/windowlist/imagepopup.cpp +++ b/forest-panel/plugins/windowlist/imagepopup.cpp @@ -23,7 +23,7 @@ imagepopup::imagepopup(QWidget *parentw) connect(pbox, &popup::mousereleased, this, &imagepopup::raisewindow); } -void imagepopup::btmouseEnter(button *bt) +void imagepopup::btmouseEnter(windowbutton *bt) { currentbt = bt; @@ -37,7 +37,7 @@ void imagepopup::btmouseEnter(button *bt) openptimer->start(500); } else { - QPixmap pix = QPixmap::fromImage(Xcbutills::getWindowImage(currentbt->xcbwindow())).scaledToHeight(100, Qt::SmoothTransformation); + QPixmap pix = QPixmap::fromImage(Xcbutills::getWindowImage(currentbt->windowId())).scaledToHeight(100, Qt::SmoothTransformation); scrshotlabel->setPixmap(QPixmap()); pbox->setMinimumSize(0,0); @@ -71,7 +71,7 @@ void imagepopup::showpopup() { if (!currentbt) return; - QPixmap pix = QPixmap::fromImage(Xcbutills::getWindowImage(currentbt->xcbwindow())).scaledToHeight(100, Qt::SmoothTransformation); + QPixmap pix = QPixmap::fromImage(Xcbutills::getWindowImage(currentbt->windowId())).scaledToHeight(100, Qt::SmoothTransformation); scrshotlabel->resize(pix.size()); scrshotlabel->setPixmap(pix); diff --git a/forest-panel/plugins/windowlist/imagepopup.h b/forest-panel/plugins/windowlist/imagepopup.h index 59389f0..2dea1fe 100644 --- a/forest-panel/plugins/windowlist/imagepopup.h +++ b/forest-panel/plugins/windowlist/imagepopup.h @@ -3,9 +3,10 @@ #include #include +#include #include "popup.h" -#include "button.h" +#include "windowbutton.h" #include "closebutton.h" #include "xcbutills/xcbutills.h" @@ -19,7 +20,7 @@ class imagepopup : public QObject signals: public slots: - void btmouseEnter(button *bt); + void btmouseEnter(windowbutton *bt); void btmouseLeave(); void btclicked(); @@ -28,12 +29,12 @@ private slots: void tryclosepopup(); void closepopup(); void deleteopenptimer(); - void closewindow(){if(currentbt){ Xcbutills::closeWindow(currentbt->xcbwindow()); closepopup();}} - void raisewindow(){if(currentbt){ Xcbutills::raiseWindow(currentbt->xcbwindow()); closepopup();}} + void closewindow(){if(currentbt){ Xcbutills::closeWindow(currentbt->windowId()); closepopup();}} + void raisewindow(){if(currentbt){ Xcbutills::raiseWindow(currentbt->windowId()); closepopup();}} //void resizepbox(){pbox->resize(pbox->sizeHint()); pbox->positionOnLauncher();} private: - button *currentbt = nullptr; + windowbutton *currentbt = nullptr; bool open = false; QTimer *openptimer = nullptr; diff --git a/forest-panel/plugins/windowlist/windowbutton.cpp b/forest-panel/plugins/windowlist/windowbutton.cpp index 6d85150..cbd5c6e 100644 --- a/forest-panel/plugins/windowlist/windowbutton.cpp +++ b/forest-panel/plugins/windowlist/windowbutton.cpp @@ -22,10 +22,44 @@ windowbutton::windowbutton(ulong windowid, int desktop, QIcon icon, QString text pmenuitem *item5 = new pmenuitem("Close", QIcon::fromTheme("application-exit")); connect(item5, &pmenuitem::clicked, this, &windowbutton::close_w); pmenu->additem(item5); + + connect(this, &windowbutton::enterevent, this, &windowbutton::handleEnterEvent); + connect(this, &windowbutton::leaveevent, this, &windowbutton::handleLeaveEvent); +} + +void windowbutton::mousePressEvent(QMouseEvent *event){ + if (event->button() == Qt::LeftButton){ + dragActive = true; + dragPos = event->pos(); + } +} + +void windowbutton::mouseMoveEvent(QMouseEvent *event){ + if (dragActive){ + if(event->pos().y() < 0){ + Xcbutills::fitWindowOnScreen(window_id); + dragActive = false; + allowReleaseAction = false; + } + else if(event->pos().x() < -5){ + emit moved(this, true); + allowReleaseAction = false; + } + else if(event->pos().x() > this->width() + 5){ + emit moved(this, false); + allowReleaseAction = false; + } + } } void windowbutton::mouseReleaseEvent(QMouseEvent *event){ panelbutton::mouseReleaseEvent(event); + dragActive = false; + + if(!allowReleaseAction){ + allowReleaseAction = true; + return; + } if (event->button() == Qt::LeftButton){ raise_w(); diff --git a/forest-panel/plugins/windowlist/windowbutton.h b/forest-panel/plugins/windowlist/windowbutton.h index f5b308a..83560ae 100644 --- a/forest-panel/plugins/windowlist/windowbutton.h +++ b/forest-panel/plugins/windowlist/windowbutton.h @@ -17,8 +17,15 @@ class windowbutton : public panelbutton int windowDesktop(){return window_desktop;} signals: + void moved(windowbutton *wbt, bool left); + + void mouseEnter(windowbutton *wbt); + void mouseLeave(windowbutton *wbt); private slots: + void handleEnterEvent(){emit mouseEnter(this);} + void handleLeaveEvent(){emit mouseLeave(this);} + void raise_w(); void maximize_w(); void minimize_w(); @@ -26,12 +33,18 @@ private slots: void demaximize_w(); protected: + void mousePressEvent(QMouseEvent *event); + void mouseMoveEvent(QMouseEvent *event); void mouseReleaseEvent(QMouseEvent *event); private: ulong window_id; int window_desktop; + bool dragActive = false; + bool allowReleaseAction = true; + QPoint dragPos; + popupmenu *pmenu = nullptr; }; diff --git a/forest-panel/plugins/windowlist/windowlist.cpp b/forest-panel/plugins/windowlist/windowlist.cpp index a3a6a6c..8f5a765 100755 --- a/forest-panel/plugins/windowlist/windowlist.cpp +++ b/forest-panel/plugins/windowlist/windowlist.cpp @@ -51,18 +51,11 @@ void windowlist::setupPlug(QBoxLayout *layout, QList itemlist){ connect(item, &pmenuitem::clicked, this, &windowlist::showsettingswidget); pmenu->additem(item); - ipopup = new imagepopup(this); + //ipopup = new imagepopup(this); loadsettings(); currentdesk = Xcbutills::getCurrentDesktop(); - //updatelist(); - - //this updates the text on the button (for like when you change directories in a file manager & the window title changes too.) - //there should be a better way to do this instead of running a timer all the time - //QTimer *t = new QTimer; - //connect(t, SIGNAL(timeout()), this, SIGNAL(updatebuttondata())); - //t->start(800); connect(KWindowSystem::self(), &KWindowSystem::windowAdded, this, &windowlist::onWindowAdded); connect(KWindowSystem::self(), &KWindowSystem::windowRemoved, this, &windowlist::onWindowRemoved); @@ -74,86 +67,15 @@ void windowlist::setupPlug(QBoxLayout *layout, QList itemlist){ QHash windowlist::getpluginfo(){ QHash info; info["name"] = "Window List"; - info["needsXcbEvents"] = "true"; info["stretch"] = "true"; return info; } -//void windowlist::updatelist(){ -/* QList x_client_list = Xcbutills::getClientList(); - - if (x_client_list != oldwindows || currentdesk != olddesk){ - mainlayout->removeWidget(stretchwidget); - - QList desk_windows; - foreach (xcb_window_t window, x_client_list) { - if (Xcbutills::getWindowDesktop(window) == currentdesk){ - desk_windows.append(window); - - if (!oldwindows.contains(window)){ - - KWindowInfo info(window, NET::WMVisibleName | NET::WMName); - QString title = info.visibleName().isEmpty() ? info.name() : info.visibleName(); - - button *bt = new button(window, Xcbutills::getWindowIcon(window), bttype, title); - connect(this, &windowlist::changehighlight, bt, &button::sethighlight); - connect(this, &windowlist::updatebuttondata, bt, &button::updatedata); - //connect(bt, &button::mouseEnter, ipopup, &imagepopup::btmouseEnter); - //connect(bt, &button::mouseLeave, ipopup, &imagepopup::btmouseLeave); - //connect(bt, &button::clicked, ipopup, &imagepopup::btclicked); - - bt->setMaximumWidth(maxbtsize); - mainlayout->addWidget(bt, 1); - button_list[window] = bt; - } - } - } - - foreach (xcb_window_t window, oldwindows) { - if (!desk_windows.contains(window)) { - mainlayout->removeWidget(button_list[window]); - button_list[window]->close(); - delete button_list[window]; - } - } - - mainlayout->addWidget(stretchwidget, 0); - oldwindows = desk_windows; - olddesk = currentdesk; - }*/ -//} - -//process events from X11 i.e. active window change, active desktop change -//called from the plugin host -void windowlist::XcbEventFilter(xcb_generic_event_t* event){ -/* if (event->response_type == XCB_PROPERTY_NOTIFY){ - xcb_client_message_event_t *message = reinterpret_cast(event); - - if (message->type == Xcbutills::atom("_NET_CLIENT_LIST")){ - updatelist(); - } - if (message->type == Xcbutills::atom("_NET_ACTIVE_WINDOW")){ - emit changehighlight(Xcbutills::getActiveWindow()); - } - if (message->type == Xcbutills::atom("_NET_CURRENT_DESKTOP")){ - int newcurrentdesk = Xcbutills::getCurrentDesktop(); - if (newcurrentdesk != currentdesk){ - currentdesk = newcurrentdesk; - updatelist(); - } - } - }*/ -} - void windowlist::reloadsettings(){ loadsettings(); - /*foreach (unsigned long key, button_list.keys()) { - button *bt = button_list[key]; - if (bt){ bt->close(); bt->deleteLater(); } - }*/ - //button_list.clear(); - //oldwindows.clear(); - //updatelist(); + foreach (windowbutton *wbt, button_list) { + wbt->setMaximumWidth(maxbtsize); + } } void windowlist::mouseReleaseEvent(QMouseEvent *event){ @@ -165,7 +87,7 @@ void windowlist::loadsettings(){ QSettings settings("Forest", "Window List"); settings.sync(); bttype = settings.value("buttontype", "twopart").toString(); - maxbtsize = settings.value("maxbuttonsize", 150).toInt(); + maxbtsize = settings.value("maxbuttonsize", 170).toInt(); } void windowlist::showsettingswidget(){ @@ -218,6 +140,9 @@ void windowlist::onWindowAdded(WId window){ if(!button_list.contains(window)){ windowbutton *wbt = new windowbutton(window, desktop, Xcbutills::getWindowIcon(window), Xcbutills::getWindowTitle(window)); + connect(wbt, &windowbutton::moved, this, &windowlist::onButtonMoved); + //connect(wbt, &windowbutton::mouseEnter, ipopup, &imagepopup::btmouseEnter); + //connect(wbt, &windowbutton::mouseLeave, ipopup, &imagepopup::btmouseLeave); wbt->setMaximumWidth(maxbtsize); mainlayout->addWidget(wbt, 1); button_list[window] = wbt; @@ -277,3 +202,20 @@ void windowlist::onDesktopChanged(int desktop){ wbt->setHidden(wbt->windowDesktop() != currentdesk); } } + +void windowlist::onButtonMoved(windowbutton *wbt, bool left){ + int index = mainlayout->indexOf(wbt); + + if (left){ + if (index != 0){ + QLayoutItem *item = mainlayout->takeAt(index); + mainlayout->insertWidget(index-1, item->widget(), 1); + } + } + else{ + if (index != mainlayout->count() - 1){ + QLayoutItem *item = mainlayout->takeAt(index); + mainlayout->insertWidget(index+1, item->widget(), 1); + } + } +} diff --git a/forest-panel/plugins/windowlist/windowlist.h b/forest-panel/plugins/windowlist/windowlist.h index a152ac7..6428123 100755 --- a/forest-panel/plugins/windowlist/windowlist.h +++ b/forest-panel/plugins/windowlist/windowlist.h @@ -33,10 +33,9 @@ #include -#include "button.h" #include "windowbutton.h" -#include "imagepopup.h" +//#include "imagepopup.h" #include "settingswidget.h" #include "panelpluginterface.h" #include "xcbutills/xcbutills.h" @@ -54,7 +53,7 @@ class windowlist : public QWidget, panelpluginterface //begin plugin interface void setupPlug(QBoxLayout *layout, QList itemlist); void closePlug(){ close(); deleteLater();} - void XcbEventFilter(xcb_generic_event_t* event); + void XcbEventFilter(xcb_generic_event_t*){} QHash getpluginfo(); //end plugin interface @@ -70,7 +69,6 @@ public slots: void mouseReleaseEvent(QMouseEvent *event); private slots: - //void updatelist(); void loadsettings(); void showsettingswidget(); @@ -81,6 +79,12 @@ private slots: void onWindowChanged(WId window, NET::Properties prop, NET::Properties2 prop2); void onDesktopChanged(int desktop); + void onButtonMoved(windowbutton *wbt, bool left); + void onButtonEnter(windowbutton *wbt); + void onButtonLeave(windowbutton *); + + void previewWindow(); + private: QWidget *stretchwidget = new QWidget; QHBoxLayout *mainlayout = new QHBoxLayout(); @@ -95,12 +99,10 @@ private slots: int maxbtsize; popupmenu *pmenu = nullptr; - imagepopup *ipopup = nullptr; + //imagepopup *ipopup = nullptr; settingswidget *swidget = new settingswidget; - //QHash> windowHash; - }; #endif // WINDOWLIST_H diff --git a/forest-panel/plugins/windowlist/windowlist.pro b/forest-panel/plugins/windowlist/windowlist.pro index 44ff382..a59968d 100755 --- a/forest-panel/plugins/windowlist/windowlist.pro +++ b/forest-panel/plugins/windowlist/windowlist.pro @@ -7,6 +7,8 @@ DESTDIR = ../../../usr/lib/forest/panel INCLUDEPATH += ../../library INCLUDEPATH += ../../../library +LIBS += -lxcb-image + LIBS += -L/usr/lib/x86_64-linux-gnu/ -lKF5WindowSystem INCLUDEPATH += /usr/include/KF5/KWindowSystem DEPENDPATH += /usr/include/KF5/KWindowSystem @@ -29,7 +31,6 @@ DEFINES += QT_DEPRECATED_WARNINGS #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 SOURCES += \ - button.cpp \ imagepopup.cpp \ settingswidget.cpp \ windowbutton.cpp \ @@ -37,7 +38,6 @@ SOURCES += \ ../../../library/xcbutills/xcbutills.cpp HEADERS += \ - button.h \ closebutton.h \ imagepopup.h \ settingswidget.h \ @@ -46,7 +46,6 @@ HEADERS += \ ../../../library/xcbutills/xcbutills.h \ ../../library/panelbutton.h \ ../../library/popup.h \ - ../../library/popuprender.h \ ../../library/popupmenu.h # Default rules for deployment. diff --git a/library/xcbutills/xcbutills.cpp b/library/xcbutills/xcbutills.cpp index 60e7467..1dca802 100644 --- a/library/xcbutills/xcbutills.cpp +++ b/library/xcbutills/xcbutills.cpp @@ -23,6 +23,8 @@ #include "xcbutills.h" #include +#include "xcb/xcb_image.h" + xcb_connection_t* Xcbutills::xcbconnection = QX11Info::connection(); xcb_atom_t Xcbutills::atom(QString name){ @@ -126,13 +128,28 @@ QImage Xcbutills::getWindowImage(xcb_window_t window) { const xcb_get_geometry_cookie_t geoCookie = xcb_get_geometry_unchecked(xcbconnection, window); xcb_get_geometry_reply_t* geo(xcb_get_geometry_reply(xcbconnection, geoCookie, nullptr)); - if (!geo) - { + if (!geo){ + return QImage(); + } + + + //xcb_connection_t *con = xcb_connect(nullptr, nullptr); + + //xcb_map_window(xcbconnection, window); + //xcb_clear_area(xcbconnection, 1, window, 0, 0, geo->width, geo->height); + //xcb_flush(xcbconnection); + + + xcb_image_t *image = xcb_image_get(xcbconnection, window, 0, 0, geo->width, geo->height, 0xFFFFFFFF, XCB_IMAGE_FORMAT_Z_PIXMAP); + + if (image) { + return QImage(image->data, image->width, image->height, QImage::Format_ARGB32); + } else { return QImage(); } - const xcb_get_image_cookie_t imageCookie = xcb_get_image_unchecked(xcbconnection, XCB_IMAGE_FORMAT_Z_PIXMAP, window, 0, 0, geo->width, geo->height, uint32_t(~0)); - xcb_get_image_reply_t* xImage(xcb_get_image_reply(xcbconnection, imageCookie, nullptr)); + /*const xcb_get_image_cookie_t imageCookie = xcb_get_image_unchecked(con, XCB_IMAGE_FORMAT_Z_PIXMAP, window, 0, 0, geo->width, geo->height, uint32_t(~0)); + xcb_get_image_reply_t* xImage(xcb_get_image_reply(con, imageCookie, nullptr)); if (!xImage) { // for some unknown reason, xcb_get_image failed. try another less efficient method. qDebug() << "Xcbutils: falling back to screen->grabWindow"; xcb_clear_area(xcbconnection, 1, window, 0, 0, geo->width, geo->height); @@ -172,7 +189,7 @@ QImage Xcbutills::getWindowImage(xcb_window_t window) image.setColor(0, QColor(Qt::white).rgb()); image.setColor(1, QColor(Qt::black).rgb()); } - return image; + return image;*/ } int Xcbutills::getNumDesktops() @@ -297,6 +314,16 @@ void Xcbutills::moveWindow(xcb_window_t window, int x, int y) xcb_configure_window(xcbconnection, window, XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y, configVals); } +void Xcbutills::fitWindowOnScreen(xcb_window_t window){ + demaximizeWindow(window); + KWindowInfo info(window, NET::WMGeometry); + QRect screengeo = qApp->primaryScreen()->geometry(); + QRect windowgeo = info.geometry(); + int x = (windowgeo.width() > screengeo.width()) ? 50 : screengeo.width()/2 - windowgeo.width()/2; + int y = (windowgeo.height() > screengeo.height()) ? 50 : screengeo.height()/2 - windowgeo.height()/2; + moveWindow(window, x, y); +} + void Xcbutills::setCurrentDesktop(int desknum) { const uint32_t data[5] = {uint32_t(desknum - 1), 0, 0, 0, 0}; diff --git a/library/xcbutills/xcbutills.h b/library/xcbutills/xcbutills.h index c6be7a4..7e113bb 100644 --- a/library/xcbutills/xcbutills.h +++ b/library/xcbutills/xcbutills.h @@ -95,6 +95,9 @@ class Xcbutills static void moveWindow(xcb_window_t window, int x, int y); + //Move window so top of window (title bar) is on screen + static void fitWindowOnScreen(xcb_window_t window); + static void setCurrentDesktop(int desknum); //not sure what this actually does -- setPartialStrut seems to be what should be used