File indexing completed on 2024-05-05 17:41:52

0001 /*
0002     SPDX-FileCopyrightText: 2014, 2015 Ivan Cukic <ivan.cukic(at)kde.org>
0003     SPDX-FileCopyrightText: 2009 Martin Gräßlin <mgraesslin@kde.org>
0004     SPDX-FileCopyrightText: 2003 Lubos Lunak <l.lunak@kde.org>
0005     SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich <ettrich@kde.org>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 // Self
0011 #include "switcherbackend.h"
0012 
0013 // Qt
0014 #include <QAction>
0015 #include <QDBusConnection>
0016 #include <QDBusMessage>
0017 #include <QDateTime>
0018 #include <QGuiApplication>
0019 #include <QRasterWindow>
0020 
0021 // Qml and QtQuick
0022 #include <QQmlEngine>
0023 #include <QQuickImageProvider>
0024 
0025 // KDE
0026 #include <KConfig>
0027 #include <KConfigGroup>
0028 #include <KGlobalAccel>
0029 #include <KIO/PreviewJob>
0030 #include <KLocalizedString>
0031 #include <KWindowSystem>
0032 #include <KX11Extras>
0033 #include <windowtasksmodel.h>
0034 #include <xwindowtasksmodel.h>
0035 
0036 static const char *s_action_name_next_activity = "next activity";
0037 static const char *s_action_name_previous_activity = "previous activity";
0038 
0039 namespace
0040 {
0041 bool areModifiersPressed(const QKeySequence &seq)
0042 {
0043     if (seq.isEmpty()) {
0044         return false;
0045     }
0046     int mod = seq[seq.count() - 1] & Qt::KeyboardModifierMask;
0047     auto activeMods = qGuiApp->queryKeyboardModifiers();
0048     return activeMods & mod;
0049 }
0050 
0051 bool isReverseTab(const QKeySequence &prevAction)
0052 {
0053     if (prevAction == QKeySequence(Qt::ShiftModifier | Qt::Key_Tab)) {
0054         return areModifiersPressed(Qt::SHIFT);
0055     } else {
0056         return false;
0057     }
0058 }
0059 
0060 class ThumbnailImageResponse : public QQuickImageResponse
0061 {
0062 public:
0063     ThumbnailImageResponse(const QString &id, const QSize &requestedSize);
0064 
0065     QQuickTextureFactory *textureFactory() const override;
0066 
0067     void run();
0068 
0069 private:
0070     QString m_id;
0071     QSize m_requestedSize;
0072     QQuickTextureFactory *m_texture = nullptr;
0073 };
0074 
0075 ThumbnailImageResponse::ThumbnailImageResponse(const QString &id, const QSize &requestedSize)
0076     : m_id(id)
0077     , m_requestedSize(requestedSize)
0078     , m_texture(nullptr)
0079 {
0080     int width = m_requestedSize.width();
0081     int height = m_requestedSize.height();
0082 
0083     if (width <= 0) {
0084         width = 320;
0085     }
0086 
0087     if (height <= 0) {
0088         height = 240;
0089     }
0090 
0091     if (m_id.isEmpty()) {
0092         Q_EMIT finished();
0093         return;
0094     }
0095 
0096     const auto file = QUrl::fromUserInput(m_id);
0097 
0098     KFileItemList list;
0099     list.append(KFileItem(file, QString(), 0));
0100 
0101     auto job = KIO::filePreview(list, QSize(width, height));
0102     job->setScaleType(KIO::PreviewJob::Scaled);
0103     job->setIgnoreMaximumSize(true);
0104 
0105     connect(
0106         job,
0107         &KIO::PreviewJob::gotPreview,
0108         this,
0109         [this, file](const KFileItem &item, const QPixmap &pixmap) {
0110             Q_UNUSED(item);
0111 
0112             auto image = pixmap.toImage();
0113 
0114             m_texture = QQuickTextureFactory::textureFactoryForImage(image);
0115             Q_EMIT finished();
0116         },
0117         Qt::QueuedConnection);
0118 
0119     connect(job, &KIO::PreviewJob::failed, this, [this, job](const KFileItem &item) {
0120         Q_UNUSED(item);
0121         qWarning() << "SwitcherBackend: FAILED to get the thumbnail" << job->errorString() << job->detailedErrorStrings();
0122         Q_EMIT finished();
0123     });
0124 }
0125 
0126 QQuickTextureFactory *ThumbnailImageResponse::textureFactory() const
0127 {
0128     return m_texture;
0129 }
0130 
0131 class ThumbnailImageProvider : public QQuickAsyncImageProvider
0132 {
0133 public:
0134     QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override
0135     {
0136         return new ThumbnailImageResponse(id, requestedSize);
0137     }
0138 };
0139 
0140 } // local namespace
0141 
0142 template<typename Handler>
0143 inline void SwitcherBackend::registerShortcut(const QString &actionName, const QString &text, const QKeySequence &shortcut, Handler &&handler)
0144 {
0145     auto action = new QAction(this);
0146 
0147     m_actionShortcut[actionName] = shortcut;
0148 
0149     action->setObjectName(actionName);
0150     action->setText(text);
0151 
0152     KGlobalAccel::self()->setShortcut(action, {shortcut});
0153 
0154     using KActivities::Controller;
0155 
0156     connect(action, &QAction::triggered, this, std::forward<Handler>(handler));
0157 }
0158 
0159 SwitcherBackend::SwitcherBackend(QObject *parent)
0160     : QObject(parent)
0161     , m_shouldShowSwitcher(false)
0162     , m_dropModeActive(false)
0163     , m_runningActivitiesModel(new SortedActivitiesModel({KActivities::Info::Running, KActivities::Info::Stopping}, this))
0164     , m_stoppedActivitiesModel(new SortedActivitiesModel({KActivities::Info::Stopped, KActivities::Info::Starting}, this))
0165 {
0166     registerShortcut(QString::fromLatin1(s_action_name_next_activity),
0167                      i18n("Walk through activities"),
0168                      Qt::META | Qt::Key_Tab,
0169                      &SwitcherBackend::keybdSwitchToNextActivity);
0170 
0171     registerShortcut(QString::fromLatin1(s_action_name_previous_activity),
0172                      i18n("Walk through activities (Reverse)"),
0173                      Qt::META | Qt::SHIFT | Qt::Key_Tab,
0174                      &SwitcherBackend::keybdSwitchToPreviousActivity);
0175 
0176     connect(this, &SwitcherBackend::shouldShowSwitcherChanged, m_runningActivitiesModel, &SortedActivitiesModel::setInhibitUpdates);
0177 
0178     m_modKeyPollingTimer.setInterval(100);
0179     connect(&m_modKeyPollingTimer, &QTimer::timeout, this, &SwitcherBackend::showActivitySwitcherIfNeeded);
0180 
0181     m_dropModeHider.setInterval(500);
0182     m_dropModeHider.setSingleShot(true);
0183     connect(&m_dropModeHider, &QTimer::timeout, this, [this] {
0184         setShouldShowSwitcher(false);
0185     });
0186 
0187     connect(&m_activities, &KActivities::Controller::currentActivityChanged, this, &SwitcherBackend::onCurrentActivityChanged);
0188     m_previousActivity = m_activities.currentActivity();
0189 }
0190 
0191 SwitcherBackend::~SwitcherBackend()
0192 {
0193 }
0194 
0195 QObject *SwitcherBackend::instance(QQmlEngine *engine, QJSEngine *scriptEngine)
0196 {
0197     Q_UNUSED(scriptEngine)
0198     engine->addImageProvider(QStringLiteral("wallpaperthumbnail"), new ThumbnailImageProvider());
0199     return new SwitcherBackend();
0200 }
0201 
0202 void SwitcherBackend::keybdSwitchToNextActivity()
0203 {
0204     if (isReverseTab(m_actionShortcut[QString::fromLatin1(s_action_name_previous_activity)])) {
0205         switchToActivity(Previous);
0206     } else {
0207         switchToActivity(Next);
0208     }
0209 }
0210 
0211 void SwitcherBackend::keybdSwitchToPreviousActivity()
0212 {
0213     switchToActivity(Previous);
0214 }
0215 
0216 void SwitcherBackend::switchToActivity(Direction direction)
0217 {
0218     const auto activityToSet = m_runningActivitiesModel->relativeActivity(direction == Next ? 1 : -1);
0219 
0220     if (activityToSet.isEmpty())
0221         return;
0222 
0223     QTimer::singleShot(0, this, [this, activityToSet]() {
0224         setCurrentActivity(activityToSet);
0225     });
0226 
0227     keybdSwitchedToAnotherActivity();
0228 }
0229 
0230 void SwitcherBackend::keybdSwitchedToAnotherActivity()
0231 {
0232     m_lastInvokedAction = dynamic_cast<QAction *>(sender());
0233     if (KWindowSystem::isPlatformWayland() && !qGuiApp->focusWindow() && !m_inputWindow) {
0234         // create a new Window so the compositor sends us modifier info
0235         m_inputWindow = new QRasterWindow();
0236         m_inputWindow->setGeometry(0, 0, 1, 1);
0237         // Only show once the initial switch has been completed, not cause a switch back
0238         connect(&m_activities, &KActivities::Consumer::currentActivityChanged, m_inputWindow, [this] {
0239             m_inputWindow->show();
0240             m_inputWindow->update();
0241         });
0242         connect(m_inputWindow, &QWindow::activeChanged, this, [this] {
0243             showActivitySwitcherIfNeeded();
0244         });
0245     } else {
0246         QTimer::singleShot(100, this, &SwitcherBackend::showActivitySwitcherIfNeeded);
0247     }
0248 }
0249 
0250 void SwitcherBackend::showActivitySwitcherIfNeeded()
0251 {
0252     if (!m_lastInvokedAction || m_dropModeActive) {
0253         return;
0254     }
0255 
0256     auto actionName = m_lastInvokedAction->objectName();
0257 
0258     if (!m_actionShortcut.contains(actionName)) {
0259         return;
0260     }
0261 
0262     if (!areModifiersPressed(m_actionShortcut[actionName])) {
0263         m_lastInvokedAction = nullptr;
0264         setShouldShowSwitcher(false);
0265         return;
0266     }
0267 
0268     setShouldShowSwitcher(true);
0269 }
0270 
0271 void SwitcherBackend::init()
0272 {
0273     // nothing
0274 }
0275 
0276 void SwitcherBackend::onCurrentActivityChanged(const QString &id)
0277 {
0278     if (m_shouldShowSwitcher) {
0279         // If we are showing the switcher because the user is
0280         // pressing Meta+Tab, we are not ready to commit the
0281         // activity change to memory
0282         return;
0283     }
0284 
0285     if (m_previousActivity == id)
0286         return;
0287 
0288     // Safe, we have a long-lived Consumer object
0289     KActivities::Info activity(id);
0290     Q_EMIT showSwitchNotification(id, activity.name(), activity.icon());
0291 
0292     KConfig config(QStringLiteral("kactivitymanagerd-switcher"));
0293     KConfigGroup times(&config, "LastUsed");
0294 
0295     const auto now = QDateTime::currentDateTime().toSecsSinceEpoch();
0296 
0297     // Updating the time for the activity we just switched to
0298     // in the case we do not power off properly, and on the next
0299     // start, kamd switches to another activity for some reason
0300     times.writeEntry(id, now);
0301 
0302     if (!m_previousActivity.isEmpty()) {
0303         // When leaving an activity, say goodbye and fondly remember
0304         // the last time we saw it
0305         times.writeEntry(m_previousActivity, now);
0306     }
0307 
0308     times.sync();
0309 
0310     m_previousActivity = id;
0311 }
0312 
0313 bool SwitcherBackend::shouldShowSwitcher() const
0314 {
0315     return m_shouldShowSwitcher;
0316 }
0317 
0318 void SwitcherBackend::setShouldShowSwitcher(bool shouldShowSwitcher)
0319 {
0320     if (m_inputWindow) {
0321         delete m_inputWindow;
0322         m_inputWindow = nullptr;
0323     }
0324 
0325     if (m_shouldShowSwitcher == shouldShowSwitcher)
0326         return;
0327 
0328     m_shouldShowSwitcher = shouldShowSwitcher;
0329 
0330     if (m_shouldShowSwitcher) {
0331         // TODO: We really should NOT do this by polling
0332         m_modKeyPollingTimer.start();
0333     } else {
0334         m_modKeyPollingTimer.stop();
0335 
0336         // We might have an unprocessed onCurrentActivityChanged
0337         onCurrentActivityChanged(m_activities.currentActivity());
0338     }
0339 
0340     Q_EMIT shouldShowSwitcherChanged(m_shouldShowSwitcher);
0341 }
0342 
0343 QAbstractItemModel *SwitcherBackend::runningActivitiesModel() const
0344 {
0345     return m_runningActivitiesModel;
0346 }
0347 
0348 QAbstractItemModel *SwitcherBackend::stoppedActivitiesModel() const
0349 {
0350     return m_stoppedActivitiesModel;
0351 }
0352 
0353 void SwitcherBackend::setCurrentActivity(const QString &activity)
0354 {
0355     m_activities.setCurrentActivity(activity);
0356 }
0357 
0358 void SwitcherBackend::stopActivity(const QString &activity)
0359 {
0360     m_activities.stopActivity(activity);
0361 }
0362 
0363 bool SwitcherBackend::dropEnabled() const
0364 {
0365 #if HAVE_X11
0366     return true;
0367 #else
0368     return false;
0369 #endif
0370 }
0371 
0372 void SwitcherBackend::dropCopy(QMimeData *mimeData, const QVariant &activityId)
0373 {
0374     drop(mimeData, Qt::ControlModifier, activityId);
0375 }
0376 
0377 void SwitcherBackend::dropMove(QMimeData *mimeData, const QVariant &activityId)
0378 {
0379     drop(mimeData, 0, activityId);
0380 }
0381 
0382 void SwitcherBackend::drop(QMimeData *mimeData, int modifiers, const QVariant &activityId)
0383 {
0384     setDropMode(false);
0385 
0386 #if HAVE_X11
0387     if (KWindowSystem::isPlatformX11()) {
0388         bool ok = false;
0389         const QList<WId> &ids = TaskManager::XWindowTasksModel::winIdsFromMimeData(mimeData, &ok);
0390 
0391         if (!ok) {
0392             return;
0393         }
0394 
0395         const QString newActivity = activityId.toString();
0396         const QStringList runningActivities = m_activities.runningActivities();
0397 
0398         if (!runningActivities.contains(newActivity)) {
0399             return;
0400         }
0401 
0402         for (const auto &id : ids) {
0403             QStringList activities = KWindowInfo(id, NET::Properties(), NET::WM2Activities).activities();
0404 
0405             if (modifiers & Qt::ControlModifier) {
0406                 // Add to the activity instead of moving.
0407                 // This is a hack because the task manager reports that
0408                 // is supports only the 'Move' DND action.
0409                 if (!activities.contains(newActivity)) {
0410                     activities << newActivity;
0411                 }
0412 
0413             } else {
0414                 // Move to this activity
0415                 // if on only one activity, set it to only the new activity
0416                 // if on >1 activity, remove it from the current activity and add it to the new activity
0417 
0418                 const QString currentActivity = m_activities.currentActivity();
0419                 activities.removeAll(currentActivity);
0420                 activities << newActivity;
0421             }
0422 
0423             KX11Extras::setOnActivities(id, activities);
0424         }
0425     }
0426 #endif
0427 }
0428 
0429 void SwitcherBackend::setDropMode(bool value)
0430 {
0431     if (m_dropModeActive == value)
0432         return;
0433 
0434     m_dropModeActive = value;
0435     if (value) {
0436         setShouldShowSwitcher(true);
0437         m_dropModeHider.stop();
0438     } else {
0439         m_dropModeHider.start();
0440     }
0441 }
0442 
0443 void SwitcherBackend::toggleActivityManager()
0444 {
0445     auto message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.plasmashell"),
0446                                                   QStringLiteral("/PlasmaShell"),
0447                                                   QStringLiteral("org.kde.PlasmaShell"),
0448                                                   QStringLiteral("toggleActivityManager"));
0449     QDBusConnection::sessionBus().call(message, QDBus::NoBlock);
0450 }