Add Plasma task group shortcuts
This commit is contained in:
@@ -16,6 +16,7 @@ sudo dnf config-manager addrepo \
|
|||||||
```bash
|
```bash
|
||||||
sudo dnf install touchpad-hold-tap
|
sudo dnf install touchpad-hold-tap
|
||||||
sudo dnf install plasma-always-show-unlock
|
sudo dnf install plasma-always-show-unlock
|
||||||
|
sudo dnf install plasma-task-group-shortcuts
|
||||||
```
|
```
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
@@ -48,3 +49,34 @@ The package applies a narrow patch to Plasma 6.7's lock-screen QML and reapplies
|
|||||||
it after `plasma-desktop` updates. An unknown future layout is left unchanged
|
it after `plasma-desktop` updates. An unknown future layout is left unchanged
|
||||||
instead of being modified speculatively. Removing the package restores Plasma's
|
instead of being modified speculatively. Removing the package restores Plasma's
|
||||||
original behavior.
|
original behavior.
|
||||||
|
|
||||||
|
### Plasma task-group shortcuts
|
||||||
|
|
||||||
|
The `plasma-task-group-shortcuts` package makes Meta+1 through Meta+9 address
|
||||||
|
application groups instead of individual taskbar buttons. It follows the
|
||||||
|
launchers, ordering, filters, and grouping exceptions of the Task Manager on
|
||||||
|
the primary screen. Pressing a launcher opens the application; pressing an
|
||||||
|
application group starts with its first window, then advances through its
|
||||||
|
windows while Meta remains held. Holding Shift after starting a cycle moves
|
||||||
|
backward instead. Shift on the first press launches a new instance. Releasing
|
||||||
|
Meta—or pressing a different number—ends the current cycle. The shortcuts
|
||||||
|
follow the physical number-row keys for every keyboard layout configured in
|
||||||
|
KWin, including layouts where those keys do not produce digits without Shift.
|
||||||
|
|
||||||
|
With **Keep launchers separate** disabled, a pinned launcher and its running
|
||||||
|
application share one shortcut position. When enabled, the launcher is its own
|
||||||
|
position and the application's windows form another. **Hide launcher when
|
||||||
|
application starts** determines whether that separate launcher remains while
|
||||||
|
the application is running. Other groups retain the Task Manager's sort order.
|
||||||
|
|
||||||
|
If application grouping is disabled—or an application is excluded from
|
||||||
|
grouping—each of its windows remains a separate shortcut position. **Only group
|
||||||
|
when the Task Manager is full** does not alter shortcut groups; that would make
|
||||||
|
the numbers change merely because the panel changed size.
|
||||||
|
|
||||||
|
The utility starts with Plasma and records the shortcuts it replaces. Before
|
||||||
|
removing it, restore them with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
plasma-task-group-shortcuts --restore-shortcuts
|
||||||
|
```
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
cmake_minimum_required(VERSION 3.22)
|
||||||
|
|
||||||
|
project(plasma-task-group-shortcuts VERSION 0.1.0 LANGUAGES C CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_AUTOMOC ON)
|
||||||
|
|
||||||
|
find_package(ECM REQUIRED NO_MODULE)
|
||||||
|
list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
|
||||||
|
|
||||||
|
include(GNUInstallDirs)
|
||||||
|
include(CTest)
|
||||||
|
|
||||||
|
find_package(Qt6 REQUIRED COMPONENTS DBus Gui WaylandClient)
|
||||||
|
find_package(KF6 REQUIRED COMPONENTS Config GlobalAccel Service)
|
||||||
|
find_package(LibTaskManager REQUIRED)
|
||||||
|
find_package(PlasmaWaylandProtocols REQUIRED)
|
||||||
|
find_package(XKB REQUIRED)
|
||||||
|
|
||||||
|
add_executable(plasma-task-group-shortcuts
|
||||||
|
keyboardlayout.cpp
|
||||||
|
main.cpp
|
||||||
|
taskselection.cpp
|
||||||
|
)
|
||||||
|
qt6_generate_wayland_protocol_client_sources(plasma-task-group-shortcuts
|
||||||
|
PRIVATE_CODE
|
||||||
|
FILES ${PLASMA_WAYLAND_PROTOCOLS_DIR}/keystate.xml
|
||||||
|
)
|
||||||
|
target_link_libraries(plasma-task-group-shortcuts PRIVATE
|
||||||
|
Qt6::Gui
|
||||||
|
Qt6::DBus
|
||||||
|
Qt6::WaylandClient
|
||||||
|
KF6::ConfigCore
|
||||||
|
KF6::GlobalAccel
|
||||||
|
KF6::Service
|
||||||
|
PW::LibTaskManager
|
||||||
|
XKB::XKB
|
||||||
|
)
|
||||||
|
|
||||||
|
install(TARGETS plasma-task-group-shortcuts DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||||
|
install(FILES se.ajpanton.plasma-task-group-shortcuts.desktop
|
||||||
|
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
||||||
|
install(FILES se.ajpanton.plasma-task-group-shortcuts-autostart.desktop
|
||||||
|
DESTINATION ${CMAKE_INSTALL_FULL_SYSCONFDIR}/xdg/autostart)
|
||||||
|
|
||||||
|
if(BUILD_TESTING)
|
||||||
|
find_package(Qt6 REQUIRED COMPONENTS Test)
|
||||||
|
add_executable(test-taskselection
|
||||||
|
tests/test-taskselection.cpp
|
||||||
|
taskselection.cpp
|
||||||
|
)
|
||||||
|
target_link_libraries(test-taskselection PRIVATE
|
||||||
|
Qt6::Test
|
||||||
|
KF6::ConfigCore
|
||||||
|
KF6::Service
|
||||||
|
PW::LibTaskManager
|
||||||
|
)
|
||||||
|
add_test(NAME taskselection COMMAND test-taskselection)
|
||||||
|
|
||||||
|
add_executable(test-keyboardlayout
|
||||||
|
tests/test-keyboardlayout.cpp
|
||||||
|
keyboardlayout.cpp
|
||||||
|
)
|
||||||
|
target_link_libraries(test-keyboardlayout PRIVATE
|
||||||
|
Qt6::Test
|
||||||
|
Qt6::Gui
|
||||||
|
Qt6::DBus
|
||||||
|
XKB::XKB
|
||||||
|
)
|
||||||
|
add_test(NAME keyboardlayout COMMAND test-keyboardlayout)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "keyboardlayout.h"
|
||||||
|
|
||||||
|
#include <QDBusArgument>
|
||||||
|
#include <QDBusInterface>
|
||||||
|
#include <QDBusMetaType>
|
||||||
|
#include <QDBusReply>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include <xkbcommon/xkbcommon.h>
|
||||||
|
|
||||||
|
namespace KeyboardLayoutDbus
|
||||||
|
{
|
||||||
|
struct Layout
|
||||||
|
{
|
||||||
|
QString name;
|
||||||
|
QString variant;
|
||||||
|
QString displayName;
|
||||||
|
};
|
||||||
|
|
||||||
|
using Layouts = QList<Layout>;
|
||||||
|
|
||||||
|
QDBusArgument &operator<<(QDBusArgument &argument, const Layout &layout)
|
||||||
|
{
|
||||||
|
argument.beginStructure();
|
||||||
|
argument << layout.name << layout.variant << layout.displayName;
|
||||||
|
argument.endStructure();
|
||||||
|
return argument;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QDBusArgument &operator>>(const QDBusArgument &argument, Layout &layout)
|
||||||
|
{
|
||||||
|
argument.beginStructure();
|
||||||
|
argument >> layout.name >> layout.variant >> layout.displayName;
|
||||||
|
argument.endStructure();
|
||||||
|
return argument;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(KeyboardLayoutDbus::Layout)
|
||||||
|
Q_DECLARE_METATYPE(KeyboardLayoutDbus::Layouts)
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
KeyboardLayoutDbus::Layouts configuredLayouts()
|
||||||
|
{
|
||||||
|
qDBusRegisterMetaType<KeyboardLayoutDbus::Layout>();
|
||||||
|
qDBusRegisterMetaType<KeyboardLayoutDbus::Layouts>();
|
||||||
|
|
||||||
|
QDBusInterface layouts(QStringLiteral("org.kde.KWin"), QStringLiteral("/Layouts"),
|
||||||
|
QStringLiteral("org.kde.KeyboardLayouts"));
|
||||||
|
const QDBusReply<KeyboardLayoutDbus::Layouts> reply = layouts.call(QStringLiteral("getLayoutsList"));
|
||||||
|
if (!reply.isValid()) {
|
||||||
|
qWarning() << "Could not read KWin keyboard layouts:" << reply.error().message();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return reply.value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Qt::Key> numberRowKey(QStringView layout, QStringView variant, int number, bool shifted)
|
||||||
|
{
|
||||||
|
if (number < 1 || number > 9) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
using Context = std::unique_ptr<xkb_context, decltype(&xkb_context_unref)>;
|
||||||
|
using Keymap = std::unique_ptr<xkb_keymap, decltype(&xkb_keymap_unref)>;
|
||||||
|
using State = std::unique_ptr<xkb_state, decltype(&xkb_state_unref)>;
|
||||||
|
|
||||||
|
const Context context(xkb_context_new(XKB_CONTEXT_NO_FLAGS), xkb_context_unref);
|
||||||
|
if (!context) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray layoutName = layout.toLatin1();
|
||||||
|
const QByteArray variantName = variant.toLatin1();
|
||||||
|
const xkb_rule_names names{
|
||||||
|
.layout = layoutName.constData(),
|
||||||
|
.variant = variantName.isEmpty() ? nullptr : variantName.constData(),
|
||||||
|
};
|
||||||
|
const Keymap keymap(xkb_keymap_new_from_names(context.get(), &names, XKB_KEYMAP_COMPILE_NO_FLAGS),
|
||||||
|
xkb_keymap_unref);
|
||||||
|
if (!keymap) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const State state(xkb_state_new(keymap.get()), xkb_state_unref);
|
||||||
|
if (!state) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shifted) {
|
||||||
|
const xkb_mod_index_t shift = xkb_keymap_mod_get_index(keymap.get(), XKB_MOD_NAME_SHIFT);
|
||||||
|
if (shift == XKB_MOD_INVALID) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
xkb_state_update_mask(state.get(), xkb_mod_mask_t{1} << shift, 0, 0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
const QByteArray keyName = QByteArrayLiteral("AE0") + QByteArray::number(number);
|
||||||
|
const xkb_keycode_t keycode = xkb_keymap_key_by_name(keymap.get(), keyName.constData());
|
||||||
|
if (keycode == XKB_KEYCODE_INVALID) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
const uint32_t codePoint = xkb_keysym_to_utf32(xkb_state_key_get_one_sym(state.get(), keycode));
|
||||||
|
if (codePoint == 0) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return static_cast<Qt::Key>(codePoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QKeySequence> numberRowShortcuts(int number)
|
||||||
|
{
|
||||||
|
QList<QKeySequence> shortcuts;
|
||||||
|
for (const auto &layout : configuredLayouts()) {
|
||||||
|
for (const bool shifted : {false, true}) {
|
||||||
|
const auto key = numberRowKey(layout.name, layout.variant, number, shifted);
|
||||||
|
if (!key) {
|
||||||
|
qWarning() << "Could not map number-row key" << number << "for keyboard layout" << layout.name;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QKeySequence sequence(Qt::META | *key);
|
||||||
|
if (!shortcuts.contains(sequence)) {
|
||||||
|
shortcuts.append(sequence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shortcuts;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QKeySequence>
|
||||||
|
#include <QStringView>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
std::optional<Qt::Key> numberRowKey(QStringView layout, QStringView variant, int number, bool shifted);
|
||||||
|
QList<QKeySequence> numberRowShortcuts(int number);
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "keyboardlayout.h"
|
||||||
|
#include "taskselection.h"
|
||||||
|
|
||||||
|
#include <QAction>
|
||||||
|
#include <QCommandLineOption>
|
||||||
|
#include <QCommandLineParser>
|
||||||
|
#include <QDBusConnection>
|
||||||
|
#include <QDBusInterface>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QScreen>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QWaylandClientExtensionTemplate>
|
||||||
|
|
||||||
|
#include "qwayland-keystate.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <KConfigGroup>
|
||||||
|
#include <KGlobalAccel>
|
||||||
|
#include <KGlobalShortcutInfo>
|
||||||
|
#include <KSharedConfig>
|
||||||
|
|
||||||
|
#include <abstracttasksmodel.h>
|
||||||
|
#include <activityinfo.h>
|
||||||
|
#include <tasksmodel.h>
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
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<QKeySequence> &sequences)
|
||||||
|
{
|
||||||
|
KConfigGroup group = displacedShortcutGroup(number);
|
||||||
|
QJsonArray shortcuts = QJsonDocument::fromJson(group.readEntry("Shortcuts", QByteArray())).array();
|
||||||
|
QSet<QString> 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 = 1; 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<QKeySequence> 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
KConfigGroup taskManagerConfig(const KSharedConfig::Ptr &config)
|
||||||
|
{
|
||||||
|
const KConfigGroup containments(config, QStringLiteral("Containments"));
|
||||||
|
KConfigGroup 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"));
|
||||||
|
if (containment.readEntry("lastScreen", -1) == 0) {
|
||||||
|
return general;
|
||||||
|
}
|
||||||
|
if (!fallback.isValid()) {
|
||||||
|
fallback = general;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModifierState : public QWaylandClientExtensionTemplate<ModifierState>,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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_model.setActivity(m_activityInfo.currentActivity());
|
||||||
|
});
|
||||||
|
connect(qGuiApp, &QGuiApplication::primaryScreenChanged, this, &ShortcutManager::updateScreen);
|
||||||
|
connect(&m_modifierState, &ModifierState::released, this, [this] {
|
||||||
|
m_lastShortcut = -1;
|
||||||
|
});
|
||||||
|
updateScreen();
|
||||||
|
|
||||||
|
for (int number = 1; number <= 9; ++number) {
|
||||||
|
auto *action = new QAction(this);
|
||||||
|
action->setObjectName(QStringLiteral("activate task group %1").arg(number));
|
||||||
|
action->setText(QStringLiteral("Activate Task Group %1").arg(number));
|
||||||
|
|
||||||
|
const QList<QKeySequence> sequences = 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] {
|
||||||
|
useShortcut(number - 1, m_modifierState.shiftPressed());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Q_SLOTS:
|
||||||
|
Q_SCRIPTABLE void restoreShortcutsAndQuit()
|
||||||
|
{
|
||||||
|
for (QAction *action : findChildren<QAction *>()) {
|
||||||
|
KGlobalAccel::self()->removeAllShortcuts(action);
|
||||||
|
}
|
||||||
|
restoreDisplacedShortcuts();
|
||||||
|
QCoreApplication::quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void reloadConfig()
|
||||||
|
{
|
||||||
|
m_config->reparseConfiguration();
|
||||||
|
const KConfigGroup config = taskManagerConfig(m_config);
|
||||||
|
if (!config.isValid()) {
|
||||||
|
qWarning("No Plasma Task Manager configuration found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<TaskManager::TasksModel::SortMode>(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<TaskManager::TasksModel::GroupMode>(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()
|
||||||
|
{
|
||||||
|
if (const QScreen *screen = QGuiApplication::primaryScreen()) {
|
||||||
|
m_model.setScreenGeometry(screen->geometry());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void useShortcut(int row, bool shifted)
|
||||||
|
{
|
||||||
|
reloadConfig();
|
||||||
|
const QVector<int> tasks = logicalTaskRows(m_model, m_separateLaunchers);
|
||||||
|
if (row >= tasks.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool continuing = row == m_lastShortcut;
|
||||||
|
if (!continuing) {
|
||||||
|
m_cyclePosition = 0;
|
||||||
|
} else {
|
||||||
|
m_cyclePosition += shifted ? -1 : 1;
|
||||||
|
}
|
||||||
|
m_lastShortcut = row;
|
||||||
|
|
||||||
|
if (shifted && !continuing) {
|
||||||
|
m_model.requestNewInstance(m_model.index(tasks.at(row), 0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QModelIndex task = taskToActivate(m_model, tasks.at(row), m_cyclePosition);
|
||||||
|
if (task.isValid()) {
|
||||||
|
m_model.requestActivate(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KSharedConfig::Ptr m_config;
|
||||||
|
TaskManager::ActivityInfo m_activityInfo;
|
||||||
|
TaskManager::TasksModel m_model;
|
||||||
|
ModifierState m_modifierState;
|
||||||
|
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);
|
||||||
|
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"
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
Name: plasma-task-group-shortcuts
|
||||||
|
Version: 0.1.0
|
||||||
|
Release: 12%{?dist}
|
||||||
|
Summary: Application-group shortcuts for the Plasma Task Manager
|
||||||
|
|
||||||
|
License: MIT
|
||||||
|
URL: https://git.ajpanton.se/ajp_anton/fedora-tools
|
||||||
|
Source0: %{name}-%{version}.tar.gz
|
||||||
|
Source1: LICENSE
|
||||||
|
Source2: README.md
|
||||||
|
|
||||||
|
BuildRequires: cmake
|
||||||
|
BuildRequires: extra-cmake-modules
|
||||||
|
BuildRequires: gcc-c++
|
||||||
|
BuildRequires: kf6-kconfig-devel
|
||||||
|
BuildRequires: kf6-kglobalaccel-devel
|
||||||
|
BuildRequires: kf6-kitemmodels-devel
|
||||||
|
BuildRequires: kf6-kservice-devel
|
||||||
|
BuildRequires: libxkbcommon-devel
|
||||||
|
BuildRequires: plasma-wayland-protocols-devel
|
||||||
|
BuildRequires: plasma-workspace-devel >= 6.7
|
||||||
|
BuildRequires: qt6-qtbase-devel
|
||||||
|
BuildRequires: qt6-qtwayland-devel
|
||||||
|
Requires: plasma-workspace >= 6.7
|
||||||
|
|
||||||
|
%description
|
||||||
|
Changes Plasma's Meta+number shortcuts to address application groups while
|
||||||
|
leaving Task Manager windows visually ungrouped. Launcher order and task
|
||||||
|
settings are read from the existing Plasma Task Manager configuration.
|
||||||
|
|
||||||
|
%prep
|
||||||
|
%autosetup
|
||||||
|
|
||||||
|
%build
|
||||||
|
%cmake
|
||||||
|
%cmake_build
|
||||||
|
|
||||||
|
%check
|
||||||
|
%ctest
|
||||||
|
|
||||||
|
%install
|
||||||
|
%cmake_install
|
||||||
|
install -Dpm 0644 %{SOURCE1} \
|
||||||
|
%{buildroot}%{_licensedir}/%{name}/LICENSE
|
||||||
|
install -Dpm 0644 %{SOURCE2} \
|
||||||
|
%{buildroot}%{_docdir}/%{name}/README.md
|
||||||
|
|
||||||
|
%files
|
||||||
|
%license %{_licensedir}/%{name}/LICENSE
|
||||||
|
%doc %{_docdir}/%{name}/README.md
|
||||||
|
%{_bindir}/plasma-task-group-shortcuts
|
||||||
|
%{_datadir}/applications/se.ajpanton.plasma-task-group-shortcuts.desktop
|
||||||
|
%{_sysconfdir}/xdg/autostart/se.ajpanton.plasma-task-group-shortcuts-autostart.desktop
|
||||||
|
|
||||||
|
%changelog
|
||||||
|
* Fri Sep 04 2026 fedora-tools contributors - 0.1.0-12
|
||||||
|
- Initial package
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Plasma Task Group Shortcuts
|
||||||
|
Exec=/usr/bin/plasma-task-group-shortcuts
|
||||||
|
OnlyShowIn=KDE;
|
||||||
|
NoDisplay=true
|
||||||
|
X-KDE-autostart-phase=2
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Plasma Task Group Shortcuts
|
||||||
|
Comment=Activate taskbar application groups with Meta and a number
|
||||||
|
Exec=/usr/bin/plasma-task-group-shortcuts
|
||||||
|
Icon=preferences-system-windows
|
||||||
|
NoDisplay=true
|
||||||
|
X-KDE-Wayland-Interfaces=org_kde_plasma_window_management,org_kde_kwin_keystate
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "taskselection.h"
|
||||||
|
|
||||||
|
#include <QAbstractItemModel>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include <abstracttasksmodel.h>
|
||||||
|
#include <tasktools.h>
|
||||||
|
|
||||||
|
using TaskManager::AbstractTasksModel;
|
||||||
|
|
||||||
|
QModelIndex taskToActivate(const QAbstractItemModel &model, int row, int cyclePosition)
|
||||||
|
{
|
||||||
|
const QModelIndex task = model.index(row, 0);
|
||||||
|
if (!task.isValid() || model.rowCount(task) == 0) {
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int childCount = model.rowCount(task);
|
||||||
|
const int childRow = ((cyclePosition % childCount) + childCount) % childCount;
|
||||||
|
return model.index(childRow, 0, task);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<int> logicalTaskRows(const QAbstractItemModel &model, bool separateLaunchers)
|
||||||
|
{
|
||||||
|
QVector<int> rows;
|
||||||
|
rows.reserve(model.rowCount());
|
||||||
|
if (separateLaunchers) {
|
||||||
|
for (int row = 0; row < model.rowCount(); ++row) {
|
||||||
|
rows.append(row);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, int> launcherTargets;
|
||||||
|
QSet<int> mergedWindows;
|
||||||
|
for (int launcherRow = 0; launcherRow < model.rowCount(); ++launcherRow) {
|
||||||
|
const QModelIndex launcher = model.index(launcherRow, 0);
|
||||||
|
if (!launcher.data(AbstractTasksModel::IsLauncher).toBool()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int windowRow = 0; windowRow < model.rowCount(); ++windowRow) {
|
||||||
|
const QModelIndex window = model.index(windowRow, 0);
|
||||||
|
if (!window.data(AbstractTasksModel::IsWindow).toBool()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString launcherAppId = launcher.data(AbstractTasksModel::AppId).toString();
|
||||||
|
const QString windowAppId = window.data(AbstractTasksModel::AppId).toString();
|
||||||
|
const QUrl launcherUrl = launcher.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl();
|
||||||
|
const QUrl windowUrl = window.data(AbstractTasksModel::LauncherUrlWithoutIcon).toUrl();
|
||||||
|
if ((!launcherAppId.isEmpty() && launcherAppId == windowAppId)
|
||||||
|
|| (launcherUrl.isValid() && windowUrl.isValid()
|
||||||
|
&& TaskManager::launcherUrlsMatch(launcherUrl, windowUrl, TaskManager::IgnoreQueryItems))) {
|
||||||
|
launcherTargets.insert(launcherRow, windowRow);
|
||||||
|
mergedWindows.insert(windowRow);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int row = 0; row < model.rowCount(); ++row) {
|
||||||
|
if (launcherTargets.contains(row)) {
|
||||||
|
rows.append(launcherTargets.value(row));
|
||||||
|
} else if (!mergedWindows.contains(row)) {
|
||||||
|
rows.append(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QModelIndex>
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
class QAbstractItemModel;
|
||||||
|
|
||||||
|
QModelIndex taskToActivate(const QAbstractItemModel &model, int row, int cyclePosition);
|
||||||
|
QVector<int> logicalTaskRows(const QAbstractItemModel &model, bool separateLaunchers);
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "../keyboardlayout.h"
|
||||||
|
|
||||||
|
#include <QTest>
|
||||||
|
|
||||||
|
class KeyboardLayoutTest : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void mapsNumberRow();
|
||||||
|
void rejectsInvalidNumber();
|
||||||
|
};
|
||||||
|
|
||||||
|
void KeyboardLayoutTest::mapsNumberRow()
|
||||||
|
{
|
||||||
|
QCOMPARE(numberRowKey(u"se", u"", 1, false).value(), Qt::Key_1);
|
||||||
|
QCOMPARE(numberRowKey(u"se", u"", 1, true).value(), Qt::Key_Exclam);
|
||||||
|
QCOMPARE(numberRowKey(u"se", u"", 4, true).value(), Qt::Key_currency);
|
||||||
|
QCOMPARE(numberRowKey(u"us", u"", 2, true).value(), Qt::Key_At);
|
||||||
|
QCOMPARE(numberRowKey(u"de", u"", 7, true).value(), Qt::Key_Slash);
|
||||||
|
QCOMPARE(numberRowKey(u"fr", u"", 1, false).value(), Qt::Key_Ampersand);
|
||||||
|
QCOMPARE(numberRowKey(u"fr", u"", 1, true).value(), Qt::Key_1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void KeyboardLayoutTest::rejectsInvalidNumber()
|
||||||
|
{
|
||||||
|
QVERIFY(!numberRowKey(u"se", u"", 0, false));
|
||||||
|
QVERIFY(!numberRowKey(u"se", u"", 10, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_GUILESS_MAIN(KeyboardLayoutTest)
|
||||||
|
|
||||||
|
#include "test-keyboardlayout.moc"
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "../taskselection.h"
|
||||||
|
|
||||||
|
#include <QStandardItemModel>
|
||||||
|
#include <QTest>
|
||||||
|
|
||||||
|
#include <abstracttasksmodel.h>
|
||||||
|
|
||||||
|
using TaskManager::AbstractTasksModel;
|
||||||
|
|
||||||
|
class TaskSelectionTest : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void returnsTopLevelTask();
|
||||||
|
void selectsChildByCyclePosition();
|
||||||
|
void startsWithFirstChild();
|
||||||
|
void keepsSeparateLaunchersAsSlots();
|
||||||
|
void mergesLauncherWithRunningApplication();
|
||||||
|
};
|
||||||
|
|
||||||
|
void TaskSelectionTest::returnsTopLevelTask()
|
||||||
|
{
|
||||||
|
QStandardItemModel model;
|
||||||
|
model.appendRow(new QStandardItem(QStringLiteral("Dolphin")));
|
||||||
|
|
||||||
|
QCOMPARE(taskToActivate(model, 0, 0), model.index(0, 0));
|
||||||
|
QVERIFY(!taskToActivate(model, 1, 0).isValid());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskSelectionTest::selectsChildByCyclePosition()
|
||||||
|
{
|
||||||
|
QStandardItemModel model;
|
||||||
|
auto *group = new QStandardItem(QStringLiteral("Dolphin"));
|
||||||
|
group->appendRow(new QStandardItem(QStringLiteral("one")));
|
||||||
|
group->appendRow(new QStandardItem(QStringLiteral("two")));
|
||||||
|
group->appendRow(new QStandardItem(QStringLiteral("three")));
|
||||||
|
model.appendRow(group);
|
||||||
|
|
||||||
|
const QModelIndex parent = model.index(0, 0);
|
||||||
|
QCOMPARE(taskToActivate(model, 0, 0), model.index(0, 0, parent));
|
||||||
|
QCOMPARE(taskToActivate(model, 0, 1), model.index(1, 0, parent));
|
||||||
|
QCOMPARE(taskToActivate(model, 0, 2), model.index(2, 0, parent));
|
||||||
|
QCOMPARE(taskToActivate(model, 0, 3), model.index(0, 0, parent));
|
||||||
|
QCOMPARE(taskToActivate(model, 0, -1), model.index(2, 0, parent));
|
||||||
|
QCOMPARE(taskToActivate(model, 0, -2), model.index(1, 0, parent));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskSelectionTest::startsWithFirstChild()
|
||||||
|
{
|
||||||
|
QStandardItemModel model;
|
||||||
|
auto *group = new QStandardItem(QStringLiteral("Firefox"));
|
||||||
|
group->appendRow(new QStandardItem(QStringLiteral("first")));
|
||||||
|
group->appendRow(new QStandardItem(QStringLiteral("second")));
|
||||||
|
model.appendRow(group);
|
||||||
|
|
||||||
|
QCOMPARE(taskToActivate(model, 0, 0), model.index(0, 0, model.index(0, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskSelectionTest::keepsSeparateLaunchersAsSlots()
|
||||||
|
{
|
||||||
|
QStandardItemModel model;
|
||||||
|
model.appendRow(new QStandardItem(QStringLiteral("launcher")));
|
||||||
|
model.appendRow(new QStandardItem(QStringLiteral("window")));
|
||||||
|
|
||||||
|
QCOMPARE(logicalTaskRows(model, true), QVector<int>({0, 1}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskSelectionTest::mergesLauncherWithRunningApplication()
|
||||||
|
{
|
||||||
|
QStandardItemModel model;
|
||||||
|
model.appendRow(new QStandardItem(QStringLiteral("Dolphin launcher")));
|
||||||
|
model.appendRow(new QStandardItem(QStringLiteral("Firefox window")));
|
||||||
|
model.appendRow(new QStandardItem(QStringLiteral("Dolphin windows")));
|
||||||
|
|
||||||
|
model.setData(model.index(0, 0), true, AbstractTasksModel::IsLauncher);
|
||||||
|
model.setData(model.index(0, 0), QStringLiteral("org.kde.dolphin"), AbstractTasksModel::AppId);
|
||||||
|
model.setData(model.index(1, 0), true, AbstractTasksModel::IsWindow);
|
||||||
|
model.setData(model.index(1, 0), QStringLiteral("firefox"), AbstractTasksModel::AppId);
|
||||||
|
model.setData(model.index(2, 0), true, AbstractTasksModel::IsWindow);
|
||||||
|
model.setData(model.index(2, 0), QStringLiteral("org.kde.dolphin"), AbstractTasksModel::AppId);
|
||||||
|
|
||||||
|
QCOMPARE(logicalTaskRows(model, false), QVector<int>({2, 1}));
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_GUILESS_MAIN(TaskSelectionTest)
|
||||||
|
|
||||||
|
#include "test-taskselection.moc"
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
tool_dir="$repo_root/plasma-task-group-shortcuts"
|
||||||
|
topdir="$repo_root/rpmbuild"
|
||||||
|
source_dir="$topdir/SOURCES/plasma-task-group-shortcuts-0.1.0"
|
||||||
|
|
||||||
|
mkdir -p "$topdir"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||||
|
mkdir -p "$source_dir/tests"
|
||||||
|
install -m 0644 "$tool_dir/CMakeLists.txt" "$source_dir/CMakeLists.txt"
|
||||||
|
install -m 0644 "$tool_dir/keyboardlayout.cpp" "$source_dir/keyboardlayout.cpp"
|
||||||
|
install -m 0644 "$tool_dir/keyboardlayout.h" "$source_dir/keyboardlayout.h"
|
||||||
|
install -m 0644 "$tool_dir/main.cpp" "$source_dir/main.cpp"
|
||||||
|
install -m 0644 "$tool_dir/taskselection.cpp" "$source_dir/taskselection.cpp"
|
||||||
|
install -m 0644 "$tool_dir/taskselection.h" "$source_dir/taskselection.h"
|
||||||
|
install -m 0644 "$tool_dir/tests/test-taskselection.cpp" \
|
||||||
|
"$source_dir/tests/test-taskselection.cpp"
|
||||||
|
install -m 0644 "$tool_dir/tests/test-keyboardlayout.cpp" \
|
||||||
|
"$source_dir/tests/test-keyboardlayout.cpp"
|
||||||
|
install -m 0644 "$tool_dir/se.ajpanton.plasma-task-group-shortcuts.desktop" \
|
||||||
|
"$source_dir/se.ajpanton.plasma-task-group-shortcuts.desktop"
|
||||||
|
install -m 0644 "$tool_dir/se.ajpanton.plasma-task-group-shortcuts-autostart.desktop" \
|
||||||
|
"$source_dir/se.ajpanton.plasma-task-group-shortcuts-autostart.desktop"
|
||||||
|
tar -C "$topdir/SOURCES" -czf \
|
||||||
|
"$topdir/SOURCES/plasma-task-group-shortcuts-0.1.0.tar.gz" \
|
||||||
|
plasma-task-group-shortcuts-0.1.0
|
||||||
|
install -m 0644 "$repo_root/LICENSE" "$topdir/SOURCES/LICENSE"
|
||||||
|
install -m 0644 "$repo_root/README.md" "$topdir/SOURCES/README.md"
|
||||||
|
|
||||||
|
rpmbuild \
|
||||||
|
--define "_topdir $topdir" \
|
||||||
|
-bb "$tool_dir/plasma-task-group-shortcuts.spec"
|
||||||
Reference in New Issue
Block a user