File indexing completed on 2024-05-12 05:37:10

0001 /*
0002     SPDX-FileCopyrightText: 2020 Konrad Materka <materka@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "systemtraymodel.h"
0008 #include "debug.h"
0009 
0010 #include "plasmoidregistry.h"
0011 #include "statusnotifieritemhost.h"
0012 #include "statusnotifieritemsource.h"
0013 #include "systemtraysettings.h"
0014 
0015 #include <KLocalizedString>
0016 #include <Plasma/Applet>
0017 #include <Plasma5Support/DataContainer>
0018 #include <PlasmaQuick/AppletQuickItem>
0019 
0020 #include <QIcon>
0021 #include <QQuickItem>
0022 
0023 BaseModel::BaseModel(QPointer<SystemTraySettings> settings, QObject *parent)
0024     : QAbstractListModel(parent)
0025     , m_settings(settings)
0026     , m_showAllItems(m_settings ? m_settings->isShowAllItems() : true)
0027     , m_shownItems(m_settings ? m_settings->shownItems() : decltype(m_shownItems){})
0028     , m_hiddenItems(m_settings ? m_settings->hiddenItems() : decltype(m_hiddenItems){})
0029 {
0030     if (m_settings) {
0031         connect(m_settings, &SystemTraySettings::configurationChanged, this, &BaseModel::onConfigurationChanged);
0032     }
0033 }
0034 
0035 QHash<int, QByteArray> BaseModel::roleNames() const
0036 {
0037     return {
0038         {Qt::DisplayRole, QByteArrayLiteral("display")},
0039         {Qt::DecorationRole, QByteArrayLiteral("decoration")},
0040         {static_cast<int>(BaseRole::ItemType), QByteArrayLiteral("itemType")},
0041         {static_cast<int>(BaseRole::ItemId), QByteArrayLiteral("itemId")},
0042         {static_cast<int>(BaseRole::CanRender), QByteArrayLiteral("canRender")},
0043         {static_cast<int>(BaseRole::Category), QByteArrayLiteral("category")},
0044         {static_cast<int>(BaseRole::Status), QByteArrayLiteral("status")},
0045         {static_cast<int>(BaseRole::EffectiveStatus), QByteArrayLiteral("effectiveStatus")},
0046     };
0047 }
0048 
0049 void BaseModel::onConfigurationChanged()
0050 {
0051     m_showAllItems = m_settings->isShowAllItems();
0052     m_shownItems = m_settings->shownItems();
0053     m_hiddenItems = m_settings->hiddenItems();
0054 
0055     if (rowCount() == 0) {
0056         return; // Avoid assertion
0057     }
0058 
0059     Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {static_cast<int>(BaseModel::BaseRole::EffectiveStatus)});
0060 }
0061 
0062 Plasma::Types::ItemStatus BaseModel::calculateEffectiveStatus(bool canRender, Plasma::Types::ItemStatus status, QString itemId) const
0063 {
0064     if (!canRender) {
0065         return Plasma::Types::ItemStatus::HiddenStatus;
0066     }
0067 
0068     bool forcedShown = m_showAllItems || m_shownItems.contains(itemId);
0069     bool forcedHidden = m_hiddenItems.contains(itemId);
0070 
0071     if (!forcedShown && status == Plasma::Types::ItemStatus::HiddenStatus) {
0072         return Plasma::Types::ItemStatus::HiddenStatus;
0073     } else if (forcedShown || (!forcedHidden && status != Plasma::Types::ItemStatus::PassiveStatus)) {
0074         return Plasma::Types::ItemStatus::ActiveStatus;
0075     } else {
0076         return Plasma::Types::ItemStatus::PassiveStatus;
0077     }
0078 }
0079 
0080 static QString plasmoidCategoryForMetadata(const KPluginMetaData &metadata)
0081 {
0082     Q_ASSERT(metadata.isValid());
0083     return metadata.value(QStringLiteral("X-Plasma-NotificationAreaCategory"));
0084 }
0085 
0086 PlasmoidModel::PlasmoidModel(const QPointer<SystemTraySettings> &settings, const QPointer<PlasmoidRegistry> &plasmoidRegistry, QObject *parent)
0087     : BaseModel(settings, parent)
0088     , m_plasmoidRegistry(plasmoidRegistry)
0089 {
0090     connect(m_plasmoidRegistry, &PlasmoidRegistry::pluginRegistered, this, &PlasmoidModel::appendRow);
0091     connect(m_plasmoidRegistry, &PlasmoidRegistry::pluginUnregistered, this, &PlasmoidModel::removeRow);
0092 
0093     const auto appletMetaDataList = m_plasmoidRegistry->systemTrayApplets();
0094     for (const auto &info : appletMetaDataList) {
0095         if (!info.isValid() || info.value(QStringLiteral("X-Plasma-NotificationAreaCategory")).isEmpty()) {
0096             continue;
0097         }
0098         appendRow(info);
0099     }
0100 }
0101 
0102 QVariant PlasmoidModel::data(const QModelIndex &index, int role) const
0103 {
0104     if (!checkIndex(index, CheckIndexOption::IndexIsValid)) {
0105         return QVariant();
0106     }
0107 
0108     const PlasmoidModel::Item &item = m_items[index.row()];
0109     const KPluginMetaData &pluginMetaData = item.pluginMetaData;
0110     Plasma::Applet *applet = item.applet;
0111 
0112     if (role <= Qt::UserRole) {
0113         switch (role) {
0114         case Qt::DisplayRole: {
0115             return pluginMetaData.name();
0116         }
0117         case Qt::DecorationRole: {
0118             QIcon icon = QIcon::fromTheme(applet ? applet->icon() : "", QIcon::fromTheme(pluginMetaData.iconName()));
0119             return icon.isNull() ? QVariant() : icon;
0120         }
0121         default:
0122             return QVariant();
0123         }
0124     }
0125 
0126     if (role < static_cast<int>(Role::Applet)) {
0127         Plasma::Types::ItemStatus status = Plasma::Types::ItemStatus::UnknownStatus;
0128         if (applet) {
0129             status = applet->status();
0130         }
0131 
0132         switch (static_cast<BaseRole>(role)) {
0133         case BaseRole::ItemType:
0134             return QStringLiteral("Plasmoid");
0135         case BaseRole::ItemId:
0136             return pluginMetaData.pluginId();
0137         case BaseRole::CanRender:
0138             return applet != nullptr;
0139         case BaseRole::Category:
0140             return plasmoidCategoryForMetadata(pluginMetaData);
0141         case BaseRole::Status:
0142             return status;
0143         case BaseRole::EffectiveStatus:
0144             return calculateEffectiveStatus(applet != nullptr, status, pluginMetaData.pluginId());
0145         default:
0146             return QVariant();
0147         }
0148     }
0149 
0150     switch (static_cast<Role>(role)) {
0151     case Role::Applet:
0152         return applet ? QVariant::fromValue(PlasmaQuick::AppletQuickItem::itemForApplet(applet)) : QVariant();
0153     case Role::HasApplet:
0154         return applet != nullptr;
0155     default:
0156         return QVariant();
0157     }
0158 }
0159 
0160 int PlasmoidModel::rowCount(const QModelIndex &parent) const
0161 {
0162     return parent.isValid() ? 0 : m_items.size();
0163 }
0164 
0165 QHash<int, QByteArray> PlasmoidModel::roleNames() const
0166 {
0167     QHash<int, QByteArray> roles = BaseModel::roleNames();
0168 
0169     roles.insert(static_cast<int>(Role::Applet), QByteArrayLiteral("applet"));
0170     roles.insert(static_cast<int>(Role::HasApplet), QByteArrayLiteral("hasApplet"));
0171 
0172     return roles;
0173 }
0174 
0175 void PlasmoidModel::addApplet(Plasma::Applet *applet)
0176 {
0177     auto pluginMetaData = applet->pluginMetaData();
0178 
0179     int idx = indexOfPluginId(pluginMetaData.pluginId());
0180 
0181     if (idx < 0) {
0182         idx = rowCount();
0183         appendRow(pluginMetaData);
0184     }
0185 
0186     m_items[idx].applet = applet;
0187     connect(applet, &Plasma::Applet::statusChanged, this, [this, applet](Plasma::Types::ItemStatus status) {
0188         Q_UNUSED(status)
0189         int idx = indexOfPluginId(applet->pluginMetaData().pluginId());
0190         Q_EMIT dataChanged(index(idx, 0), index(idx, 0), {static_cast<int>(BaseRole::Status)});
0191     });
0192 
0193     Q_EMIT dataChanged(index(idx, 0), index(idx, 0));
0194 }
0195 
0196 void PlasmoidModel::removeApplet(Plasma::Applet *applet)
0197 {
0198     int idx = indexOfPluginId(applet->pluginMetaData().pluginId());
0199     if (idx >= 0) {
0200         m_items[idx].applet = nullptr;
0201         Q_EMIT dataChanged(index(idx, 0), index(idx, 0));
0202         applet->disconnect(this);
0203     }
0204 }
0205 
0206 void PlasmoidModel::appendRow(const KPluginMetaData &pluginMetaData)
0207 {
0208     int idx = rowCount();
0209     beginInsertRows(QModelIndex(), idx, idx);
0210 
0211     PlasmoidModel::Item item;
0212     item.pluginMetaData = pluginMetaData;
0213     m_items.append(item);
0214 
0215     endInsertRows();
0216 }
0217 
0218 void PlasmoidModel::removeRow(const QString &pluginId)
0219 {
0220     int idx = indexOfPluginId(pluginId);
0221     beginRemoveRows(QModelIndex(), idx, idx);
0222     m_items.removeAt(idx);
0223     endRemoveRows();
0224 }
0225 
0226 int PlasmoidModel::indexOfPluginId(const QString &pluginId) const
0227 {
0228     for (int i = 0; i < rowCount(); i++) {
0229         if (m_items[i].pluginMetaData.pluginId() == pluginId) {
0230             return i;
0231         }
0232     }
0233     return -1;
0234 }
0235 
0236 StatusNotifierModel::StatusNotifierModel(QObject *parent)
0237     : BaseModel(nullptr, parent)
0238 {
0239     init();
0240 }
0241 
0242 StatusNotifierModel::StatusNotifierModel(QPointer<SystemTraySettings> settings, QObject *parent)
0243     : BaseModel(settings, parent)
0244 {
0245     init();
0246 }
0247 
0248 static Plasma::Types::ItemStatus extractStatus(const StatusNotifierItemSource *sniData)
0249 {
0250     QString status = sniData->status();
0251     if (status == QLatin1String("Active")) {
0252         return Plasma::Types::ItemStatus::ActiveStatus;
0253     } else if (status == QLatin1String("NeedsAttention")) {
0254         return Plasma::Types::ItemStatus::NeedsAttentionStatus;
0255     } else if (status == QLatin1String("Passive")) {
0256         return Plasma::Types::ItemStatus::PassiveStatus;
0257     } else {
0258         return Plasma::Types::ItemStatus::UnknownStatus;
0259     }
0260 }
0261 
0262 static QVariant extractIcon(const QIcon &icon, const QVariant &defaultValue = QVariant())
0263 {
0264     if (!icon.isNull()) {
0265         return icon;
0266     } else {
0267         return defaultValue;
0268     }
0269 }
0270 
0271 static QString extractItemId(const StatusNotifierItemSource *sniData)
0272 {
0273     const QString itemId = sniData->id();
0274     // Bug 378910: workaround for Dropbox not following the SNI specification
0275     if (itemId.startsWith(QLatin1String("dropbox-client-"))) {
0276         return QLatin1String("dropbox-client-PID");
0277     } else {
0278         return itemId;
0279     }
0280 }
0281 
0282 QVariant StatusNotifierModel::data(const QModelIndex &index, int role) const
0283 {
0284     if (!checkIndex(index, CheckIndexOption::IndexIsValid)) {
0285         return QVariant();
0286     }
0287 
0288     const StatusNotifierModel::Item &item = m_items[index.row()];
0289     StatusNotifierItemSource *sniData = m_sniHost->itemForService(item.source);
0290 
0291     if (role <= Qt::UserRole) {
0292         switch (role) {
0293         case Qt::DisplayRole:
0294             return sniData->title();
0295         case Qt::DecorationRole:
0296             return extractIcon(sniData->icon(), sniData->iconName());
0297         default:
0298             return QVariant();
0299         }
0300     }
0301 
0302     const QString itemId = extractItemId(sniData);
0303 
0304     if (role < static_cast<int>(Role::DataEngineSource)) {
0305         switch (static_cast<BaseRole>(role)) {
0306         case BaseRole::ItemType:
0307             return QStringLiteral("StatusNotifier");
0308         case BaseRole::ItemId:
0309             return itemId;
0310         case BaseRole::CanRender:
0311             return true;
0312         case BaseRole::Category: {
0313             QVariant category = sniData->category();
0314             return category.isNull() ? QStringLiteral("UnknownCategory") : sniData->category();
0315         }
0316         case BaseRole::Status:
0317             return extractStatus(sniData);
0318         case BaseRole::EffectiveStatus:
0319             return calculateEffectiveStatus(true, extractStatus(sniData), itemId);
0320         default:
0321             return QVariant();
0322         }
0323     }
0324 
0325     switch (static_cast<Role>(role)) {
0326     case Role::DataEngineSource:
0327         return item.source;
0328     case Role::Service:
0329         return QVariant::fromValue(item.service);
0330     case Role::AttentionIcon:
0331         return extractIcon(sniData->attentionIcon());
0332     case Role::AttentionIconName:
0333         return sniData->attentionIconName();
0334     case Role::AttentionMovieName:
0335         return sniData->attentionMovieName();
0336     case Role::Category:
0337         return sniData->category();
0338     case Role::Icon:
0339         return extractIcon(sniData->icon());
0340     case Role::IconName:
0341         return sniData->iconName();
0342     case Role::IconThemePath:
0343         return sniData->iconThemePath();
0344     case Role::Id:
0345         return itemId;
0346     case Role::ItemIsMenu:
0347         return sniData->itemIsMenu();
0348     case Role::OverlayIconName:
0349         return sniData->overlayIconName();
0350     case Role::Status:
0351         return extractStatus(sniData);
0352     case Role::Title:
0353         return sniData->title();
0354     case Role::ToolTipSubTitle:
0355         return sniData->toolTipSubTitle();
0356     case Role::ToolTipTitle:
0357         return sniData->toolTipTitle();
0358     case Role::WindowId:
0359         return sniData->windowId();
0360     default:
0361         return QVariant();
0362     }
0363 }
0364 
0365 int StatusNotifierModel::rowCount(const QModelIndex &parent) const
0366 {
0367     return parent.isValid() ? 0 : m_items.size();
0368 }
0369 
0370 QHash<int, QByteArray> StatusNotifierModel::roleNames() const
0371 {
0372     QHash<int, QByteArray> roles = BaseModel::roleNames();
0373 
0374     roles.insert(static_cast<int>(Role::DataEngineSource), QByteArrayLiteral("DataEngineSource"));
0375     roles.insert(static_cast<int>(Role::Service), QByteArrayLiteral("Service"));
0376     roles.insert(static_cast<int>(Role::AttentionIcon), QByteArrayLiteral("AttentionIcon"));
0377     roles.insert(static_cast<int>(Role::AttentionIconName), QByteArrayLiteral("AttentionIconName"));
0378     roles.insert(static_cast<int>(Role::AttentionMovieName), QByteArrayLiteral("AttentionMovieName"));
0379     roles.insert(static_cast<int>(Role::Category), QByteArrayLiteral("Category"));
0380     roles.insert(static_cast<int>(Role::Icon), QByteArrayLiteral("Icon"));
0381     roles.insert(static_cast<int>(Role::IconName), QByteArrayLiteral("IconName"));
0382     roles.insert(static_cast<int>(Role::IconThemePath), QByteArrayLiteral("IconThemePath"));
0383     roles.insert(static_cast<int>(Role::Id), QByteArrayLiteral("Id"));
0384     roles.insert(static_cast<int>(Role::ItemIsMenu), QByteArrayLiteral("ItemIsMenu"));
0385     roles.insert(static_cast<int>(Role::OverlayIconName), QByteArrayLiteral("OverlayIconName"));
0386     roles.insert(static_cast<int>(Role::Status), QByteArrayLiteral("Status"));
0387     roles.insert(static_cast<int>(Role::Title), QByteArrayLiteral("Title"));
0388     roles.insert(static_cast<int>(Role::ToolTipSubTitle), QByteArrayLiteral("ToolTipSubTitle"));
0389     roles.insert(static_cast<int>(Role::ToolTipTitle), QByteArrayLiteral("ToolTipTitle"));
0390     roles.insert(static_cast<int>(Role::WindowId), QByteArrayLiteral("WindowId"));
0391 
0392     return roles;
0393 }
0394 
0395 void StatusNotifierModel::addSource(const QString &source)
0396 {
0397     int count = rowCount();
0398     beginInsertRows(QModelIndex(), count, count);
0399 
0400     StatusNotifierModel::Item item;
0401     item.source = source;
0402 
0403     StatusNotifierItemSource *sni = m_sniHost->itemForService(source);
0404     connect(sni, &StatusNotifierItemSource::dataUpdated, this, [=, this]() {
0405         dataUpdated(source);
0406     });
0407     item.service = sni->createService();
0408     m_items.append(item);
0409     endInsertRows();
0410 }
0411 
0412 void StatusNotifierModel::removeSource(const QString &source)
0413 {
0414     int idx = indexOfSource(source);
0415     if (idx >= 0) {
0416         beginRemoveRows(QModelIndex(), idx, idx);
0417         delete m_items[idx].service;
0418         m_items.removeAt(idx);
0419         endRemoveRows();
0420     }
0421 }
0422 
0423 void StatusNotifierModel::dataUpdated(const QString &sourceName)
0424 {
0425     int idx = indexOfSource(sourceName);
0426 
0427     if (idx >= 0) {
0428         Q_EMIT dataChanged(index(idx, 0), index(idx, 0));
0429     }
0430 }
0431 
0432 int StatusNotifierModel::indexOfSource(const QString &source) const
0433 {
0434     for (int i = 0; i < rowCount(); i++) {
0435         if (m_items[i].source == source) {
0436             return i;
0437         }
0438     }
0439     return -1;
0440 }
0441 
0442 void StatusNotifierModel::init()
0443 {
0444     m_sniHost = StatusNotifierItemHost::self();
0445 
0446     connect(m_sniHost, &StatusNotifierItemHost::itemAdded, this, &StatusNotifierModel::addSource);
0447     connect(m_sniHost, &StatusNotifierItemHost::itemRemoved, this, &StatusNotifierModel::removeSource);
0448 
0449     for (const QStringList services = m_sniHost->services(); const QString &service : services) {
0450         addSource(service);
0451     }
0452 }
0453 
0454 SystemTrayModel::SystemTrayModel(QObject *parent)
0455     : QConcatenateTablesProxyModel(parent)
0456 {
0457     m_roleNames = QConcatenateTablesProxyModel::roleNames();
0458 }
0459 
0460 QHash<int, QByteArray> SystemTrayModel::roleNames() const
0461 {
0462     return m_roleNames;
0463 }
0464 
0465 void SystemTrayModel::addSourceModel(QAbstractItemModel *sourceModel)
0466 {
0467     QHashIterator<int, QByteArray> it(sourceModel->roleNames());
0468     while (it.hasNext()) {
0469         it.next();
0470 
0471         if (!m_roleNames.contains(it.key())) {
0472             m_roleNames.insert(it.key(), it.value());
0473         }
0474     }
0475 
0476     QConcatenateTablesProxyModel::addSourceModel(sourceModel);
0477 }