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 }