// SPDX-License-Identifier: MIT #include "keyboardlayout.h" #include "pendingselection.h" #include "taskbargeometry.h" #include "taskselection.h" #include "windowpreview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "qwayland-keystate.h" #include #include #include #include #include #include #include #include #include #include namespace { constexpr auto configFile = "plasma-org.kde.plasma.desktop-appletsrc"; constexpr auto componentId = "se.ajpanton.plasma-task-group-shortcuts"; constexpr auto stateFile = "plasma-task-group-shortcutsrc"; struct ShortcutSettings { bool startWithFirst = true; bool initialShiftOpensNewInstance = true; bool shiftCyclesBackward = true; }; ShortcutSettings shortcutSettings() { const auto config = KSharedConfig::openConfig(QString::fromLatin1(stateFile)); const KConfigGroup group(config, QStringLiteral("Settings")); return { group.readEntry("StartWithFirstWindow", true), group.readEntry("InitialShiftOpensNewInstance", true), group.readEntry("ShiftCyclesBackward", true), }; } KConfigGroup displacedShortcutGroup(int number) { const auto config = KSharedConfig::openConfig(QString::fromLatin1(stateFile)); return KConfigGroup(config, QStringLiteral("Displaced shortcut %1").arg(number)); } void rememberDisplacedShortcuts(int number, const QList &sequences) { KConfigGroup group = displacedShortcutGroup(number); QJsonArray shortcuts = QJsonDocument::fromJson(group.readEntry("Shortcuts", QByteArray())).array(); QSet savedActions; for (const QJsonValue &value : std::as_const(shortcuts)) { const QJsonObject shortcut = value.toObject(); savedActions.insert(shortcut.value(QStringLiteral("component")).toString() + QLatin1Char('\n') + shortcut.value(QStringLiteral("action")).toString()); } QStringList capturedSequences = group.readEntry("Sequences", QStringList()); for (const QKeySequence &sequence : sequences) { const QString sequenceName = sequence.toString(QKeySequence::PortableText); if (capturedSequences.contains(sequenceName)) { continue; } capturedSequences.append(sequenceName); for (const KGlobalShortcutInfo &shortcut : KGlobalAccel::globalShortcutsByKey(sequence)) { const QString actionId = shortcut.componentUniqueName() + QLatin1Char('\n') + shortcut.uniqueName(); if (shortcut.componentUniqueName() == QLatin1String(componentId) || savedActions.contains(actionId)) { continue; } QJsonArray keys; for (const QKeySequence &key : shortcut.keys()) { keys.append(key.toString(QKeySequence::PortableText)); } shortcuts.append(QJsonObject{ {QStringLiteral("component"), shortcut.componentUniqueName()}, {QStringLiteral("componentName"), shortcut.componentFriendlyName()}, {QStringLiteral("action"), shortcut.uniqueName()}, {QStringLiteral("actionName"), shortcut.friendlyName()}, {QStringLiteral("keys"), keys}, }); savedActions.insert(actionId); } } group.writeEntry("Saved", true); group.writeEntry("Sequences", capturedSequences); group.writeEntry("Shortcuts", QJsonDocument(shortcuts).toJson(QJsonDocument::Compact)); group.sync(); } void restoreDisplacedShortcuts() { for (int number = 0; number <= 9; ++number) { KConfigGroup group = displacedShortcutGroup(number); if (!group.readEntry("Saved", false)) { continue; } const QJsonArray shortcuts = QJsonDocument::fromJson(group.readEntry("Shortcuts", QByteArray())).array(); for (const QJsonValue &value : shortcuts) { const QJsonObject shortcut = value.toObject(); QAction action; action.setObjectName(shortcut.value(QStringLiteral("action")).toString()); action.setText(shortcut.value(QStringLiteral("actionName")).toString()); action.setProperty("componentName", shortcut.value(QStringLiteral("component")).toString()); action.setProperty("componentDisplayName", shortcut.value(QStringLiteral("componentName")).toString()); action.setProperty("isConfigurationAction", true); QList keys; for (const QJsonValue &key : shortcut.value(QStringLiteral("keys")).toArray()) { keys.append(QKeySequence::fromString(key.toString(), QKeySequence::PortableText)); } KGlobalAccel::self()->setShortcut(&action, keys, KGlobalAccel::NoAutoloading); } group.deleteGroup(); group.sync(); } KGlobalAccel::cleanComponent(QString::fromLatin1(componentId)); } struct PanelConfig { KConfigGroup general; int location = Plasma::Types::BottomEdge; }; PanelConfig taskManagerConfig(const KSharedConfig::Ptr &config) { const KConfigGroup containments(config, QStringLiteral("Containments")); PanelConfig fallback; for (const QString &containmentId : containments.groupList()) { const KConfigGroup containment = containments.group(containmentId); const KConfigGroup applets = containment.group(QStringLiteral("Applets")); for (const QString &appletId : applets.groupList()) { const KConfigGroup applet = applets.group(appletId); if (applet.readEntry("plugin") == QLatin1String("org.kde.plasma.taskmanager")) { const KConfigGroup general = applet.group(QStringLiteral("Configuration")).group(QStringLiteral("General")); const PanelConfig panel{general, containment.readEntry("location", int(Plasma::Types::BottomEdge))}; if (containment.readEntry("lastScreen", -1) == 0) { return panel; } if (!fallback.general.isValid()) { fallback = panel; } } } } return fallback; } } class ModifierState : public QWaylandClientExtensionTemplate, public QtWayland::org_kde_kwin_keystate { Q_OBJECT public: ModifierState() : QWaylandClientExtensionTemplate(5) { connect(this, &QWaylandClientExtension::activeChanged, this, [this] { if (isActive()) { fetchStates(); } }); initialize(); } bool shiftPressed() const { return m_shiftPressed; } bool metaPressed() const { return isActive() && m_metaPressed; } Q_SIGNALS: void released(); private: void org_kde_kwin_keystate_stateChanged(uint32_t key, uint32_t state) override { if (key == key_shift) { m_shiftPressed = state == state_pressed; } else if (key == key_meta) { const bool pressed = state == state_pressed; if (m_metaPressed && !pressed) { Q_EMIT released(); } m_metaPressed = pressed; } } bool m_metaPressed = false; bool m_shiftPressed = false; }; class ShortcutManager : public QObject { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "se.ajpanton.PlasmaTaskGroupShortcuts.Manager") public: explicit ShortcutManager(QObject *parent = nullptr) : QObject(parent) , m_config(KSharedConfig::openConfig(QString::fromLatin1(configFile))) { m_model.setGroupInline(false); reloadConfig(); connect(&m_activityInfo, &TaskManager::ActivityInfo::currentActivityChanged, this, [this] { m_lastShortcut = -1; m_selection.cancel(); m_model.setActivity(m_activityInfo.currentActivity()); }); connect(&m_model, &QAbstractItemModel::modelReset, this, [this] { m_lastShortcut = -1; }); connect(qGuiApp, &QGuiApplication::primaryScreenChanged, this, &ShortcutManager::updateScreen); connect(&m_modifierState, &ModifierState::released, this, [this] { m_lastShortcut = -1; m_selection.finish(); }); connect(&m_modifierState, &QWaylandClientExtension::activeChanged, this, [this] { if (!m_modifierState.isActive()) { m_lastShortcut = -1; m_selection.cancel(); } }); connect(&m_selection, &PendingSelection::activationRequested, &m_model, &TaskManager::TasksModel::requestActivate); connect(&m_selection, &PendingSelection::previewRequested, this, [this](const QModelIndex &task) { m_preview.show(task, m_panelLocation); m_taskbarGeometry.request(task.data(TaskManager::AbstractTasksModel::WinIdList).toList().value(0).toString()); }); connect(&m_selection, &PendingSelection::previewHidden, this, [this] { m_taskbarGeometry.cancel(); m_preview.hide(); }); connect(&m_taskbarGeometry, &TaskbarGeometry::received, this, [this](const QRectF &geometry) { m_preview.setAnchor(geometry.toAlignedRect()); }); updateScreen(); for (int number = 0; number <= 9; ++number) { auto *action = new QAction(this); action->setObjectName(number == 0 ? QStringLiteral("cancel task group selection") : QStringLiteral("activate task group %1").arg(number)); action->setText(number == 0 ? QStringLiteral("Cancel Task Group Selection") : QStringLiteral("Activate Task Group %1").arg(number)); const QList sequences = number == 0 ? QList{QKeySequence(Qt::META | Qt::Key_Escape), QKeySequence(Qt::META | Qt::SHIFT | Qt::Key_Escape)} : numberRowShortcuts(number); rememberDisplacedShortcuts(number, sequences); for (const QKeySequence &sequence : sequences) { KGlobalAccel::stealShortcutSystemwide(sequence); } KGlobalAccel::self()->setDefaultShortcut(action, sequences); KGlobalAccel::self()->setShortcut(action, sequences, KGlobalAccel::NoAutoloading); connect(action, &QAction::triggered, this, [this, number] { if (number == 0) { m_lastShortcut = -1; m_selection.cancel(); } else { useShortcut(number - 1, m_modifierState.shiftPressed()); } }); } } public Q_SLOTS: Q_SCRIPTABLE void restoreShortcutsAndQuit() { for (QAction *action : findChildren()) { KGlobalAccel::self()->removeAllShortcuts(action); } restoreDisplacedShortcuts(); QCoreApplication::quit(); } private: void reloadConfig() { m_config->reparseConfiguration(); const PanelConfig panel = taskManagerConfig(m_config); const KConfigGroup config = panel.general; if (!config.isValid()) { qWarning("No Plasma Task Manager configuration found"); return; } m_panelLocation = panel.location; m_model.setLauncherList(config.readEntry("launchers", QStringList())); m_model.setFilterByCurrentVirtualDesktop(config.readEntry("showOnlyCurrentDesktop", true)); m_model.setFilterByScreen(config.readEntry("showOnlyCurrentScreen", false)); m_model.setFilterByActivity(config.readEntry("showOnlyCurrentActivity", true)); m_model.setFilterNotMinimized(config.readEntry("showOnlyMinimized", false)); const int sortingStrategy = config.readEntry("sortingStrategy", 1); m_model.setSortMode(static_cast(sortingStrategy)); m_separateLaunchers = sortingStrategy != TaskManager::TasksModel::SortManual || config.readEntry("separateLaunchers", true); m_model.setSeparateLaunchers(m_separateLaunchers); m_model.setLaunchInPlace(false); m_model.setHideActivatedLaunchers(config.readEntry("hideLauncherOnStart", true)); m_model.setGroupMode(static_cast(config.readEntry("groupingStrategy", 1))); m_model.setGroupingWindowTasksThreshold(-1); m_model.setGroupingAppIdBlacklist(config.readEntry("groupingAppIdBlacklist", QStringList())); m_model.setGroupingLauncherUrlBlacklist(config.readEntry("groupingLauncherUrlBlacklist", QStringList())); m_model.setActivity(m_activityInfo.currentActivity()); } void updateScreen() { m_lastShortcut = -1; m_selection.cancel(); if (const QScreen *screen = QGuiApplication::primaryScreen()) { m_model.setScreenGeometry(screen->geometry()); } } void useShortcut(int row, bool shifted) { reloadConfig(); const QVector tasks = logicalTaskRows(m_model, m_separateLaunchers); if (row >= tasks.size()) { m_selection.cancel(); m_lastShortcut = -1; return; } const ShortcutSettings settings = shortcutSettings(); const bool continuing = row == m_lastShortcut; if (!continuing) { const bool backwards = shifted && settings.shiftCyclesBackward && !settings.initialShiftOpensNewInstance; m_cyclePosition = initialCyclePosition( m_model, tasks.at(row), settings.startWithFirst, backwards); } else { m_cyclePosition += shifted && settings.shiftCyclesBackward ? -1 : 1; } const bool held = m_modifierState.metaPressed(); m_lastShortcut = held ? row : -1; if (shifted && !continuing && settings.initialShiftOpensNewInstance) { m_selection.cancel(); m_model.requestNewInstance(m_model.index(tasks.at(row), 0)); return; } const QModelIndex task = taskToActivate(m_model, tasks.at(row), m_cyclePosition); // A quick key release can reach us before the shortcut's D-Bus signal. // In that case there is no held cycle left to preview. m_selection.select(task, task.data(TaskManager::AbstractTasksModel::IsWindow).toBool() && held); } KSharedConfig::Ptr m_config; TaskManager::ActivityInfo m_activityInfo; TaskManager::TasksModel m_model; PendingSelection m_selection{m_model}; WindowPreview m_preview; TaskbarGeometry m_taskbarGeometry; ModifierState m_modifierState; int m_panelLocation = Plasma::Types::BottomEdge; bool m_separateLaunchers = true; int m_lastShortcut = -1; int m_cyclePosition = 0; }; int main(int argc, char **argv) { const bool restoring = std::any_of(argv + 1, argv + argc, [](const char *argument) { return qstrcmp(argument, "--restore-shortcuts") == 0; }); if (restoring) { qputenv("QT_QPA_PLATFORM", "offscreen"); } QGuiApplication application(argc, argv); application.setQuitOnLastWindowClosed(false); QCoreApplication::setQuitLockEnabled(false); QCoreApplication::setApplicationName(QString::fromLatin1(componentId)); QGuiApplication::setApplicationDisplayName(QStringLiteral("Plasma Task Group Shortcuts")); QGuiApplication::setDesktopFileName(QString::fromLatin1(componentId)); QCommandLineParser parser; parser.addHelpOption(); QCommandLineOption restoreOption(QStringLiteral("restore-shortcuts"), QStringLiteral("Restore the shortcuts displaced by this tool and exit.")); parser.addOption(restoreOption); parser.process(application); if (parser.isSet(restoreOption)) { QDBusInterface running(QString::fromLatin1(componentId), QStringLiteral("/Manager"), QStringLiteral("se.ajpanton.PlasmaTaskGroupShortcuts.Manager")); if (running.isValid()) { running.call(QDBus::Block, QStringLiteral("restoreShortcutsAndQuit")); return 0; } restoreDisplacedShortcuts(); return 0; } if (!QDBusConnection::sessionBus().registerService(QString::fromLatin1(componentId))) { return 0; } ShortcutManager manager; QDBusConnection::sessionBus().registerObject(QStringLiteral("/Manager"), &manager, QDBusConnection::ExportScriptableSlots); return application.exec(); } #include "main.moc"