1 /*
2     SPDX-FileCopyrightText: 2020 Konrad Materka <materka@gmail.com>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "systemtraymodel.h"
8 #include "debug.h"
9 
10 #include "plasmoidregistry.h"
11 #include "statusnotifieritemhost.h"
12 #include "statusnotifieritemsource.h"
13 #include "systemtraysettings.h"
14 
15 #include <KLocalizedString>
16 #include <Plasma/Applet>
17 #include <Plasma/DataContainer>
18 #include <Plasma/Service>
19 #include <PluginLoader>
20 
21 #include <QIcon>
22 #include <QQuickItem>
23 
BaseModel(QPointer<SystemTraySettings> settings,QObject * parent)24 BaseModel::BaseModel(QPointer<SystemTraySettings> settings, QObject *parent)
25     : QAbstractListModel(parent)
26     , m_settings(settings)
27     , m_showAllItems(m_settings->isShowAllItems())
28     , m_shownItems(m_settings->shownItems())
29     , m_hiddenItems(m_settings->hiddenItems())
30 {
31     connect(m_settings, &SystemTraySettings::configurationChanged, this, &BaseModel::onConfigurationChanged);
32 }
33 
roleNames() const34 QHash<int, QByteArray> BaseModel::roleNames() const
35 {
36     return {
37         {Qt::DisplayRole, QByteArrayLiteral("display")},
38         {Qt::DecorationRole, QByteArrayLiteral("decoration")},
39         {static_cast<int>(BaseRole::ItemType), QByteArrayLiteral("itemType")},
40         {static_cast<int>(BaseRole::ItemId), QByteArrayLiteral("itemId")},
41         {static_cast<int>(BaseRole::CanRender), QByteArrayLiteral("canRender")},
42         {static_cast<int>(BaseRole::Category), QByteArrayLiteral("category")},
43         {static_cast<int>(BaseRole::Status), QByteArrayLiteral("status")},
44         {static_cast<int>(BaseRole::EffectiveStatus), QByteArrayLiteral("effectiveStatus")},
45     };
46 }
47 
onConfigurationChanged()48 void BaseModel::onConfigurationChanged()
49 {
50     m_showAllItems = m_settings->isShowAllItems();
51     m_shownItems = m_settings->shownItems();
52     m_hiddenItems = m_settings->hiddenItems();
53 
54     Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {static_cast<int>(BaseModel::BaseRole::EffectiveStatus)});
55 }
56 
calculateEffectiveStatus(bool canRender,Plasma::Types::ItemStatus status,QString itemId) const57 Plasma::Types::ItemStatus BaseModel::calculateEffectiveStatus(bool canRender, Plasma::Types::ItemStatus status, QString itemId) const
58 {
59     if (!canRender) {
60         return Plasma::Types::ItemStatus::HiddenStatus;
61     }
62 
63     if (status == Plasma::Types::ItemStatus::HiddenStatus) {
64         return Plasma::Types::ItemStatus::HiddenStatus;
65     }
66 
67     bool forcedShown = m_showAllItems || m_shownItems.contains(itemId);
68     bool forcedHidden = m_hiddenItems.contains(itemId);
69 
70     if (forcedShown || (!forcedHidden && status != Plasma::Types::ItemStatus::PassiveStatus)) {
71         return Plasma::Types::ItemStatus::ActiveStatus;
72     } else {
73         return Plasma::Types::ItemStatus::PassiveStatus;
74     }
75 }
76 
plasmoidCategoryForMetadata(const KPluginMetaData & metadata)77 static QString plasmoidCategoryForMetadata(const KPluginMetaData &metadata)
78 {
79     QString category = QStringLiteral("UnknownCategory");
80 
81     if (metadata.isValid()) {
82         const QString notificationAreaCategory = metadata.value(QStringLiteral("X-Plasma-NotificationAreaCategory"));
83         if (!notificationAreaCategory.isEmpty()) {
84             category = notificationAreaCategory;
85         }
86     }
87 
88     return category;
89 }
90 
PlasmoidModel(QPointer<SystemTraySettings> settings,QPointer<PlasmoidRegistry> plasmoidRegistry,QObject * parent)91 PlasmoidModel::PlasmoidModel(QPointer<SystemTraySettings> settings, QPointer<PlasmoidRegistry> plasmoidRegistry, QObject *parent)
92     : BaseModel(settings, parent)
93     , m_plasmoidRegistry(plasmoidRegistry)
94 {
95     connect(m_plasmoidRegistry, &PlasmoidRegistry::pluginRegistered, this, &PlasmoidModel::appendRow);
96     connect(m_plasmoidRegistry, &PlasmoidRegistry::pluginUnregistered, this, &PlasmoidModel::removeRow);
97 
98     const auto appletMetaDataList = m_plasmoidRegistry->systemTrayApplets();
99     for (const auto &info : appletMetaDataList) {
100         if (!info.isValid() || info.value(QStringLiteral("X-Plasma-NotificationArea")) != "true") {
101             continue;
102         }
103         appendRow(info);
104     }
105 }
106 
data(const QModelIndex & index,int role) const107 QVariant PlasmoidModel::data(const QModelIndex &index, int role) const
108 {
109     if (!checkIndex(index, CheckIndexOption::IndexIsValid)) {
110         return QVariant();
111     }
112 
113     const PlasmoidModel::Item &item = m_items[index.row()];
114     const KPluginMetaData &pluginMetaData = item.pluginMetaData;
115     const Plasma::Applet *applet = item.applet;
116 
117     if (role <= Qt::UserRole) {
118         switch (role) {
119         case Qt::DisplayRole: {
120             const QString dbusactivation = pluginMetaData.value(QStringLiteral("X-Plasma-DBusActivationService"));
121             if (dbusactivation.isEmpty()) {
122                 return pluginMetaData.name();
123             } else {
124                 return i18nc("Suffix added to the applet name if the applet is autoloaded via DBus activation", "%1 (Automatic load)", pluginMetaData.name());
125             }
126         }
127         case Qt::DecorationRole: {
128             QIcon icon = QIcon::fromTheme(pluginMetaData.iconName());
129             return icon.isNull() ? QVariant() : icon;
130         }
131         default:
132             return QVariant();
133         }
134     }
135 
136     if (role < static_cast<int>(Role::Applet)) {
137         Plasma::Types::ItemStatus status = Plasma::Types::ItemStatus::UnknownStatus;
138         if (applet) {
139             status = applet->status();
140         }
141 
142         switch (static_cast<BaseRole>(role)) {
143         case BaseRole::ItemType:
144             return QStringLiteral("Plasmoid");
145         case BaseRole::ItemId:
146             return pluginMetaData.pluginId();
147         case BaseRole::CanRender:
148             return applet != nullptr;
149         case BaseRole::Category:
150             return plasmoidCategoryForMetadata(pluginMetaData);
151         case BaseRole::Status:
152             return status;
153         case BaseRole::EffectiveStatus:
154             return calculateEffectiveStatus(applet != nullptr, status, pluginMetaData.pluginId());
155         default:
156             return QVariant();
157         }
158     }
159 
160     switch (static_cast<Role>(role)) {
161     case Role::Applet:
162         return applet ? applet->property("_plasma_graphicObject") : QVariant();
163     case Role::HasApplet:
164         return applet != nullptr;
165     default:
166         return QVariant();
167     }
168 }
169 
rowCount(const QModelIndex & parent) const170 int PlasmoidModel::rowCount(const QModelIndex &parent) const
171 {
172     return parent.isValid() ? 0 : m_items.size();
173 }
174 
roleNames() const175 QHash<int, QByteArray> PlasmoidModel::roleNames() const
176 {
177     QHash<int, QByteArray> roles = BaseModel::roleNames();
178 
179     roles.insert(static_cast<int>(Role::Applet), QByteArrayLiteral("applet"));
180     roles.insert(static_cast<int>(Role::HasApplet), QByteArrayLiteral("hasApplet"));
181 
182     return roles;
183 }
184 
addApplet(Plasma::Applet * applet)185 void PlasmoidModel::addApplet(Plasma::Applet *applet)
186 {
187     auto pluginMetaData = applet->pluginMetaData();
188 
189     int idx = indexOfPluginId(pluginMetaData.pluginId());
190 
191     if (idx < 0) {
192         idx = rowCount();
193         appendRow(pluginMetaData);
194     }
195 
196     m_items[idx].applet = applet;
197     connect(applet, &Plasma::Applet::statusChanged, this, [this, applet](Plasma::Types::ItemStatus status) {
198         Q_UNUSED(status)
199         int idx = indexOfPluginId(applet->pluginMetaData().pluginId());
200         Q_EMIT dataChanged(index(idx, 0), index(idx, 0), {static_cast<int>(BaseRole::Status)});
201     });
202 
203     Q_EMIT dataChanged(index(idx, 0), index(idx, 0));
204 }
205 
removeApplet(Plasma::Applet * applet)206 void PlasmoidModel::removeApplet(Plasma::Applet *applet)
207 {
208     int idx = indexOfPluginId(applet->pluginMetaData().pluginId());
209     if (idx >= 0) {
210         m_items[idx].applet = nullptr;
211         Q_EMIT dataChanged(index(idx, 0), index(idx, 0));
212         applet->disconnect(this);
213     }
214 }
215 
appendRow(const KPluginMetaData & pluginMetaData)216 void PlasmoidModel::appendRow(const KPluginMetaData &pluginMetaData)
217 {
218     int idx = rowCount();
219     beginInsertRows(QModelIndex(), idx, idx);
220 
221     PlasmoidModel::Item item;
222     item.pluginMetaData = pluginMetaData;
223     m_items.append(item);
224 
225     endInsertRows();
226 }
227 
removeRow(const QString & pluginId)228 void PlasmoidModel::removeRow(const QString &pluginId)
229 {
230     int idx = indexOfPluginId(pluginId);
231     beginRemoveRows(QModelIndex(), idx, idx);
232     m_items.removeAt(idx);
233     endRemoveRows();
234 }
235 
indexOfPluginId(const QString & pluginId) const236 int PlasmoidModel::indexOfPluginId(const QString &pluginId) const
237 {
238     for (int i = 0; i < rowCount(); i++) {
239         if (m_items[i].pluginMetaData.pluginId() == pluginId) {
240             return i;
241         }
242     }
243     return -1;
244 }
245 
StatusNotifierModel(QPointer<SystemTraySettings> settings,QObject * parent)246 StatusNotifierModel::StatusNotifierModel(QPointer<SystemTraySettings> settings, QObject *parent)
247     : BaseModel(settings, parent)
248 {
249     m_sniHost = StatusNotifierItemHost::self();
250 
251     connect(m_sniHost, &StatusNotifierItemHost::itemAdded, this, &StatusNotifierModel::addSource);
252     connect(m_sniHost, &StatusNotifierItemHost::itemRemoved, this, &StatusNotifierModel::removeSource);
253 
254     for (auto service : m_sniHost->services()) {
255         addSource(service);
256     }
257 }
258 
extractStatus(const StatusNotifierItemSource * sniData)259 static Plasma::Types::ItemStatus extractStatus(const StatusNotifierItemSource *sniData)
260 {
261     QString status = sniData->status();
262     if (status == QLatin1String("Active")) {
263         return Plasma::Types::ItemStatus::ActiveStatus;
264     } else if (status == QLatin1String("NeedsAttention")) {
265         return Plasma::Types::ItemStatus::NeedsAttentionStatus;
266     } else if (status == QLatin1String("Passive")) {
267         return Plasma::Types::ItemStatus::PassiveStatus;
268     } else {
269         return Plasma::Types::ItemStatus::UnknownStatus;
270     }
271 }
272 
extractIcon(const QIcon & icon,const QVariant & defaultValue=QVariant ())273 static QVariant extractIcon(const QIcon &icon, const QVariant &defaultValue = QVariant())
274 {
275     if (!icon.isNull()) {
276         return icon;
277     } else {
278         return defaultValue;
279     }
280 }
281 
extractItemId(const StatusNotifierItemSource * sniData)282 static QString extractItemId(const StatusNotifierItemSource *sniData)
283 {
284     const QString itemId = sniData->id();
285     // Bug 378910: workaround for Dropbox not following the SNI specification
286     if (itemId.startsWith(QLatin1String("dropbox-client-"))) {
287         return QLatin1String("dropbox-client-PID");
288     } else {
289         return itemId;
290     }
291 }
292 
data(const QModelIndex & index,int role) const293 QVariant StatusNotifierModel::data(const QModelIndex &index, int role) const
294 {
295     if (!checkIndex(index, CheckIndexOption::IndexIsValid)) {
296         return QVariant();
297     }
298 
299     StatusNotifierModel::Item item = m_items[index.row()];
300     StatusNotifierItemSource *sniData = m_sniHost->itemForService(item.source);
301 
302     const QString itemId = extractItemId(sniData);
303 
304     if (role <= Qt::UserRole) {
305         switch (role) {
306         case Qt::DisplayRole:
307             return sniData->title();
308         case Qt::DecorationRole:
309             return extractIcon(sniData->icon(), sniData->iconName());
310         default:
311             return QVariant();
312         }
313     }
314 
315     if (role < static_cast<int>(Role::DataEngineSource)) {
316         switch (static_cast<BaseRole>(role)) {
317         case BaseRole::ItemType:
318             return QStringLiteral("StatusNotifier");
319         case BaseRole::ItemId:
320             return itemId;
321         case BaseRole::CanRender:
322             return true;
323         case BaseRole::Category: {
324             QVariant category = sniData->category();
325             return category.isNull() ? QStringLiteral("UnknownCategory") : sniData->category();
326         }
327         case BaseRole::Status:
328             return extractStatus(sniData);
329         case BaseRole::EffectiveStatus:
330             return calculateEffectiveStatus(true, extractStatus(sniData), itemId);
331         default:
332             return QVariant();
333         }
334     }
335 
336     switch (static_cast<Role>(role)) {
337     case Role::DataEngineSource:
338         return item.source;
339     case Role::Service:
340         return QVariant::fromValue(item.service);
341     case Role::AttentionIcon:
342         return extractIcon(sniData->attentionIcon());
343     case Role::AttentionIconName:
344         return sniData->attentionIconName();
345     case Role::AttentionMovieName:
346         return sniData->attentionMovieName();
347     case Role::Category:
348         return sniData->category();
349     case Role::Icon:
350         return extractIcon(sniData->icon());
351     case Role::IconName:
352         return sniData->iconName();
353     case Role::IconThemePath:
354         return sniData->iconThemePath();
355     case Role::Id:
356         return itemId;
357     case Role::ItemIsMenu:
358         return sniData->itemIsMenu();
359     case Role::OverlayIconName:
360         return sniData->overlayIconName();
361     case Role::Status:
362         return extractStatus(sniData);
363     case Role::Title:
364         return sniData->title();
365     case Role::ToolTipSubTitle:
366         return sniData->toolTipSubTitle();
367     case Role::ToolTipTitle:
368         return sniData->toolTipTitle();
369     case Role::WindowId:
370         return sniData->windowId();
371     default:
372         return QVariant();
373     }
374 }
375 
rowCount(const QModelIndex & parent) const376 int StatusNotifierModel::rowCount(const QModelIndex &parent) const
377 {
378     return parent.isValid() ? 0 : m_items.size();
379 }
380 
roleNames() const381 QHash<int, QByteArray> StatusNotifierModel::roleNames() const
382 {
383     QHash<int, QByteArray> roles = BaseModel::roleNames();
384 
385     roles.insert(static_cast<int>(Role::DataEngineSource), QByteArrayLiteral("DataEngineSource"));
386     roles.insert(static_cast<int>(Role::Service), QByteArrayLiteral("Service"));
387     roles.insert(static_cast<int>(Role::AttentionIcon), QByteArrayLiteral("AttentionIcon"));
388     roles.insert(static_cast<int>(Role::AttentionIconName), QByteArrayLiteral("AttentionIconName"));
389     roles.insert(static_cast<int>(Role::AttentionMovieName), QByteArrayLiteral("AttentionMovieName"));
390     roles.insert(static_cast<int>(Role::Category), QByteArrayLiteral("Category"));
391     roles.insert(static_cast<int>(Role::Icon), QByteArrayLiteral("Icon"));
392     roles.insert(static_cast<int>(Role::IconName), QByteArrayLiteral("IconName"));
393     roles.insert(static_cast<int>(Role::IconThemePath), QByteArrayLiteral("IconThemePath"));
394     roles.insert(static_cast<int>(Role::Id), QByteArrayLiteral("Id"));
395     roles.insert(static_cast<int>(Role::ItemIsMenu), QByteArrayLiteral("ItemIsMenu"));
396     roles.insert(static_cast<int>(Role::OverlayIconName), QByteArrayLiteral("OverlayIconName"));
397     roles.insert(static_cast<int>(Role::Status), QByteArrayLiteral("Status"));
398     roles.insert(static_cast<int>(Role::Title), QByteArrayLiteral("Title"));
399     roles.insert(static_cast<int>(Role::ToolTipSubTitle), QByteArrayLiteral("ToolTipSubTitle"));
400     roles.insert(static_cast<int>(Role::ToolTipTitle), QByteArrayLiteral("ToolTipTitle"));
401     roles.insert(static_cast<int>(Role::WindowId), QByteArrayLiteral("WindowId"));
402 
403     return roles;
404 }
405 
addSource(const QString & source)406 void StatusNotifierModel::addSource(const QString &source)
407 {
408     int count = rowCount();
409     beginInsertRows(QModelIndex(), count, count);
410 
411     StatusNotifierModel::Item item;
412     item.source = source;
413 
414     StatusNotifierItemSource *sni = m_sniHost->itemForService(source);
415     connect(sni, &StatusNotifierItemSource::dataUpdated, this, [=]() {
416         dataUpdated(source);
417     });
418     item.service = sni->createService();
419     m_items.append(item);
420     endInsertRows();
421 }
422 
removeSource(const QString & source)423 void StatusNotifierModel::removeSource(const QString &source)
424 {
425     int idx = indexOfSource(source);
426     if (idx >= 0) {
427         beginRemoveRows(QModelIndex(), idx, idx);
428         delete m_items[idx].service;
429         m_items.removeAt(idx);
430         endRemoveRows();
431     }
432 }
433 
dataUpdated(const QString & sourceName)434 void StatusNotifierModel::dataUpdated(const QString &sourceName)
435 {
436     int idx = indexOfSource(sourceName);
437 
438     if (idx >= 0) {
439         Q_EMIT dataChanged(index(idx, 0), index(idx, 0));
440     }
441 }
442 
indexOfSource(const QString & source) const443 int StatusNotifierModel::indexOfSource(const QString &source) const
444 {
445     for (int i = 0; i < rowCount(); i++) {
446         if (m_items[i].source == source) {
447             return i;
448         }
449     }
450     return -1;
451 }
452 
SystemTrayModel(QObject * parent)453 SystemTrayModel::SystemTrayModel(QObject *parent)
454     : QConcatenateTablesProxyModel(parent)
455 {
456     m_roleNames = QConcatenateTablesProxyModel::roleNames();
457 }
458 
roleNames() const459 QHash<int, QByteArray> SystemTrayModel::roleNames() const
460 {
461     return m_roleNames;
462 }
463 
addSourceModel(QAbstractItemModel * sourceModel)464 void SystemTrayModel::addSourceModel(QAbstractItemModel *sourceModel)
465 {
466     QHashIterator<int, QByteArray> it(sourceModel->roleNames());
467     while (it.hasNext()) {
468         it.next();
469 
470         if (!m_roleNames.contains(it.key())) {
471             m_roleNames.insert(it.key(), it.value());
472         }
473     }
474 
475     QConcatenateTablesProxyModel::addSourceModel(sourceModel);
476 }
477