/* SPDX-FileCopyrightText: 2014, 2015 Ivan Cukic SPDX-FileCopyrightText: 2009 Martin Gräßlin SPDX-FileCopyrightText: 2003 Lubos Lunak SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich SPDX-License-Identifier: GPL-2.0-or-later */ // Self #include "switcherbackend.h" // Qt #include #include #include #include #include #include // Qml and QtQuick #include #include // KDE #include #include #include #include #include #include #include #include #include static const char *s_action_name_next_activity = "next activity"; static const char *s_action_name_previous_activity = "previous activity"; namespace { bool areModifiersPressed(const QKeySequence &seq, const KModifierKeyInfo &modifierInfo) { if (seq.isEmpty()) { return false; } auto mod = seq[seq.count() - 1].keyboardModifiers(); if (mod & Qt::ShiftModifier && modifierInfo.isKeyPressed(Qt::Key_Shift)) { return true; } if (mod & Qt::ControlModifier && modifierInfo.isKeyPressed(Qt::Key_Control)) { return true; } if (mod & Qt::AltModifier && modifierInfo.isKeyPressed(Qt::Key_Alt)) { return true; } if (mod & Qt::MetaModifier && (modifierInfo.isKeyPressed(Qt::Key_Meta))) { return true; } return false; } bool isReverseTab(const QKeySequence &prevAction, const KModifierKeyInfo &modifierInfo) { if (prevAction == QKeySequence(Qt::ShiftModifier | Qt::Key_Tab)) { return areModifiersPressed(Qt::SHIFT, modifierInfo); } else { return false; } } class ThumbnailImageResponse : public QQuickImageResponse { public: ThumbnailImageResponse(const QString &id, const QSize &requestedSize); QQuickTextureFactory *textureFactory() const override; void run(); private: QString m_id; QSize m_requestedSize; QQuickTextureFactory *m_texture = nullptr; }; ThumbnailImageResponse::ThumbnailImageResponse(const QString &id, const QSize &requestedSize) : m_id(id) , m_requestedSize(requestedSize) , m_texture(nullptr) { int width = m_requestedSize.width(); int height = m_requestedSize.height(); if (width <= 0) { width = 320; } if (height <= 0) { height = 240; } if (m_id.isEmpty()) { Q_EMIT finished(); return; } const auto file = QUrl::fromUserInput(m_id); KFileItemList list; list.append(KFileItem(file, QString(), 0)); auto job = KIO::filePreview(list, QSize(width, height)); job->setScaleType(KIO::PreviewJob::Scaled); job->setIgnoreMaximumSize(true); connect( job, &KIO::PreviewJob::generated, this, [this, file](const KFileItem &item, const QImage &image) { Q_UNUSED(item); m_texture = QQuickTextureFactory::textureFactoryForImage(image); Q_EMIT finished(); }, Qt::QueuedConnection); connect(job, &KIO::PreviewJob::failed, this, [this, job](const KFileItem &item) { Q_UNUSED(item); qWarning() << "SwitcherBackend: FAILED to get the thumbnail" << job->errorString() << job->detailedErrorStrings(); Q_EMIT finished(); }); } QQuickTextureFactory *ThumbnailImageResponse::textureFactory() const { return m_texture; } class ThumbnailImageProvider : public QQuickAsyncImageProvider { public: QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override { return new ThumbnailImageResponse(id, requestedSize); } }; } // local namespace template inline void SwitcherBackend::registerShortcut(const QString &actionName, const QString &text, const QKeySequence &shortcut, Handler &&handler) { auto action = new QAction(this); m_actionShortcut[actionName] = shortcut; action->setObjectName(actionName); action->setText(text); KGlobalAccel::self()->setShortcut(action, {shortcut}); using KActivities::Controller; connect(action, &QAction::triggered, this, std::forward(handler)); } SwitcherBackend::SwitcherBackend(QObject *parent) : QObject(parent) , m_shouldShowSwitcher(false) , m_dropModeActive(false) , m_runningActivitiesModel(new SortedActivitiesModel(this)) { registerShortcut(QString::fromLatin1(s_action_name_next_activity), i18n("Walk through activities"), Qt::META | Qt::Key_A, &SwitcherBackend::keybdSwitchToNextActivity); registerShortcut(QString::fromLatin1(s_action_name_previous_activity), i18n("Walk through activities (Reverse)"), Qt::META | Qt::SHIFT | Qt::Key_A, &SwitcherBackend::keybdSwitchToPreviousActivity); connect(this, &SwitcherBackend::shouldShowSwitcherChanged, m_runningActivitiesModel, &SortedActivitiesModel::setInhibitUpdates); m_modKeyPollingTimer.setInterval(100); connect(&m_modKeyPollingTimer, &QTimer::timeout, this, &SwitcherBackend::showActivitySwitcherIfNeeded); m_dropModeHider.setInterval(500); m_dropModeHider.setSingleShot(true); connect(&m_dropModeHider, &QTimer::timeout, this, [this] { setShouldShowSwitcher(false); }); connect(&m_activities, &KActivities::Controller::currentActivityChanged, this, &SwitcherBackend::onCurrentActivityChanged); m_previousActivity = m_activities.currentActivity(); // migrate values to state location KConfig oldConfig(QStringLiteral("kactivitymanagerd-switcher")); KConfigGroup oldTimes(&oldConfig, QStringLiteral("LastUsed")); KConfig stateConfig(QStringLiteral("plasma/activitiesstaterc"), KConfig::SimpleConfig, QStandardPaths::GenericStateLocation); KConfigGroup times = stateConfig.group(QStringLiteral("LastUsed")); oldTimes.moveValuesTo(times); } SwitcherBackend::~SwitcherBackend() { } SwitcherBackend *SwitcherBackend::create(QQmlEngine *engine, QJSEngine *scriptEngine) { Q_UNUSED(scriptEngine) engine->addImageProvider(QStringLiteral("wallpaperthumbnail"), new ThumbnailImageProvider()); return new SwitcherBackend(nullptr); } void SwitcherBackend::keybdSwitchToNextActivity() { if (isReverseTab(m_actionShortcut[QString::fromLatin1(s_action_name_previous_activity)], m_modifierInfo)) { switchToActivity(Previous); } else { switchToActivity(Next); } } void SwitcherBackend::keybdSwitchToPreviousActivity() { switchToActivity(Previous); } void SwitcherBackend::switchToActivity(Direction direction) { const auto activityToSet = m_runningActivitiesModel->relativeActivity(direction == Next ? 1 : -1); if (activityToSet.isEmpty()) return; QTimer::singleShot(0, this, [this, activityToSet]() { setCurrentActivity(activityToSet); }); keybdSwitchedToAnotherActivity(); } void SwitcherBackend::keybdSwitchedToAnotherActivity() { m_lastInvokedAction = dynamic_cast(sender()); QTimer::singleShot(100, this, &SwitcherBackend::showActivitySwitcherIfNeeded); } void SwitcherBackend::showActivitySwitcherIfNeeded() { if (!m_lastInvokedAction || m_dropModeActive) { return; } auto actionName = m_lastInvokedAction->objectName(); if (!m_actionShortcut.contains(actionName)) { return; } if (!areModifiersPressed(m_actionShortcut[actionName], m_modifierInfo)) { m_lastInvokedAction = nullptr; setShouldShowSwitcher(false); return; } setShouldShowSwitcher(true); } void SwitcherBackend::init() { // nothing } void SwitcherBackend::onCurrentActivityChanged(const QString &id) { if (m_shouldShowSwitcher) { // If we are showing the switcher because the user is // pressing Meta+Tab, we are not ready to commit the // activity change to memory return; } if (m_previousActivity == id) return; // Safe, we have a long-lived Consumer object KActivities::Info activity(id); Q_EMIT showSwitchNotification(id, activity.name(), activity.icon()); KConfig config(QStringLiteral("plasma/activitiesstaterc"), KConfig::SimpleConfig, QStandardPaths::GenericStateLocation); KConfigGroup times(&config, QStringLiteral("LastUsed")); const auto now = QDateTime::currentSecsSinceEpoch(); // Updating the time for the activity we just switched to // in the case we do not power off properly, and on the next // start, kamd switches to another activity for some reason times.writeEntry(id, now); if (!m_previousActivity.isEmpty()) { // When leaving an activity, say goodbye and fondly remember // the last time we saw it times.writeEntry(m_previousActivity, now); } times.sync(); m_previousActivity = id; } bool SwitcherBackend::shouldShowSwitcher() const { return m_shouldShowSwitcher; } void SwitcherBackend::setShouldShowSwitcher(bool shouldShowSwitcher) { if (m_shouldShowSwitcher == shouldShowSwitcher) return; m_shouldShowSwitcher = shouldShowSwitcher; if (m_shouldShowSwitcher) { // TODO: We really should NOT do this by polling m_modKeyPollingTimer.start(); } else { m_modKeyPollingTimer.stop(); // We might have an unprocessed onCurrentActivityChanged onCurrentActivityChanged(m_activities.currentActivity()); } Q_EMIT shouldShowSwitcherChanged(m_shouldShowSwitcher); } QAbstractItemModel *SwitcherBackend::runningActivitiesModel() const { return m_runningActivitiesModel; } void SwitcherBackend::setCurrentActivity(const QString &activity) { m_activities.setCurrentActivity(activity); } void SwitcherBackend::removeActivity(const QString &activity) { m_activities.removeActivity(activity); } bool SwitcherBackend::dropEnabled() const { return true; } void SwitcherBackend::dropCopy(QMimeData *mimeData, const QVariant &activityId) { drop(mimeData, Qt::ControlModifier, activityId); } void SwitcherBackend::dropMove(QMimeData *mimeData, const QVariant &activityId) { drop(mimeData, 0, activityId); } void SwitcherBackend::drop(QMimeData *mimeData, int modifiers, const QVariant &activityId) { setDropMode(false); } void SwitcherBackend::setDropMode(bool value) { if (m_dropModeActive == value) return; m_dropModeActive = value; if (value) { setShouldShowSwitcher(true); m_dropModeHider.stop(); } else { m_dropModeHider.start(); } } bool SwitcherBackend::dragContainsWindows(QMimeData *mimeData) const { if (KWindowSystem::isPlatformWayland()) { return TaskManager::WaylandTasksModel::winIdsFromMimeData(mimeData).count(); } return false; } void SwitcherBackend::toggleActivityManager() { auto message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.plasmashell"), QStringLiteral("/PlasmaShell"), QStringLiteral("org.kde.PlasmaShell"), QStringLiteral("toggleActivityManager")); QDBusConnection::sessionBus().call(message, QDBus::NoBlock); } #include "moc_switcherbackend.cpp"