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