File indexing completed on 2024-05-12 05:35:36

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