File indexing completed on 2024-04-21 05:46:39

0001 // SPDX-License-Identifier: GPL-3.0-or-later
0002 /*
0003   Copyright 2017 - 2020 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 <SysTray.hxx>
0022 #include <DBusTypes.hxx>
0023 #include <NotificationServer.hxx>
0024 #include <Network.hxx>
0025 #include <DeviceNotifier.hxx>
0026 #include <Battery.hxx>
0027 #include <Bluetooth.hxx>
0028 
0029 #ifdef WITH_PACKAGEKIT
0030 #include <PkUpdates.hxx>
0031 #endif
0032 
0033 #include <QApplication>
0034 #include <QDBusConnection>
0035 #include <QDBusMessage>
0036 #include <QDBusPendingReply>
0037 #include <QDBusMetaType>
0038 #include <QDebug>
0039 
0040 #include <KLocalizedString>
0041 
0042 #include <cmath>
0043 
0044 //--------------------------------------------------------------------------------
0045 
0046 static const QLatin1String WATCHER_SERVICE("org.kde.StatusNotifierWatcher");
0047 
0048 //--------------------------------------------------------------------------------
0049 
0050 SysTray::SysTray(DesktopPanel *parent)
0051   : QFrame(parent)
0052 {
0053   qRegisterMetaType<KDbusImageStruct>("KDbusImageStruct");
0054   qRegisterMetaType<KDbusImageVector>("KDbusImageVector");
0055 
0056   qDBusRegisterMetaType<KDbusImageStruct>();
0057   qDBusRegisterMetaType<KDbusImageVector>();
0058   qDBusRegisterMetaType<KDbusToolTipStruct>();
0059 
0060   setFrameShape(QFrame::StyledPanel);
0061   setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
0062 
0063   connect(parent, &DesktopPanel::rowsChanged, this, &SysTray::fill);
0064 
0065   QHBoxLayout *hbox = new QHBoxLayout(this);
0066   hbox->setContentsMargins(QMargins(4, 0, 4, 0));
0067 
0068   vbox = new QVBoxLayout;
0069   vbox->setContentsMargins(QMargins());
0070   vbox->setSpacing(4);
0071 
0072   QFrame *separator = new QFrame;
0073   separator->setFrameStyle(QFrame::Plain);
0074   separator->setFrameShape(QFrame::VLine);
0075 
0076   appsVbox = new QVBoxLayout;
0077   appsVbox->setContentsMargins(QMargins());
0078   appsVbox->setSpacing(4);
0079 
0080   hbox->addLayout(vbox);
0081   hbox->addWidget(separator);
0082   hbox->addLayout(appsVbox);
0083 
0084   if ( QDBusConnection::sessionBus().isConnected() )
0085   {
0086     serviceName = QString("org.kde.StatusNotifierHost-%1").arg(QApplication::applicationPid());
0087     QDBusConnection::sessionBus().registerService(serviceName);
0088 
0089     registerWatcher();
0090   }
0091 
0092   fill();
0093 }
0094 
0095 //--------------------------------------------------------------------------------
0096 
0097 void SysTray::fill()
0098 {
0099   // delete all internal widgets
0100   QLayoutItem *child;
0101   while ( (child = vbox->takeAt(0)) )
0102   {
0103     if ( child->layout() )
0104     {
0105       while ( QLayoutItem *widgetItem = child->layout()->takeAt(0) )
0106         delete widgetItem->widget();
0107     }
0108 
0109     delete child;
0110   }
0111 
0112   const int MAX_ROWS = qobject_cast<DesktopPanel *>(parentWidget())->getRows();
0113 
0114   QVector<QHBoxLayout *> rowsLayout(MAX_ROWS);
0115   for (int i = 0; i < MAX_ROWS; i++)
0116   {
0117     rowsLayout[i] = new QHBoxLayout;
0118     rowsLayout[i]->setContentsMargins(QMargins());
0119     rowsLayout[i]->setSpacing(4);
0120     vbox->addLayout(rowsLayout[i]);
0121   }
0122 
0123   QVector<QWidget *> internalWidgets =
0124   {
0125     notificationServer = new NotificationServer(this),
0126     new Network(this),
0127     new DeviceNotifier(this),
0128     new Battery(this),
0129     new Bluetooth(this),
0130 #ifdef WITH_PACKAGEKIT
0131     new PkUpdates(this)
0132 #endif
0133   };
0134 
0135   for (int i = 0; i < internalWidgets.count(); i++)
0136     rowsLayout[i % MAX_ROWS]->addWidget(internalWidgets[i], 0, Qt::AlignLeft);
0137 
0138   for (int i = 0; i < MAX_ROWS; i++)
0139     rowsLayout[i]->addStretch();
0140 
0141   // notifier items
0142 
0143   qDeleteAll(appsRows);
0144   appsRows.clear();
0145   appsRows.resize(MAX_ROWS);
0146   for (int i = 0; i < MAX_ROWS; i++)
0147   {
0148     appsRows[i] = new QHBoxLayout;
0149     appsRows[i]->setContentsMargins(QMargins());
0150     appsRows[i]->setSpacing(4);
0151     appsVbox->addLayout(appsRows[i]);
0152   }
0153 
0154   arrangeNotifyItems();
0155 }
0156 
0157 //--------------------------------------------------------------------------------
0158 
0159 void SysTray::registerWatcher()
0160 {
0161   QDBusMessage msg =
0162       QDBusMessage::createMethodCall(WATCHER_SERVICE, "/StatusNotifierWatcher",
0163                                      "org.kde.StatusNotifierWatcher",
0164                                      "RegisterStatusNotifierHost");
0165 
0166   msg << serviceName;
0167 
0168   QDBusConnection::sessionBus().send(msg);
0169 
0170   // get list of currently existing items
0171   msg = QDBusMessage::createMethodCall(WATCHER_SERVICE, "/StatusNotifierWatcher",
0172                                        "org.freedesktop.DBus.Properties",
0173                                        "Get");
0174 
0175   msg << "org.kde.StatusNotifierWatcher" << "RegisteredStatusNotifierItems";
0176   QDBusPendingCall call = QDBusConnection::sessionBus().asyncCall(msg);
0177   QDBusPendingCallWatcher *pendingCallWatcher = new QDBusPendingCallWatcher(call, this);
0178   connect(pendingCallWatcher, &QDBusPendingCallWatcher::finished, this,
0179           [this](QDBusPendingCallWatcher *w)
0180           {
0181             w->deleteLater();
0182             QDBusPendingReply<QDBusVariant> reply = *w;
0183             QStringList items = reply.value().variant().toStringList();
0184             for (const QString &item : items)
0185               itemRegistered(item);
0186           }
0187          );
0188 
0189   // connect for new items
0190   QDBusConnection::sessionBus().
0191       connect(WATCHER_SERVICE, "/StatusNotifierWatcher",
0192               "org.kde.StatusNotifierWatcher",
0193               "StatusNotifierItemRegistered",
0194               this, SLOT(itemRegistered(QString)));
0195 
0196   // connect for removed items
0197   QDBusConnection::sessionBus().
0198       connect(WATCHER_SERVICE, "/StatusNotifierWatcher",
0199               "org.kde.StatusNotifierWatcher",
0200               "StatusNotifierItemUnregistered",
0201               this, SLOT(itemUnregistered(QString)));
0202 }
0203 
0204 //--------------------------------------------------------------------------------
0205 
0206 void SysTray::itemRegistered(QString item)
0207 {
0208   //qDebug() << "itemRegistered" << item;
0209 
0210   if ( items.contains(item) )  // we have this already (e.g. try kded5 --replace)
0211     return;
0212 
0213   int slash = item.indexOf('/');
0214   if ( slash < 1 )
0215     return;
0216 
0217   QString service = item.left(slash);
0218   QString path = item.mid(slash);
0219 
0220   // create item but insert it into the layout just when it was initialized
0221   // since it might be an invalid item which has no StatusNotifierItem path
0222   // and will delete itself on error
0223   SysTrayNotifyItem *sysItem = new SysTrayNotifyItem(this, service, path);
0224   sysItem->setObjectName(item);
0225   sysItem->setFixedSize(QSize(22, 22));
0226   sysItem->hide();  // until initialized
0227 
0228   connect(sysItem, &SysTrayNotifyItem::initialized, this, &SysTray::itemInitialized);
0229 }
0230 
0231 //--------------------------------------------------------------------------------
0232 
0233 void SysTray::itemInitialized(SysTrayNotifyItem *item)
0234 {
0235   if ( !items.contains(item->objectName()) )
0236     items.insert(item->objectName(), item);
0237 
0238   arrangeNotifyItems();  // show/hide icons
0239 }
0240 
0241 //--------------------------------------------------------------------------------
0242 
0243 void SysTray::itemUnregistered(QString item)
0244 {
0245   //qDebug() << "itemUnregistered" << item;
0246 
0247   if ( items.contains(item) )
0248   {
0249     delete items.take(item);
0250     arrangeNotifyItems();
0251   }
0252 }
0253 
0254 //--------------------------------------------------------------------------------
0255 
0256 void SysTray::arrangeNotifyItems()
0257 {
0258   // cut all items from all layouts
0259   foreach (QHBoxLayout *row, appsRows)
0260   {
0261     while ( QLayoutItem *item = row->itemAt(0) )
0262     {
0263       if ( item->widget() )
0264         row->removeWidget(item->widget());  // take widget out but do not delete it
0265       else
0266         delete row->takeAt(0);  // spacer items
0267     }
0268   }
0269 
0270   // rearrange visible items - "row first" layout
0271   int visibleItems = 0;
0272   foreach (SysTrayNotifyItem *item, items)
0273     if ( item && item->isVisible() )
0274       visibleItems++;
0275 
0276   const int MAX_COLUMNS = static_cast<int>(std::ceil(visibleItems / float(appsRows.count())));
0277   int row = 0;
0278   foreach (SysTrayNotifyItem *item, items)
0279   {
0280     if ( item && item->isVisible() )
0281     {
0282       appsRows[row]->addWidget(item, 0, Qt::AlignLeft);
0283       if ( appsRows[row]->count() == MAX_COLUMNS )
0284         row++;
0285     }
0286   }
0287 
0288   foreach (QHBoxLayout *row, appsRows)
0289     row->addStretch();
0290 }
0291 
0292 //--------------------------------------------------------------------------------
0293 
0294 void SysTray::contextMenuEvent(QContextMenuEvent *event)
0295 {
0296   Q_UNUSED(event)
0297 
0298   // allow to disable notification popups (e.g. during a presentation)
0299   if ( notificationServer )
0300   {
0301     QMenu menu;
0302 
0303     QAction *action = menu.addAction(i18n("Show New Notifications"));
0304     action->setCheckable(true);
0305     action->setChecked(!notificationServer->getAvoidPopup());
0306 
0307     connect(action, &QAction::triggered,
0308             [this](bool checked)
0309             {
0310               notificationServer->setAvoidPopup(!checked);
0311             });
0312 
0313     menu.exec(QCursor::pos());
0314   }
0315 }
0316 
0317 //--------------------------------------------------------------------------------