File indexing completed on 2024-05-05 05:38:37

0001 /*
0002     SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005 */
0006 
0007 #include "waylandstartuptasksmodel.h"
0008 #include "libtaskmanager_debug.h"
0009 #include "tasktools.h"
0010 
0011 #include <KConfigGroup>
0012 #include <KConfigWatcher>
0013 
0014 #include <qwayland-plasma-window-management.h>
0015 
0016 #include <QDebug>
0017 #include <QTimer>
0018 #include <QUrl>
0019 #include <QtWaylandClient/QWaylandClientExtensionTemplate>
0020 
0021 using namespace Qt::StringLiterals;
0022 
0023 namespace TaskManager
0024 {
0025 
0026 class PlasmaActivation : public QObject, public QtWayland::org_kde_plasma_activation
0027 {
0028     Q_OBJECT
0029 public:
0030     PlasmaActivation(::org_kde_plasma_activation *object)
0031         : QtWayland::org_kde_plasma_activation(object)
0032     {
0033     }
0034     ~PlasmaActivation() override
0035     {
0036         destroy();
0037     }
0038     void org_kde_plasma_activation_app_id(const QString &app_id) override
0039     {
0040         Q_EMIT appId(app_id);
0041     }
0042     void org_kde_plasma_activation_finished() override
0043     {
0044         Q_EMIT finished();
0045     }
0046 Q_SIGNALS:
0047     void appId(const QString &appId);
0048     void finished();
0049 };
0050 class PlasmaActivationFeedback : public QWaylandClientExtensionTemplate<PlasmaActivationFeedback>, public QtWayland::org_kde_plasma_activation_feedback
0051 {
0052     Q_OBJECT
0053 public:
0054     PlasmaActivationFeedback()
0055         : QWaylandClientExtensionTemplate(1)
0056     {
0057         connect(this, &QWaylandClientExtension::activeChanged, this, [this] {
0058             if (!isActive()) {
0059                 destroy();
0060             }
0061         });
0062     }
0063     ~PlasmaActivationFeedback()
0064     {
0065         if (isActive()) {
0066             destroy();
0067         }
0068     }
0069 Q_SIGNALS:
0070     void newActivation(PlasmaActivation *activation);
0071 
0072 protected:
0073     void org_kde_plasma_activation_feedback_activation(::org_kde_plasma_activation *id) override
0074     {
0075         Q_EMIT newActivation(new PlasmaActivation(id));
0076     }
0077 };
0078 
0079 class Q_DECL_HIDDEN WaylandStartupTasksModel::Private
0080 {
0081 public:
0082     Private(WaylandStartupTasksModel *q);
0083 
0084     void addActivation(PlasmaActivation *activation);
0085     void removeActivation(PlasmaActivation *activation);
0086 
0087     void init();
0088     void loadConfig();
0089 
0090     struct Startup {
0091         QString name;
0092         QIcon icon;
0093         QString applicationId;
0094         QUrl launcherUrl;
0095         std::unique_ptr<PlasmaActivation> activation;
0096     };
0097 
0098     WaylandStartupTasksModel *q;
0099     KConfigWatcher::Ptr configWatcher = nullptr;
0100     std::unique_ptr<PlasmaActivationFeedback> feedback = nullptr;
0101     std::vector<Startup> startups;
0102     std::chrono::seconds startupTimeout = std::chrono::seconds::zero();
0103 };
0104 
0105 WaylandStartupTasksModel::Private::Private(WaylandStartupTasksModel *q)
0106     : q(q)
0107 {
0108 }
0109 
0110 void WaylandStartupTasksModel::Private::init()
0111 {
0112     configWatcher = KConfigWatcher::create(KSharedConfig::openConfig(QStringLiteral("klaunchrc"), KConfig::NoGlobals));
0113     QObject::connect(configWatcher.data(), &KConfigWatcher::configChanged, q, [this] {
0114         loadConfig();
0115     });
0116 
0117     loadConfig();
0118 }
0119 
0120 void WaylandStartupTasksModel::Private::loadConfig()
0121 {
0122     KConfigGroup feedbackConfig(configWatcher->config(), u"FeedbackStyle"_s);
0123 
0124     if (!feedbackConfig.readEntry("TaskbarButton", true)) {
0125         q->beginResetModel();
0126         startups.clear();
0127         feedback.reset();
0128         q->endResetModel();
0129         return;
0130     }
0131 
0132     const KConfigGroup taskbarButtonConfig(configWatcher->config(), u"TaskbarButtonSettings"_s);
0133     startupTimeout = std::chrono::seconds(taskbarButtonConfig.readEntry("Timeout", 5));
0134 
0135     feedback = std::make_unique<PlasmaActivationFeedback>();
0136 
0137     QObject::connect(feedback.get(), &PlasmaActivationFeedback::activeChanged, q, [this] {
0138         if (!feedback->isActive()) {
0139             q->beginResetModel();
0140             startups.clear();
0141             q->endResetModel();
0142         }
0143     });
0144 
0145     QObject::connect(feedback.get(), &PlasmaActivationFeedback::newActivation, q, [this](PlasmaActivation *activation) {
0146         addActivation(activation);
0147     });
0148 }
0149 
0150 void WaylandStartupTasksModel::Private::addActivation(PlasmaActivation *activation)
0151 {
0152     QObject::connect(activation, &PlasmaActivation::appId, q, [this, activation](const QString &appId) {
0153         // The application id is guaranteed to be the desktop filename without ".desktop"
0154         const QString desktopFileName = appId + QLatin1String(".desktop");
0155         const QString desktopFilePath = QStandardPaths::locate(QStandardPaths::ApplicationsLocation, desktopFileName);
0156         if (desktopFilePath.isEmpty()) {
0157             qCWarning(TASKMANAGER_DEBUG) << "Got invalid activation app_id:" << appId;
0158             return;
0159         }
0160 
0161         const QUrl launcherUrl(QStringLiteral("applications:") + desktopFileName);
0162         const AppData appData = appDataFromUrl(QUrl::fromLocalFile(desktopFilePath));
0163 
0164         const int count = startups.size();
0165         q->beginInsertRows(QModelIndex(), count, count);
0166         startups.push_back(Startup{
0167             .name = appData.name,
0168             .icon = appData.icon,
0169             .applicationId = appId,
0170             .launcherUrl = launcherUrl,
0171             .activation = std::unique_ptr<PlasmaActivation>(activation),
0172         });
0173         q->endInsertRows();
0174 
0175         // Remove the activation if it doesn't finish within certain time interval.
0176         QTimer *timeoutTimer = new QTimer(activation);
0177         QObject::connect(timeoutTimer, &QTimer::timeout, q, [this, activation]() {
0178             removeActivation(activation);
0179         });
0180         timeoutTimer->setSingleShot(true);
0181         timeoutTimer->start(startupTimeout);
0182     });
0183 
0184     QObject::connect(activation, &PlasmaActivation::finished, q, [this, activation]() {
0185         removeActivation(activation);
0186     });
0187 }
0188 
0189 void WaylandStartupTasksModel::Private::removeActivation(PlasmaActivation *activation)
0190 {
0191     auto it = std::find_if(startups.begin(), startups.end(), [activation](const Startup &startup) {
0192         return startup.activation.get() == activation;
0193     });
0194     if (it == startups.end()) {
0195         return;
0196     }
0197     const int position = std::distance(startups.begin(), it);
0198     q->beginRemoveRows(QModelIndex(), position, position);
0199     startups.erase(it);
0200     q->endRemoveRows();
0201 }
0202 
0203 WaylandStartupTasksModel::WaylandStartupTasksModel(QObject *parent)
0204     : AbstractTasksModel(parent)
0205     , d(new Private(this))
0206 {
0207     d->init();
0208 }
0209 
0210 WaylandStartupTasksModel::~WaylandStartupTasksModel()
0211 {
0212 }
0213 
0214 QVariant WaylandStartupTasksModel::data(const QModelIndex &index, int role) const
0215 {
0216     // Note: when index is valid, its row >= 0, so casting to unsigned is safe
0217     if (!index.isValid() || static_cast<size_t>(index.row()) >= d->startups.size()) {
0218         return QVariant();
0219     }
0220 
0221     const auto &data = d->startups[index.row()];
0222     if (role == Qt::DisplayRole) {
0223         return data.name;
0224     } else if (role == Qt::DecorationRole) {
0225         return data.icon;
0226     } else if (role == AppId) {
0227         return data.applicationId;
0228     } else if (role == AppName) {
0229         return data.name;
0230     } else if (role == LauncherUrl || role == LauncherUrlWithoutIcon) {
0231         return data.launcherUrl;
0232     } else if (role == IsStartup) {
0233         return true;
0234     } else if (role == CanLaunchNewInstance) {
0235         return false;
0236     } else if (role == IsOnAllVirtualDesktops) {
0237         return true;
0238     }
0239 
0240     return AbstractTasksModel::data(index, role);
0241 }
0242 
0243 int WaylandStartupTasksModel::rowCount(const QModelIndex &parent) const
0244 {
0245     return parent.isValid() ? 0 : d->startups.size();
0246 }
0247 
0248 } // namespace TaskManager
0249 
0250 #include "waylandstartuptasksmodel.moc"