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