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 }