File indexing completed on 2024-03-24 17:23:11

0001 // SPDX-License-Identifier: GPL-3.0-or-later
0002 /*
0003   Copyright 2017 - 2021 Martin Koller, kollix@aon.at
0004 
0005   This file is part of liquidshell.
0006 
0007   liquidshell is free software: you can redistribute it and/or modify
0008   it under the terms of the GNU General Public License as published by
0009   the Free Software Foundation, either version 3 of the License, or
0010   (at your option) any later version.
0011 
0012   liquidshell is distributed in the hope that it will be useful,
0013   but WITHOUT ANY WARRANTY; without even the implied warranty of
0014   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
0015   GNU General Public License for more details.
0016 
0017   You should have received a copy of the GNU General Public License
0018   along with liquidshell.  If not, see <http://www.gnu.org/licenses/>.
0019 */
0020 
0021 #include <SysTrayNotifyItem.hxx>
0022 
0023 #include <statusnotifieritem_interface.h>
0024 
0025 #include <QDBusMessage>
0026 #include <QDBusPendingCall>
0027 #include <QDBusPendingCallWatcher>
0028 #include <QIcon>
0029 #include <QWheelEvent>
0030 #include <QContextMenuEvent>
0031 #include <QPainter>
0032 
0033 #include <KWinCompat.hxx>
0034 
0035 //--------------------------------------------------------------------------------
0036 // See https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierItem
0037 
0038 SysTrayNotifyItem::SysTrayNotifyItem(QWidget *parent, const QString &service, const QString &path)
0039   : QLabel("?", parent)
0040 {
0041   setContextMenuPolicy(Qt::PreventContextMenu);
0042 
0043   // make sure several "New*" signals from dbus just trigger one fetchData() call
0044   fetchTimer.setSingleShot(true);
0045   fetchTimer.setInterval(500);
0046   connect(&fetchTimer, &QTimer::timeout, this, &SysTrayNotifyItem::fetchData);
0047 
0048   dbus = new OrgKdeStatusNotifierItem(service, path, QDBusConnection::sessionBus(), this);
0049 
0050   connect(dbus, &OrgKdeStatusNotifierItem::NewAttentionIcon, this, &SysTrayNotifyItem::startTimer);
0051   connect(dbus, &OrgKdeStatusNotifierItem::NewIcon,          this, &SysTrayNotifyItem::startTimer);
0052   connect(dbus, &OrgKdeStatusNotifierItem::NewOverlayIcon,   this, &SysTrayNotifyItem::startTimer);
0053   connect(dbus, &OrgKdeStatusNotifierItem::NewStatus,        this, &SysTrayNotifyItem::startTimer);
0054   connect(dbus, &OrgKdeStatusNotifierItem::NewTitle,         this, &SysTrayNotifyItem::startTimer);
0055   connect(dbus, &OrgKdeStatusNotifierItem::NewToolTip,       this, &SysTrayNotifyItem::startTimer);
0056 
0057   fetchData();
0058 }
0059 
0060 //--------------------------------------------------------------------------------
0061 
0062 void SysTrayNotifyItem::startTimer()
0063 {
0064   fetchTimer.start();
0065 }
0066 
0067 //--------------------------------------------------------------------------------
0068 
0069 void SysTrayNotifyItem::fetchData()
0070 {
0071   QDBusMessage msg =
0072       QDBusMessage::createMethodCall(dbus->service(), dbus->path(),
0073                                      "org.freedesktop.DBus.Properties",
0074                                      "GetAll");
0075 
0076   msg << dbus->interface();
0077 
0078   QDBusPendingCall call = QDBusConnection::sessionBus().asyncCall(msg);
0079   QDBusPendingCallWatcher *pendingCallWatcher = new QDBusPendingCallWatcher(call, this);
0080   connect(pendingCallWatcher, &QDBusPendingCallWatcher::finished, this, &SysTrayNotifyItem::fetchDataReply);
0081 }
0082 
0083 //--------------------------------------------------------------------------------
0084 
0085 void SysTrayNotifyItem::fetchDataReply(QDBusPendingCallWatcher *w)
0086 {
0087   w->deleteLater();
0088   QDBusPendingReply<QVariantMap> reply = *w;
0089 
0090   if ( reply.isError() )
0091   {
0092     //qDebug() << dbus->service() << reply.error();
0093     deleteLater();
0094     return;
0095   }
0096 
0097 #if 0
0098   qDebug() << dbus->title() << dbus->service() << dbus->path() << dbus->interface();
0099   qDebug() << "att pixmap file:" << dbus->attentionIconName();
0100   qDebug() << "pixmap file:" << dbus->iconName();
0101   qDebug() << "overlay pixmap file:" << dbus->overlayIconName();
0102   qDebug() << "IconThemePath" << dbus->iconThemePath();
0103   qDebug() << "ItemIsMenu" << dbus->itemIsMenu();
0104   qDebug() << "Menu" << dbus->menu().path();
0105 #endif
0106 
0107   // NOTE: each call to get a property of the "dbus" object involves a DBUS call, which I often
0108   // encounter blocking. Therefore reduce the number of calls as much as possible
0109 
0110   menuPath = dbus->menu().path();
0111 
0112   QStringList origThemePaths = QIcon::themeSearchPaths();
0113 
0114   const QString iconThemePath = dbus->iconThemePath();
0115   if ( !iconThemePath.isEmpty() )
0116   {
0117     QStringList paths = origThemePaths;
0118     paths.prepend(iconThemePath);
0119     QIcon::setThemeSearchPaths(paths);
0120   }
0121 
0122   QPixmap attentionPixmap = dbus->attentionIconPixmap().pixmap(size());
0123   if ( attentionPixmap.isNull() )
0124     attentionPixmap = QIcon::fromTheme(dbus->attentionIconName(), QIcon()).pixmap(size());
0125 
0126   QPixmap pixmap = dbus->iconPixmap().pixmap(size());
0127 
0128   if ( pixmap.isNull() )
0129   {
0130     const QString iconName = dbus->iconName();
0131 
0132     pixmap = QIcon::fromTheme(iconName, QIcon()).pixmap(size());
0133 
0134     if ( pixmap.isNull() && !iconName.isEmpty() && !iconThemePath.isEmpty() )
0135     {
0136       // the file should be findable, but probably the iconThemePath does not contain the index.theme file
0137       pixmap = findPixmap(iconName, iconThemePath);
0138     }
0139 
0140     if ( pixmap.isNull() )
0141       pixmap =  QIcon::fromTheme("image-missing").pixmap(size());
0142   }
0143 
0144   QPixmap overlay = dbus->overlayIconPixmap().pixmap(size());
0145   if ( overlay.isNull() )
0146     overlay = QIcon::fromTheme(dbus->overlayIconName(), QIcon()).pixmap(size());
0147 
0148   // reset to orig since this setting is application wide
0149   QIcon::setThemeSearchPaths(origThemePaths);
0150 
0151   QPixmap finalPixmap;
0152 
0153   const QString status = dbus->status();
0154 
0155   if ( !attentionPixmap.isNull() && (status == "NeedsAttention") )
0156     finalPixmap = applyOverlay(attentionPixmap, overlay);
0157   else if ( !pixmap.isNull() )
0158     finalPixmap = applyOverlay(pixmap, overlay);
0159 
0160   const QString id = dbus->id();
0161   const KDbusToolTipStruct toolTip = dbus->toolTip();
0162 
0163   if ( (id == "KMail") || (id == "Akregator") )
0164   {
0165     // hack for the unwillingness of the kmail maintainer to show unread message count on icon
0166     QString text = toolTip.subTitle.left(toolTip.subTitle.indexOf(QChar(' ')));
0167     bool ok = false;
0168     int num = text.toInt(&ok);
0169     if ( ok && (num < 100) )
0170     {
0171       QPainter painter(&finalPixmap);
0172       painter.setPen(Qt::blue);
0173       painter.drawText(finalPixmap.rect(), Qt::AlignCenter, QString::number(num));
0174     }
0175   }
0176 
0177   if ( !finalPixmap.isNull() )
0178     setPixmap(finalPixmap);
0179 
0180   QString tip(dbus->title());
0181 
0182   if ( //!toolTip.icon.isEmpty() ||
0183        //!toolTip.image.isNull() ||
0184        !toolTip.title.isEmpty() ||
0185        !toolTip.subTitle.isEmpty() )
0186   {
0187     tip = QString("<html><center><b>%1</b><br>%2</center></html>")
0188                   .arg(toolTip.title)
0189                   .arg(toolTip.subTitle);
0190   }
0191 
0192   setToolTip(tip);
0193   show();
0194 
0195   if ( status == "Passive" )
0196   {
0197     // TODO make it configurable
0198     if ( id == "KMail" )
0199       hide();
0200   }
0201 
0202   emit initialized(this);
0203 }
0204 
0205 //--------------------------------------------------------------------------------
0206 
0207 QPixmap SysTrayNotifyItem::findPixmap(const QString &name, const QString &path)
0208 {
0209   QDir dir(path);
0210   QFileInfoList infoList = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files);
0211 
0212   for (const QFileInfo &info : infoList)
0213   {
0214     if ( info.fileName().startsWith(name) )    // maybe check for xxx and xxx. to match xxx.png etc when only xxx given
0215       return QPixmap(info.absoluteFilePath()).scaled(size(), Qt::KeepAspectRatio);
0216 
0217     if ( info.isDir() )
0218     {
0219       QPixmap pix = findPixmap(name, info.absoluteFilePath());
0220       if ( !pix.isNull() )  // only if found, return it, else continue search
0221         return pix;
0222     }
0223   }
0224 
0225   return QPixmap();
0226 }
0227 
0228 //--------------------------------------------------------------------------------
0229 
0230 QPixmap SysTrayNotifyItem::applyOverlay(const QPixmap &pixmap, const QPixmap &overlay)
0231 {
0232   QPixmap result(pixmap);
0233 
0234   if ( !overlay.isNull() )
0235   {
0236     QPainter painter(&result);
0237     painter.drawPixmap(0, 0, overlay);
0238     painter.end();
0239   }
0240 
0241   return result;
0242 }
0243 
0244 //--------------------------------------------------------------------------------
0245 
0246 void SysTrayNotifyItem::wheelEvent(QWheelEvent *event)
0247 {
0248   int delta;
0249   QString orientation;
0250 
0251   if ( event->angleDelta().x() != 0 )
0252   {
0253     orientation = "horizontal";
0254     delta = event->angleDelta().x();
0255   }
0256   else
0257   {
0258     orientation = "vertical";
0259     delta = event->angleDelta().y();
0260   }
0261 
0262   dbus->Scroll(delta, orientation);
0263   event->accept();
0264 }
0265 
0266 //--------------------------------------------------------------------------------
0267 
0268 void SysTrayNotifyItem::mouseReleaseEvent(QMouseEvent *event)
0269 {
0270   // need to do this in the release event since otherwise the popup opened
0271   // by the other application would immediately close again (I assume since this widget
0272   // still grabs the mouse)
0273 
0274   if ( event->button() == Qt::LeftButton )
0275   {
0276     QDBusPendingReply<> reply = dbus->Activate(event->globalPos().x(), event->globalPos().y());
0277     reply.waitForFinished();
0278 
0279     if ( !reply.isError() && dbus->windowId() )
0280     {
0281       WId wid = dbus->windowId();
0282       KWindowSystem::raiseWindow(wid);
0283       KWinCompat::forceActiveWindow(wid);
0284     }
0285   }
0286   else if ( event->button() == Qt::RightButton )
0287   {
0288     QDBusPendingReply<> reply = dbus->ContextMenu(event->globalPos().x(), event->globalPos().y());
0289     reply.waitForFinished();
0290 
0291     if ( reply.isError() && (reply.error().type() == QDBusError::UnknownMethod) &&
0292          !menuPath.isEmpty() )
0293     {
0294       QDBusMessage msg =
0295           QDBusMessage::createMethodCall(dbus->service(), menuPath,
0296                                          "com.canonical.dbusmenu",
0297                                          "GetLayout");
0298 
0299       msg << 0;  // root node id
0300       msg << -1; // all items below root
0301       msg << QStringList(); // all properties
0302 
0303       QDBusPendingCall call = QDBusConnection::sessionBus().asyncCall(msg);
0304       QDBusPendingCallWatcher *pendingCallWatcher = new QDBusPendingCallWatcher(call, this);
0305       connect(pendingCallWatcher, &QDBusPendingCallWatcher::finished, this, &SysTrayNotifyItem::menuLayoutReply);
0306     }
0307   }
0308 }
0309 
0310 //--------------------------------------------------------------------------------
0311 
0312 void SysTrayNotifyItem::menuLayoutReply(QDBusPendingCallWatcher *w)
0313 {
0314   w->deleteLater();
0315 
0316   QDBusMenuItem::registerDBusTypes();
0317   QDBusPendingReply<unsigned int, QDBusMenuLayoutItem> reply = *w;
0318 
0319   if ( reply.isError() )
0320   {
0321     //qDebug() << dbus->service() << reply.error();
0322     return;
0323   }
0324 
0325   QDBusMenuLayoutItem item = reply.argumentAt<1>();
0326 
0327   QMenu menu;
0328   fillMenu(menu, item);
0329 
0330   QAction *action = menu.exec(QCursor::pos());
0331 
0332   if ( !action )
0333     return;
0334 
0335   QDBusMessage msg =
0336       QDBusMessage::createMethodCall(dbus->service(), menuPath,
0337                                      "com.canonical.dbusmenu",
0338                                      "Event");
0339 
0340   msg << action->data().toInt();
0341   msg << "clicked";
0342   msg << QVariant::fromValue(QDBusVariant(0));
0343   msg << (unsigned)QDateTime::currentDateTime().toSecsSinceEpoch();
0344 
0345   QDBusConnection::sessionBus().asyncCall(msg);
0346 }
0347 
0348 //--------------------------------------------------------------------------------
0349 // https://github.com/gnustep/libs-dbuskit/blob/master/Bundles/DBusMenu/com.canonical.dbusmenu.xml
0350 
0351 void SysTrayNotifyItem::fillMenu(QMenu &menu, const QDBusMenuLayoutItem &item)
0352 {
0353   QString type = item.m_properties.value("type", "standard").toString();
0354 
0355   if ( type == "separator" )
0356     menu.addSeparator();
0357   else if ( type == "standard" )
0358   {
0359     if ( item.m_properties.value("visible", true).toBool() )
0360     {
0361       QIcon icon;
0362 
0363       QString iconName = item.m_properties.value("icon-name").toString();
0364       if ( !iconName.isEmpty() )
0365       {
0366         QStringList origThemePaths = QIcon::themeSearchPaths();
0367 
0368         QString path = dbus->iconThemePath();
0369         if ( !path.isEmpty() )
0370         {
0371           QStringList paths = origThemePaths;
0372           paths.append(path);
0373           QIcon::setThemeSearchPaths(paths);
0374         }
0375 
0376         icon = QIcon::fromTheme(iconName);
0377 
0378         // reset to orig since this setting is application wide
0379         QIcon::setThemeSearchPaths(origThemePaths);
0380       }
0381       else
0382       {
0383         QByteArray pixmapData = item.m_properties.value("icon-data").toByteArray();
0384         if ( !pixmapData.isEmpty() )
0385         {
0386           QPixmap pixmap;
0387           pixmap.loadFromData(pixmapData, "PNG");
0388           icon = QIcon(pixmap);
0389         }
0390       }
0391 
0392       QString title, label = item.m_properties.value("label").toString();
0393       bool foundAccessKey = false;
0394       for (int i = 0; i < label.length(); i++)
0395       {
0396         if ( label[i] != '_' )
0397           title += label[i];
0398         else if ( (i < (label.length() - 1)) && (label[i + 1] == '_') )
0399         {
0400           title += '_';  // two consecutive underscore characters "__" are displayed as a single underscore
0401           i++;
0402         }
0403         else if ( i != (label.length() - 1) && !foundAccessKey )
0404         {
0405           foundAccessKey = true;
0406           title += QString('&') + label[++i];
0407         }
0408       }
0409 
0410       if ( item.m_properties.value("children-display").toString() == "submenu" )
0411       {
0412         QMenu *submenu = title.isEmpty() ? &menu : menu.addMenu(icon, title);
0413 
0414         for (const QDBusMenuLayoutItem &subItem : item.m_children)
0415         {
0416           fillMenu(*submenu, subItem);
0417 
0418           if ( submenu != &menu )
0419             menu.addMenu(submenu);
0420         }
0421       }
0422       else
0423       {
0424         QAction *action = menu.addAction(icon, title);
0425         action->setEnabled(item.m_properties.value("enabled", true).toBool());
0426 
0427         if ( item.m_properties.value("toggle-type").toString() == "checkmark" )
0428         {
0429           action->setCheckable(true);
0430           int state = item.m_properties.value("toggle-state").toInt();
0431           if ( state == 1 )
0432             action->setChecked(true);
0433         }
0434 
0435         action->setData(item.m_id);
0436       }
0437     }
0438   }
0439 }
0440 
0441 //--------------------------------------------------------------------------------