Preview task-group selections until Meta is released
This commit is contained in:
@@ -82,13 +82,19 @@ Meta—or pressing a different number—ends the current cycle. The shortcuts
|
|||||||
follow the physical number-row keys for every keyboard layout configured in
|
follow the physical number-row keys for every keyboard layout configured in
|
||||||
KWin, including layouts where those keys do not produce digits without Shift.
|
KWin, including layouts where those keys do not produce digits without Shift.
|
||||||
|
|
||||||
|
A small thumbnail above the selected taskbar button shows the selection while
|
||||||
|
Meta is held, even for single-window groups. Only releasing Meta activates the
|
||||||
|
selected window, so Alt+Tab can return to the application used before the cycle.
|
||||||
|
Esc cancels the selection; another number press starts a fresh cycle without
|
||||||
|
releasing Meta. Launcher and new-instance actions still run immediately.
|
||||||
|
|
||||||
With **Keep launchers separate** disabled, a pinned launcher and its running
|
With **Keep launchers separate** disabled, a pinned launcher and its running
|
||||||
application share one shortcut position. When enabled, the launcher is its own
|
application share one shortcut position. When enabled, the launcher is its own
|
||||||
position and the application's windows form another. **Hide launcher when
|
position and the application's windows form another. **Hide launcher when
|
||||||
application starts** determines whether that separate launcher remains while
|
application starts** determines whether that separate launcher remains while
|
||||||
the application is running. Other groups retain the Task Manager's sort order.
|
the application is running. Other groups retain the Task Manager's sort order.
|
||||||
|
|
||||||
The first-window behavior and both Shift behaviors can be changed through
|
The first-window behaviour and both Shift behaviours can be changed through
|
||||||
Fedora Tools settings. Changes take effect with the next shortcut press.
|
Fedora Tools settings. Changes take effect with the next shortcut press.
|
||||||
|
|
||||||
If application grouping is disabled—or an application is excluded from
|
If application grouping is disabled—or an application is excluded from
|
||||||
|
|||||||
@@ -14,17 +14,31 @@ list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
|
|||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
include(CTest)
|
include(CTest)
|
||||||
|
|
||||||
find_package(Qt6 REQUIRED COMPONENTS DBus Gui WaylandClient)
|
find_package(Qt6 REQUIRED COMPONENTS DBus Gui Quick WaylandClient)
|
||||||
find_package(KF6 REQUIRED COMPONENTS Config GlobalAccel Service)
|
find_package(KF6 REQUIRED COMPONENTS Config GlobalAccel Service)
|
||||||
find_package(LibTaskManager REQUIRED)
|
find_package(LibTaskManager REQUIRED)
|
||||||
|
find_package(Plasma REQUIRED)
|
||||||
find_package(PlasmaWaylandProtocols REQUIRED)
|
find_package(PlasmaWaylandProtocols REQUIRED)
|
||||||
find_package(XKB REQUIRED)
|
find_package(XKB REQUIRED)
|
||||||
|
|
||||||
|
# KWin caches both QML components and directory listings for its whole session.
|
||||||
|
# Changed helper code needs a new directory, not just a new filename.
|
||||||
|
file(SHA256 "${CMAKE_CURRENT_SOURCE_DIR}/TaskbarGeometry.qml" geometry_hash)
|
||||||
|
set(geometry_dir "plasma-task-group-shortcuts/${geometry_hash}")
|
||||||
|
configure_file(TaskbarGeometry.qml "qml/${geometry_hash}/TaskbarGeometry.qml" COPYONLY)
|
||||||
|
|
||||||
add_executable(plasma-task-group-shortcuts
|
add_executable(plasma-task-group-shortcuts
|
||||||
keyboardlayout.cpp
|
keyboardlayout.cpp
|
||||||
main.cpp
|
main.cpp
|
||||||
|
pendingselection.cpp
|
||||||
|
previewplacement.cpp
|
||||||
|
taskbargeometry.cpp
|
||||||
taskselection.cpp
|
taskselection.cpp
|
||||||
|
windowpreview.cpp
|
||||||
)
|
)
|
||||||
|
target_compile_definitions(plasma-task-group-shortcuts PRIVATE
|
||||||
|
GEOMETRY_SCRIPT_PATH="${CMAKE_INSTALL_FULL_DATADIR}/${geometry_dir}/TaskbarGeometry.qml")
|
||||||
|
qt_add_resources(plasma-task-group-shortcuts preview PREFIX "/" FILES Preview.qml)
|
||||||
qt6_generate_wayland_protocol_client_sources(plasma-task-group-shortcuts
|
qt6_generate_wayland_protocol_client_sources(plasma-task-group-shortcuts
|
||||||
PRIVATE_CODE
|
PRIVATE_CODE
|
||||||
FILES ${PLASMA_WAYLAND_PROTOCOLS_DIR}/keystate.xml
|
FILES ${PLASMA_WAYLAND_PROTOCOLS_DIR}/keystate.xml
|
||||||
@@ -32,15 +46,18 @@ qt6_generate_wayland_protocol_client_sources(plasma-task-group-shortcuts
|
|||||||
target_link_libraries(plasma-task-group-shortcuts PRIVATE
|
target_link_libraries(plasma-task-group-shortcuts PRIVATE
|
||||||
Qt6::Gui
|
Qt6::Gui
|
||||||
Qt6::DBus
|
Qt6::DBus
|
||||||
|
Qt6::Quick
|
||||||
Qt6::WaylandClient
|
Qt6::WaylandClient
|
||||||
KF6::ConfigCore
|
KF6::ConfigCore
|
||||||
KF6::GlobalAccel
|
KF6::GlobalAccel
|
||||||
KF6::Service
|
KF6::Service
|
||||||
PW::LibTaskManager
|
PW::LibTaskManager
|
||||||
|
Plasma::Plasma
|
||||||
XKB::XKB
|
XKB::XKB
|
||||||
)
|
)
|
||||||
|
|
||||||
install(TARGETS plasma-task-group-shortcuts DESTINATION ${CMAKE_INSTALL_BINDIR})
|
install(TARGETS plasma-task-group-shortcuts DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||||
|
install(FILES TaskbarGeometry.qml DESTINATION "${CMAKE_INSTALL_DATADIR}/${geometry_dir}")
|
||||||
install(FILES se.ajpanton.plasma-task-group-shortcuts.desktop
|
install(FILES se.ajpanton.plasma-task-group-shortcuts.desktop
|
||||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
||||||
install(FILES se.ajpanton.plasma-task-group-shortcuts-autostart.desktop
|
install(FILES se.ajpanton.plasma-task-group-shortcuts-autostart.desktop
|
||||||
@@ -71,4 +88,14 @@ if(BUILD_TESTING)
|
|||||||
XKB::XKB
|
XKB::XKB
|
||||||
)
|
)
|
||||||
add_test(NAME keyboardlayout COMMAND test-keyboardlayout)
|
add_test(NAME keyboardlayout COMMAND test-keyboardlayout)
|
||||||
|
|
||||||
|
add_executable(test-pendingselection tests/test-pendingselection.cpp pendingselection.cpp)
|
||||||
|
target_link_libraries(test-pendingselection PRIVATE Qt6::Test Qt6::Gui)
|
||||||
|
add_test(NAME pendingselection COMMAND test-pendingselection)
|
||||||
|
|
||||||
|
add_executable(test-preview tests/test-preview.cpp taskbargeometry.cpp previewplacement.cpp)
|
||||||
|
target_compile_definitions(test-preview PRIVATE GEOMETRY_SCRIPT_PATH="${CMAKE_CURRENT_BINARY_DIR}/qml/${geometry_hash}/TaskbarGeometry.qml")
|
||||||
|
qt_add_resources(test-preview preview PREFIX "/" FILES Preview.qml)
|
||||||
|
target_link_libraries(test-preview PRIVATE Qt6::Test Qt6::Quick Qt6::DBus Plasma::Plasma PW::LibTaskManager KF6::Service)
|
||||||
|
add_test(NAME preview COMMAND test-preview -platform offscreen)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.plasma.core as PlasmaCore
|
||||||
|
import org.kde.plasma.components as PlasmaComponents
|
||||||
|
import org.kde.taskmanager as TaskManager
|
||||||
|
import org.kde.pipewire as PipeWire
|
||||||
|
|
||||||
|
PlasmaCore.Dialog {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string windowId: ""
|
||||||
|
property string windowTitle: ""
|
||||||
|
property int position: 0
|
||||||
|
property int count: 0
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
type: PlasmaCore.Dialog.Tooltip
|
||||||
|
// A Qt tooltip needs a parent surface; this standalone helper has none.
|
||||||
|
// Plasma's tooltip role still keeps it out of the taskbar and focus history.
|
||||||
|
flags: Qt.Tool | Qt.FramelessWindowHint | Qt.WindowDoesNotAcceptFocus | Qt.WindowTransparentForInput
|
||||||
|
hideOnWindowDeactivate: false
|
||||||
|
|
||||||
|
mainItem: ColumnLayout {
|
||||||
|
width: Kirigami.Units.gridUnit * 18
|
||||||
|
height: implicitHeight
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: width * 9 / 16
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: thumbnail
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.visible && root.windowId !== ""
|
||||||
|
sourceComponent: PipeWire.PipeWireSourceItem {
|
||||||
|
nodeId: request.nodeId
|
||||||
|
objectSerial: request.objectSerial
|
||||||
|
|
||||||
|
TaskManager.ScreencastingRequest {
|
||||||
|
id: request
|
||||||
|
uuid: root.windowId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaComponents.Label {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: !thumbnail.item || !thumbnail.item.ready
|
||||||
|
text: qsTr("Window preview unavailable")
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaComponents.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: root.windowTitle
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaComponents.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: qsTr("%1 of %2 · Release Meta to switch · Esc to cancel").arg(root.position).arg(root.count)
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import org.kde.kwin as KWin
|
||||||
|
import org.kde.plasma.workspace.dbus as DBus
|
||||||
|
|
||||||
|
// KWin knows the actual task-button geometry published by Plasma, including
|
||||||
|
// panel placement and scaling. No windows or Plasma settings are changed here.
|
||||||
|
DBus.SignalWatcher {
|
||||||
|
service: "se.ajpanton.plasma-task-group-shortcuts"
|
||||||
|
path: "/TaskbarGeometry"
|
||||||
|
iface: "se.ajpanton.PlasmaTaskGroupShortcuts.TaskbarGeometry"
|
||||||
|
|
||||||
|
function dbuslookup(serial, uuid) {
|
||||||
|
// D-Bus supplies a boxed string; normalize before strict comparison.
|
||||||
|
const windowId = String(uuid);
|
||||||
|
const window = KWin.Workspace.windows.find(w => String(w.internalId) === windowId);
|
||||||
|
const rect = window ? window.iconGeometry : Qt.rect(0, 0, 0, 0);
|
||||||
|
send("complete", [serial, rect.x, rect.y, rect.width, rect.height], "(idddd)");
|
||||||
|
}
|
||||||
|
|
||||||
|
function send(member, args, signature) {
|
||||||
|
DBus.SessionBus.asyncCall({
|
||||||
|
service, path, iface, member, signature, arguments: args
|
||||||
|
}, () => {}, result => console.warn("Taskbar geometry:", result.error.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: send("bridgeReady", [], "()")
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
#include "keyboardlayout.h"
|
#include "keyboardlayout.h"
|
||||||
|
#include "pendingselection.h"
|
||||||
|
#include "taskbargeometry.h"
|
||||||
#include "taskselection.h"
|
#include "taskselection.h"
|
||||||
|
#include "windowpreview.h"
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QCommandLineOption>
|
#include <QCommandLineOption>
|
||||||
@@ -29,6 +32,7 @@
|
|||||||
#include <abstracttasksmodel.h>
|
#include <abstracttasksmodel.h>
|
||||||
#include <activityinfo.h>
|
#include <activityinfo.h>
|
||||||
#include <tasksmodel.h>
|
#include <tasksmodel.h>
|
||||||
|
#include <plasma/plasma.h>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -109,7 +113,7 @@ void rememberDisplacedShortcuts(int number, const QList<QKeySequence> &sequences
|
|||||||
|
|
||||||
void restoreDisplacedShortcuts()
|
void restoreDisplacedShortcuts()
|
||||||
{
|
{
|
||||||
for (int number = 1; number <= 9; ++number) {
|
for (int number = 0; number <= 9; ++number) {
|
||||||
KConfigGroup group = displacedShortcutGroup(number);
|
KConfigGroup group = displacedShortcutGroup(number);
|
||||||
if (!group.readEntry("Saved", false)) {
|
if (!group.readEntry("Saved", false)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -137,10 +141,15 @@ void restoreDisplacedShortcuts()
|
|||||||
KGlobalAccel::cleanComponent(QString::fromLatin1(componentId));
|
KGlobalAccel::cleanComponent(QString::fromLatin1(componentId));
|
||||||
}
|
}
|
||||||
|
|
||||||
KConfigGroup taskManagerConfig(const KSharedConfig::Ptr &config)
|
struct PanelConfig {
|
||||||
|
KConfigGroup general;
|
||||||
|
int location = Plasma::Types::BottomEdge;
|
||||||
|
};
|
||||||
|
|
||||||
|
PanelConfig taskManagerConfig(const KSharedConfig::Ptr &config)
|
||||||
{
|
{
|
||||||
const KConfigGroup containments(config, QStringLiteral("Containments"));
|
const KConfigGroup containments(config, QStringLiteral("Containments"));
|
||||||
KConfigGroup fallback;
|
PanelConfig fallback;
|
||||||
for (const QString &containmentId : containments.groupList()) {
|
for (const QString &containmentId : containments.groupList()) {
|
||||||
const KConfigGroup containment = containments.group(containmentId);
|
const KConfigGroup containment = containments.group(containmentId);
|
||||||
const KConfigGroup applets = containment.group(QStringLiteral("Applets"));
|
const KConfigGroup applets = containment.group(QStringLiteral("Applets"));
|
||||||
@@ -148,11 +157,12 @@ KConfigGroup taskManagerConfig(const KSharedConfig::Ptr &config)
|
|||||||
const KConfigGroup applet = applets.group(appletId);
|
const KConfigGroup applet = applets.group(appletId);
|
||||||
if (applet.readEntry("plugin") == QLatin1String("org.kde.plasma.taskmanager")) {
|
if (applet.readEntry("plugin") == QLatin1String("org.kde.plasma.taskmanager")) {
|
||||||
const KConfigGroup general = applet.group(QStringLiteral("Configuration")).group(QStringLiteral("General"));
|
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) {
|
if (containment.readEntry("lastScreen", -1) == 0) {
|
||||||
return general;
|
return panel;
|
||||||
}
|
}
|
||||||
if (!fallback.isValid()) {
|
if (!fallback.general.isValid()) {
|
||||||
fallback = general;
|
fallback = panel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,6 +193,11 @@ public:
|
|||||||
return m_shiftPressed;
|
return m_shiftPressed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool metaPressed() const
|
||||||
|
{
|
||||||
|
return isActive() && m_metaPressed;
|
||||||
|
}
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void released();
|
void released();
|
||||||
|
|
||||||
@@ -218,20 +233,50 @@ public:
|
|||||||
reloadConfig();
|
reloadConfig();
|
||||||
|
|
||||||
connect(&m_activityInfo, &TaskManager::ActivityInfo::currentActivityChanged, this, [this] {
|
connect(&m_activityInfo, &TaskManager::ActivityInfo::currentActivityChanged, this, [this] {
|
||||||
|
m_lastShortcut = -1;
|
||||||
|
m_selection.cancel();
|
||||||
m_model.setActivity(m_activityInfo.currentActivity());
|
m_model.setActivity(m_activityInfo.currentActivity());
|
||||||
});
|
});
|
||||||
|
connect(&m_model, &QAbstractItemModel::modelReset, this, [this] {
|
||||||
|
m_lastShortcut = -1;
|
||||||
|
});
|
||||||
connect(qGuiApp, &QGuiApplication::primaryScreenChanged, this, &ShortcutManager::updateScreen);
|
connect(qGuiApp, &QGuiApplication::primaryScreenChanged, this, &ShortcutManager::updateScreen);
|
||||||
connect(&m_modifierState, &ModifierState::released, this, [this] {
|
connect(&m_modifierState, &ModifierState::released, this, [this] {
|
||||||
m_lastShortcut = -1;
|
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();
|
updateScreen();
|
||||||
|
|
||||||
for (int number = 1; number <= 9; ++number) {
|
for (int number = 0; number <= 9; ++number) {
|
||||||
auto *action = new QAction(this);
|
auto *action = new QAction(this);
|
||||||
action->setObjectName(QStringLiteral("activate task group %1").arg(number));
|
action->setObjectName(number == 0 ? QStringLiteral("cancel task group selection")
|
||||||
action->setText(QStringLiteral("Activate Task Group %1").arg(number));
|
: 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<QKeySequence> sequences = numberRowShortcuts(number);
|
const QList<QKeySequence> sequences = number == 0
|
||||||
|
? QList<QKeySequence>{QKeySequence(Qt::META | Qt::Key_Escape),
|
||||||
|
QKeySequence(Qt::META | Qt::SHIFT | Qt::Key_Escape)}
|
||||||
|
: numberRowShortcuts(number);
|
||||||
rememberDisplacedShortcuts(number, sequences);
|
rememberDisplacedShortcuts(number, sequences);
|
||||||
for (const QKeySequence &sequence : sequences) {
|
for (const QKeySequence &sequence : sequences) {
|
||||||
KGlobalAccel::stealShortcutSystemwide(sequence);
|
KGlobalAccel::stealShortcutSystemwide(sequence);
|
||||||
@@ -240,7 +285,12 @@ public:
|
|||||||
KGlobalAccel::self()->setShortcut(action, sequences, KGlobalAccel::NoAutoloading);
|
KGlobalAccel::self()->setShortcut(action, sequences, KGlobalAccel::NoAutoloading);
|
||||||
|
|
||||||
connect(action, &QAction::triggered, this, [this, number] {
|
connect(action, &QAction::triggered, this, [this, number] {
|
||||||
|
if (number == 0) {
|
||||||
|
m_lastShortcut = -1;
|
||||||
|
m_selection.cancel();
|
||||||
|
} else {
|
||||||
useShortcut(number - 1, m_modifierState.shiftPressed());
|
useShortcut(number - 1, m_modifierState.shiftPressed());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,11 +309,13 @@ private:
|
|||||||
void reloadConfig()
|
void reloadConfig()
|
||||||
{
|
{
|
||||||
m_config->reparseConfiguration();
|
m_config->reparseConfiguration();
|
||||||
const KConfigGroup config = taskManagerConfig(m_config);
|
const PanelConfig panel = taskManagerConfig(m_config);
|
||||||
|
const KConfigGroup config = panel.general;
|
||||||
if (!config.isValid()) {
|
if (!config.isValid()) {
|
||||||
qWarning("No Plasma Task Manager configuration found");
|
qWarning("No Plasma Task Manager configuration found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
m_panelLocation = panel.location;
|
||||||
|
|
||||||
m_model.setLauncherList(config.readEntry("launchers", QStringList()));
|
m_model.setLauncherList(config.readEntry("launchers", QStringList()));
|
||||||
m_model.setFilterByCurrentVirtualDesktop(config.readEntry("showOnlyCurrentDesktop", true));
|
m_model.setFilterByCurrentVirtualDesktop(config.readEntry("showOnlyCurrentDesktop", true));
|
||||||
@@ -286,6 +338,8 @@ private:
|
|||||||
|
|
||||||
void updateScreen()
|
void updateScreen()
|
||||||
{
|
{
|
||||||
|
m_lastShortcut = -1;
|
||||||
|
m_selection.cancel();
|
||||||
if (const QScreen *screen = QGuiApplication::primaryScreen()) {
|
if (const QScreen *screen = QGuiApplication::primaryScreen()) {
|
||||||
m_model.setScreenGeometry(screen->geometry());
|
m_model.setScreenGeometry(screen->geometry());
|
||||||
}
|
}
|
||||||
@@ -296,6 +350,8 @@ private:
|
|||||||
reloadConfig();
|
reloadConfig();
|
||||||
const QVector<int> tasks = logicalTaskRows(m_model, m_separateLaunchers);
|
const QVector<int> tasks = logicalTaskRows(m_model, m_separateLaunchers);
|
||||||
if (row >= tasks.size()) {
|
if (row >= tasks.size()) {
|
||||||
|
m_selection.cancel();
|
||||||
|
m_lastShortcut = -1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,23 +366,29 @@ private:
|
|||||||
} else {
|
} else {
|
||||||
m_cyclePosition += shifted && settings.shiftCyclesBackward ? -1 : 1;
|
m_cyclePosition += shifted && settings.shiftCyclesBackward ? -1 : 1;
|
||||||
}
|
}
|
||||||
m_lastShortcut = row;
|
const bool held = m_modifierState.metaPressed();
|
||||||
|
m_lastShortcut = held ? row : -1;
|
||||||
|
|
||||||
if (shifted && !continuing && settings.initialShiftOpensNewInstance) {
|
if (shifted && !continuing && settings.initialShiftOpensNewInstance) {
|
||||||
|
m_selection.cancel();
|
||||||
m_model.requestNewInstance(m_model.index(tasks.at(row), 0));
|
m_model.requestNewInstance(m_model.index(tasks.at(row), 0));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QModelIndex task = taskToActivate(m_model, tasks.at(row), m_cyclePosition);
|
const QModelIndex task = taskToActivate(m_model, tasks.at(row), m_cyclePosition);
|
||||||
if (task.isValid()) {
|
// A quick key release can reach us before the shortcut's D-Bus signal.
|
||||||
m_model.requestActivate(task);
|
// 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;
|
KSharedConfig::Ptr m_config;
|
||||||
TaskManager::ActivityInfo m_activityInfo;
|
TaskManager::ActivityInfo m_activityInfo;
|
||||||
TaskManager::TasksModel m_model;
|
TaskManager::TasksModel m_model;
|
||||||
|
PendingSelection m_selection{m_model};
|
||||||
|
WindowPreview m_preview;
|
||||||
|
TaskbarGeometry m_taskbarGeometry;
|
||||||
ModifierState m_modifierState;
|
ModifierState m_modifierState;
|
||||||
|
int m_panelLocation = Plasma::Types::BottomEdge;
|
||||||
bool m_separateLaunchers = true;
|
bool m_separateLaunchers = true;
|
||||||
int m_lastShortcut = -1;
|
int m_lastShortcut = -1;
|
||||||
int m_cyclePosition = 0;
|
int m_cyclePosition = 0;
|
||||||
@@ -342,6 +404,7 @@ int main(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
|
|
||||||
QGuiApplication application(argc, argv);
|
QGuiApplication application(argc, argv);
|
||||||
|
application.setQuitOnLastWindowClosed(false);
|
||||||
QCoreApplication::setQuitLockEnabled(false);
|
QCoreApplication::setQuitLockEnabled(false);
|
||||||
QCoreApplication::setApplicationName(QString::fromLatin1(componentId));
|
QCoreApplication::setApplicationName(QString::fromLatin1(componentId));
|
||||||
QGuiApplication::setApplicationDisplayName(QStringLiteral("Plasma Task Group Shortcuts"));
|
QGuiApplication::setApplicationDisplayName(QStringLiteral("Plasma Task Group Shortcuts"));
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "pendingselection.h"
|
||||||
|
|
||||||
|
PendingSelection::PendingSelection(QAbstractItemModel &model)
|
||||||
|
{
|
||||||
|
connect(&model, &QAbstractItemModel::modelReset, this, &PendingSelection::cancel);
|
||||||
|
connect(&model, &QAbstractItemModel::rowsRemoved, this, [this] {
|
||||||
|
if (!m_task.isValid()) {
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void PendingSelection::select(const QModelIndex &task, bool defer)
|
||||||
|
{
|
||||||
|
m_task = task;
|
||||||
|
if (defer && task.isValid()) {
|
||||||
|
Q_EMIT previewRequested(task);
|
||||||
|
} else {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PendingSelection::finish()
|
||||||
|
{
|
||||||
|
const QPersistentModelIndex task = m_task;
|
||||||
|
cancel();
|
||||||
|
if (task.isValid()) {
|
||||||
|
Q_EMIT activationRequested(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PendingSelection::cancel()
|
||||||
|
{
|
||||||
|
m_task = QPersistentModelIndex();
|
||||||
|
Q_EMIT previewHidden();
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QAbstractItemModel>
|
||||||
|
#include <QPersistentModelIndex>
|
||||||
|
|
||||||
|
// A preview is not an activation: only the final selection enters focus history.
|
||||||
|
class PendingSelection : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit PendingSelection(QAbstractItemModel &model);
|
||||||
|
void select(const QModelIndex &task, bool defer);
|
||||||
|
void finish();
|
||||||
|
void cancel();
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void previewRequested(const QModelIndex &task);
|
||||||
|
void previewHidden();
|
||||||
|
void activationRequested(const QModelIndex &task);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QPersistentModelIndex m_task;
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
Name: plasma-task-group-shortcuts
|
Name: plasma-task-group-shortcuts
|
||||||
Version: 0.1.0
|
Version: 0.1.0
|
||||||
Release: 15%{?dist}
|
Release: 20%{?dist}
|
||||||
Summary: Windows-like application-group shortcuts for the Plasma Task Manager
|
Summary: Windows-like application-group shortcuts for the Plasma Task Manager
|
||||||
|
|
||||||
License: MIT
|
License: MIT
|
||||||
@@ -17,11 +17,18 @@ BuildRequires: kf6-kglobalaccel-devel
|
|||||||
BuildRequires: kf6-kitemmodels-devel
|
BuildRequires: kf6-kitemmodels-devel
|
||||||
BuildRequires: kf6-kservice-devel
|
BuildRequires: kf6-kservice-devel
|
||||||
BuildRequires: libxkbcommon-devel
|
BuildRequires: libxkbcommon-devel
|
||||||
|
BuildRequires: libplasma-devel
|
||||||
BuildRequires: plasma-wayland-protocols-devel
|
BuildRequires: plasma-wayland-protocols-devel
|
||||||
BuildRequires: plasma-workspace-devel >= 6.7
|
BuildRequires: plasma-workspace-devel >= 6.7
|
||||||
BuildRequires: qt6-qtbase-devel
|
BuildRequires: qt6-qtbase-devel
|
||||||
|
BuildRequires: qt6-qtdeclarative-devel
|
||||||
BuildRequires: qt6-qtwayland-devel
|
BuildRequires: qt6-qtwayland-devel
|
||||||
Requires: plasma-workspace >= 6.7
|
Requires: plasma-workspace >= 6.7
|
||||||
|
Requires: kpipewire
|
||||||
|
Requires: kf6-kirigami
|
||||||
|
Requires: libplasma
|
||||||
|
BuildRequires: kpipewire
|
||||||
|
BuildRequires: kf6-kirigami
|
||||||
Provides: fedora-tools-tool
|
Provides: fedora-tools-tool
|
||||||
|
|
||||||
%description
|
%description
|
||||||
@@ -50,15 +57,32 @@ install -Dpm 0644 %{SOURCE2} \
|
|||||||
%license %{_licensedir}/%{name}/LICENSE
|
%license %{_licensedir}/%{name}/LICENSE
|
||||||
%doc %{_docdir}/%{name}/README.md
|
%doc %{_docdir}/%{name}/README.md
|
||||||
%{_bindir}/plasma-task-group-shortcuts
|
%{_bindir}/plasma-task-group-shortcuts
|
||||||
|
%{_datadir}/plasma-task-group-shortcuts/
|
||||||
%{_datadir}/applications/se.ajpanton.plasma-task-group-shortcuts.desktop
|
%{_datadir}/applications/se.ajpanton.plasma-task-group-shortcuts.desktop
|
||||||
%{_sysconfdir}/xdg/autostart/se.ajpanton.plasma-task-group-shortcuts-autostart.desktop
|
%{_sysconfdir}/xdg/autostart/se.ajpanton.plasma-task-group-shortcuts-autostart.desktop
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Thu Sep 10 2026 fedora-tools contributors - 0.1.0-20
|
||||||
|
- Load updated geometry helpers without reusing KWin's cached QML
|
||||||
|
|
||||||
|
* Thu Sep 10 2026 fedora-tools contributors - 0.1.0-19
|
||||||
|
- Normalize D-Bus window IDs when locating the selected taskbar button
|
||||||
|
|
||||||
|
* Thu Sep 10 2026 fedora-tools contributors - 0.1.0-18
|
||||||
|
- Anchor previews to task buttons and defer single-window selection
|
||||||
|
- Cancel and reset the held cycle with Meta+Esc
|
||||||
|
|
||||||
|
* Thu Sep 10 2026 fedora-tools contributors - 0.1.0-17
|
||||||
|
- Show the standalone selection preview without requiring a parent surface
|
||||||
|
|
||||||
|
* Wed Sep 09 2026 fedora-tools contributors - 0.1.0-16
|
||||||
|
- Preview multi-window selections and activate only when Meta is released
|
||||||
|
|
||||||
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-15
|
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-15
|
||||||
- Clarify the package summary
|
- Clarify the package summary
|
||||||
|
|
||||||
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-14
|
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-14
|
||||||
- Add configurable cycle start and Shift behavior
|
- Add configurable cycle start and Shift behaviour
|
||||||
|
|
||||||
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-13
|
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-13
|
||||||
- Advertise the tool to Fedora Tools settings
|
- Advertise the tool to Fedora Tools settings
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "previewplacement.h"
|
||||||
|
|
||||||
|
#include <plasma/plasma.h>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
QPoint previewPosition(const QRect &button, const QRect &screen, const QSize &preview, int panelLocation)
|
||||||
|
{
|
||||||
|
const QRect anchor = button.isValid() ? button : screen;
|
||||||
|
const int centerX = anchor.x() + (anchor.width() - preview.width()) / 2;
|
||||||
|
const int centerY = anchor.y() + (anchor.height() - preview.height()) / 2;
|
||||||
|
QPoint result;
|
||||||
|
switch (panelLocation) {
|
||||||
|
case Plasma::Types::TopEdge:
|
||||||
|
result = {centerX, button.isValid() ? button.bottom() + 1 : screen.top()};
|
||||||
|
break;
|
||||||
|
case Plasma::Types::LeftEdge:
|
||||||
|
result = {button.isValid() ? button.right() + 1 : screen.left(), centerY};
|
||||||
|
break;
|
||||||
|
case Plasma::Types::RightEdge:
|
||||||
|
result = {button.isValid() ? button.left() - preview.width() : screen.right() + 1 - preview.width(), centerY};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result = {centerX, button.isValid() ? button.top() - preview.height() : screen.bottom() + 1 - preview.height()};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
result.setX(std::clamp(result.x(), screen.left(), std::max(screen.left(), screen.right() + 1 - preview.width())));
|
||||||
|
result.setY(std::clamp(result.y(), screen.top(), std::max(screen.top(), screen.bottom() + 1 - preview.height())));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
#include <QRect>
|
||||||
|
#include <QSize>
|
||||||
|
|
||||||
|
QPoint previewPosition(const QRect &button, const QRect &screen, const QSize &preview, int panelLocation);
|
||||||
@@ -5,4 +5,4 @@ Comment=Activate taskbar application groups with Meta and a number
|
|||||||
Exec=/usr/bin/plasma-task-group-shortcuts
|
Exec=/usr/bin/plasma-task-group-shortcuts
|
||||||
Icon=preferences-system-windows
|
Icon=preferences-system-windows
|
||||||
NoDisplay=true
|
NoDisplay=true
|
||||||
X-KDE-Wayland-Interfaces=org_kde_plasma_window_management,org_kde_kwin_keystate
|
X-KDE-Wayland-Interfaces=org_kde_plasma_window_management,org_kde_kwin_keystate,org_kde_plasma_shell,zkde_screencast_unstable_v1
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "taskbargeometry.h"
|
||||||
|
|
||||||
|
#include <QDBusConnection>
|
||||||
|
#include <QDBusInterface>
|
||||||
|
#include <QDBusReply>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr auto scriptName = "se.ajpanton.task-group-shortcuts.geometry";
|
||||||
|
constexpr auto objectPath = "/TaskbarGeometry";
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskbarGeometry::TaskbarGeometry(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
auto bus = QDBusConnection::sessionBus();
|
||||||
|
if (!bus.registerObject(QString::fromLatin1(objectPath), this, QDBusConnection::ExportScriptableContents)) {
|
||||||
|
qFatal("Cannot register the taskbar geometry interface");
|
||||||
|
}
|
||||||
|
QDBusInterface scripting(QStringLiteral("org.kde.KWin"), QStringLiteral("/Scripting"),
|
||||||
|
QStringLiteral("org.kde.kwin.Scripting"));
|
||||||
|
// Replace any copy left behind by an earlier process that was terminated.
|
||||||
|
scripting.call(QStringLiteral("unloadScript"), QString::fromLatin1(scriptName));
|
||||||
|
const QDBusReply<int> loaded = scripting.call(QStringLiteral("loadDeclarativeScript"),
|
||||||
|
QStringLiteral(GEOMETRY_SCRIPT_PATH), QString::fromLatin1(scriptName));
|
||||||
|
if (!loaded.isValid() || loaded.value() < 0) {
|
||||||
|
qWarning() << "Cannot load taskbar geometry script:" << loaded.error().message();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_loaded = true;
|
||||||
|
QDBusInterface script(QStringLiteral("org.kde.KWin"),
|
||||||
|
QStringLiteral("/Scripting/Script%1").arg(loaded.value()), QStringLiteral("org.kde.kwin.Script"));
|
||||||
|
const QDBusReply<void> started = script.call(QStringLiteral("run"));
|
||||||
|
if (!started.isValid()) {
|
||||||
|
qWarning() << "Cannot start taskbar geometry script:" << started.error().message();
|
||||||
|
m_loaded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskbarGeometry::~TaskbarGeometry()
|
||||||
|
{
|
||||||
|
QDBusInterface scripting(QStringLiteral("org.kde.KWin"), QStringLiteral("/Scripting"),
|
||||||
|
QStringLiteral("org.kde.kwin.Scripting"));
|
||||||
|
scripting.call(QStringLiteral("unloadScript"), QString::fromLatin1(scriptName));
|
||||||
|
QDBusConnection::sessionBus().unregisterObject(QString::fromLatin1(objectPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskbarGeometry::request(const QString &uuid)
|
||||||
|
{
|
||||||
|
++m_serial;
|
||||||
|
m_uuid = uuid;
|
||||||
|
if (m_ready) {
|
||||||
|
Q_EMIT lookup(m_serial, uuid);
|
||||||
|
} else if (!m_loaded) {
|
||||||
|
Q_EMIT received({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskbarGeometry::cancel()
|
||||||
|
{
|
||||||
|
++m_serial;
|
||||||
|
m_uuid.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskbarGeometry::bridgeReady()
|
||||||
|
{
|
||||||
|
m_ready = true;
|
||||||
|
if (!m_uuid.isEmpty()) {
|
||||||
|
Q_EMIT lookup(m_serial, m_uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskbarGeometry::complete(int serial, double x, double y, double width, double height)
|
||||||
|
{
|
||||||
|
if (serial == m_serial && !m_uuid.isEmpty()) {
|
||||||
|
Q_EMIT received(QRectF(x, y, width, height));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QRectF>
|
||||||
|
|
||||||
|
class TaskbarGeometry : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_CLASSINFO("D-Bus Interface", "se.ajpanton.PlasmaTaskGroupShortcuts.TaskbarGeometry")
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit TaskbarGeometry(QObject *parent = nullptr);
|
||||||
|
~TaskbarGeometry() override;
|
||||||
|
void request(const QString &uuid);
|
||||||
|
void cancel();
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
Q_SCRIPTABLE void lookup(int serial, const QString &uuid);
|
||||||
|
void received(const QRectF &geometry);
|
||||||
|
|
||||||
|
public Q_SLOTS:
|
||||||
|
Q_SCRIPTABLE void bridgeReady();
|
||||||
|
Q_SCRIPTABLE void complete(int serial, double x, double y, double width, double height);
|
||||||
|
|
||||||
|
private:
|
||||||
|
int m_serial = 0;
|
||||||
|
bool m_ready = false;
|
||||||
|
bool m_loaded = false;
|
||||||
|
QString m_uuid;
|
||||||
|
};
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "../pendingselection.h"
|
||||||
|
|
||||||
|
#include <QSignalSpy>
|
||||||
|
#include <QStandardItemModel>
|
||||||
|
#include <QTest>
|
||||||
|
|
||||||
|
class PendingSelectionTest : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void singleWindowWaitsForRelease()
|
||||||
|
{
|
||||||
|
QStandardItemModel model(1, 1);
|
||||||
|
PendingSelection selection(model);
|
||||||
|
QSignalSpy activated(&selection, &PendingSelection::activationRequested);
|
||||||
|
QSignalSpy previewed(&selection, &PendingSelection::previewRequested);
|
||||||
|
selection.select(model.index(0, 0), true);
|
||||||
|
QCOMPARE(previewed.count(), 1);
|
||||||
|
QCOMPARE(activated.count(), 0);
|
||||||
|
selection.finish();
|
||||||
|
QCOMPARE(activated.count(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancellationAllowsAnotherSelectionBeforeRelease()
|
||||||
|
{
|
||||||
|
QStandardItemModel model(2, 1);
|
||||||
|
PendingSelection selection(model);
|
||||||
|
QSignalSpy activated(&selection, &PendingSelection::activationRequested);
|
||||||
|
selection.select(model.index(1, 0), true);
|
||||||
|
selection.cancel();
|
||||||
|
selection.select(model.index(0, 0), true);
|
||||||
|
QCOMPARE(activated.count(), 0);
|
||||||
|
selection.finish();
|
||||||
|
QCOMPARE(activated.count(), 1);
|
||||||
|
QCOMPARE(activated.first().first().value<QModelIndex>(), model.index(0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void activatesOnlyFinalSelection()
|
||||||
|
{
|
||||||
|
QStandardItemModel model(3, 1);
|
||||||
|
PendingSelection selection(model);
|
||||||
|
QSignalSpy activated(&selection, &PendingSelection::activationRequested);
|
||||||
|
QSignalSpy previewed(&selection, &PendingSelection::previewRequested);
|
||||||
|
QSignalSpy hidden(&selection, &PendingSelection::previewHidden);
|
||||||
|
|
||||||
|
selection.select(model.index(0, 0), true);
|
||||||
|
selection.select(model.index(1, 0), true);
|
||||||
|
selection.select(model.index(2, 0), true);
|
||||||
|
QCOMPARE(previewed.count(), 3);
|
||||||
|
QCOMPARE(activated.count(), 0);
|
||||||
|
selection.finish();
|
||||||
|
QCOMPARE(hidden.count(), 1);
|
||||||
|
QCOMPARE(activated.count(), 1);
|
||||||
|
QCOMPARE(activated.first().first().value<QModelIndex>(), model.index(2, 0));
|
||||||
|
selection.finish();
|
||||||
|
QCOMPARE(activated.count(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void switchingGroupsAbandonsPreviousSelection()
|
||||||
|
{
|
||||||
|
QStandardItemModel model;
|
||||||
|
for (const auto &name : {"Dolphin", "Firefox"}) {
|
||||||
|
auto *group = new QStandardItem(QString::fromLatin1(name));
|
||||||
|
group->appendRow(new QStandardItem(QStringLiteral("first")));
|
||||||
|
group->appendRow(new QStandardItem(QStringLiteral("second")));
|
||||||
|
model.appendRow(group);
|
||||||
|
}
|
||||||
|
PendingSelection selection(model);
|
||||||
|
QSignalSpy activated(&selection, &PendingSelection::activationRequested);
|
||||||
|
selection.select(model.index(1, 0, model.index(0, 0)), true);
|
||||||
|
const QModelIndex firefox = model.index(0, 0, model.index(1, 0));
|
||||||
|
selection.select(firefox, true);
|
||||||
|
QCOMPARE(activated.count(), 0);
|
||||||
|
selection.finish();
|
||||||
|
QCOMPARE(activated.count(), 1);
|
||||||
|
QCOMPARE(activated.first().first().value<QModelIndex>(), firefox);
|
||||||
|
}
|
||||||
|
|
||||||
|
void immediateActionReplacesPreview()
|
||||||
|
{
|
||||||
|
QStandardItemModel model(2, 1);
|
||||||
|
PendingSelection selection(model);
|
||||||
|
QSignalSpy activated(&selection, &PendingSelection::activationRequested);
|
||||||
|
selection.select(model.index(0, 0), true);
|
||||||
|
selection.select(model.index(1, 0), false);
|
||||||
|
QCOMPARE(activated.count(), 1);
|
||||||
|
QCOMPARE(activated.first().first().value<QModelIndex>(), model.index(1, 0));
|
||||||
|
selection.finish();
|
||||||
|
QCOMPARE(activated.count(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancellationDoesNotActivate()
|
||||||
|
{
|
||||||
|
QStandardItemModel model(1, 1);
|
||||||
|
PendingSelection selection(model);
|
||||||
|
QSignalSpy activated(&selection, &PendingSelection::activationRequested);
|
||||||
|
selection.select(model.index(0, 0), true);
|
||||||
|
selection.cancel();
|
||||||
|
selection.finish();
|
||||||
|
selection.select(model.index(0, 0), true);
|
||||||
|
selection.select({}, true);
|
||||||
|
selection.finish();
|
||||||
|
QCOMPARE(activated.count(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void selectedWindowClosingCancels()
|
||||||
|
{
|
||||||
|
QStandardItemModel model(2, 1);
|
||||||
|
PendingSelection selection(model);
|
||||||
|
QSignalSpy activated(&selection, &PendingSelection::activationRequested);
|
||||||
|
QSignalSpy hidden(&selection, &PendingSelection::previewHidden);
|
||||||
|
selection.select(model.index(0, 0), true);
|
||||||
|
model.removeRow(0);
|
||||||
|
QVERIFY(!hidden.isEmpty());
|
||||||
|
selection.finish();
|
||||||
|
QCOMPARE(activated.count(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void rowChangeKeepsSelectedWindow()
|
||||||
|
{
|
||||||
|
QStandardItemModel model(3, 1);
|
||||||
|
model.setData(model.index(2, 0), QStringLiteral("selected"));
|
||||||
|
PendingSelection selection(model);
|
||||||
|
QSignalSpy activated(&selection, &PendingSelection::activationRequested);
|
||||||
|
selection.select(model.index(2, 0), true);
|
||||||
|
model.removeRow(0);
|
||||||
|
selection.finish();
|
||||||
|
QCOMPARE(activated.count(), 1);
|
||||||
|
QCOMPARE(activated.first().first().value<QModelIndex>().data().toString(), QStringLiteral("selected"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void modelResetCancels()
|
||||||
|
{
|
||||||
|
QStandardItemModel model(1, 1);
|
||||||
|
PendingSelection selection(model);
|
||||||
|
QSignalSpy activated(&selection, &PendingSelection::activationRequested);
|
||||||
|
selection.select(model.index(0, 0), true);
|
||||||
|
model.clear();
|
||||||
|
model.setRowCount(1);
|
||||||
|
selection.finish();
|
||||||
|
QCOMPARE(activated.count(), 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QTEST_GUILESS_MAIN(PendingSelectionTest)
|
||||||
|
#include "test-pendingselection.moc"
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include <QQmlComponent>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
#include <QQuickWindow>
|
||||||
|
#include <QQuickItem>
|
||||||
|
#include <QTest>
|
||||||
|
#include <memory>
|
||||||
|
#include <QDBusConnection>
|
||||||
|
#include <QSignalSpy>
|
||||||
|
#include <plasma/plasma.h>
|
||||||
|
#include "../previewplacement.h"
|
||||||
|
#include "../taskbargeometry.h"
|
||||||
|
#include <abstracttasksmodel.h>
|
||||||
|
#include <tasksmodel.h>
|
||||||
|
|
||||||
|
class PreviewTest : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void placementTracksButtonAndScreenEdges()
|
||||||
|
{
|
||||||
|
const QRect screen(0, 0, 1000, 800);
|
||||||
|
const QSize size(300, 200);
|
||||||
|
QCOMPARE(previewPosition(QRect(400, 760, 100, 40), screen, size, Plasma::Types::BottomEdge), QPoint(300, 560));
|
||||||
|
QCOMPARE(previewPosition(QRect(0, 760, 100, 40), screen, size, Plasma::Types::BottomEdge), QPoint(0, 560));
|
||||||
|
QCOMPARE(previewPosition(QRect(900, 760, 100, 40), screen, size, Plasma::Types::BottomEdge), QPoint(700, 560));
|
||||||
|
QCOMPARE(previewPosition(QRect(400, 0, 100, 40), screen, size, Plasma::Types::TopEdge), QPoint(300, 40));
|
||||||
|
QCOMPARE(previewPosition(QRect(0, 300, 40, 100), screen, size, Plasma::Types::LeftEdge), QPoint(40, 250));
|
||||||
|
QCOMPARE(previewPosition(QRect(960, 300, 40, 100), screen, size, Plasma::Types::RightEdge), QPoint(660, 250));
|
||||||
|
QCOMPARE(previewPosition(QRect(-1000, 760, 100, 40), QRect(-1000, 0, 1000, 800), size,
|
||||||
|
Plasma::Types::BottomEdge), QPoint(-1000, 560));
|
||||||
|
QCOMPARE(previewPosition({}, screen, size, Plasma::Types::BottomEdge), QPoint(350, 600));
|
||||||
|
}
|
||||||
|
|
||||||
|
void geometryBridgeOnWayland()
|
||||||
|
{
|
||||||
|
if (QGuiApplication::platformName() != QLatin1String("wayland")) {
|
||||||
|
QSKIP("Requires KWin's Wayland compositor");
|
||||||
|
}
|
||||||
|
auto bus = QDBusConnection::sessionBus();
|
||||||
|
const QString service = QStringLiteral("se.ajpanton.plasma-task-group-shortcuts");
|
||||||
|
QVERIFY(bus.registerService(service));
|
||||||
|
{
|
||||||
|
TaskbarGeometry geometry;
|
||||||
|
QSignalSpy results(&geometry, &TaskbarGeometry::received);
|
||||||
|
QSignalSpy lookups(&geometry, &TaskbarGeometry::lookup);
|
||||||
|
// The script must respond even when a window has disappeared.
|
||||||
|
geometry.request(QStringLiteral("nonexistent-window"));
|
||||||
|
QTRY_COMPARE(lookups.count(), 1);
|
||||||
|
QTRY_COMPARE(results.count(), 1);
|
||||||
|
QVERIFY(results.first().first().toRectF().isEmpty());
|
||||||
|
geometry.request(QStringLiteral("another-window"));
|
||||||
|
geometry.cancel();
|
||||||
|
QTest::qWait(100);
|
||||||
|
QCOMPARE(results.count(), 1);
|
||||||
|
}
|
||||||
|
QVERIFY(bus.unregisterService(service));
|
||||||
|
}
|
||||||
|
|
||||||
|
void findsPublishedButtonGeometryOnWayland()
|
||||||
|
{
|
||||||
|
if (QGuiApplication::platformName() != QLatin1String("wayland")) {
|
||||||
|
QSKIP("Requires KWin and the plasma-window-management protocol");
|
||||||
|
}
|
||||||
|
TaskManager::TasksModel model;
|
||||||
|
model.setGroupMode(TaskManager::TasksModel::GroupDisabled);
|
||||||
|
QQuickWindow target;
|
||||||
|
target.setTitle(QStringLiteral("Task group geometry test"));
|
||||||
|
target.resize(400, 300);
|
||||||
|
target.show();
|
||||||
|
QTRY_VERIFY(target.isExposed());
|
||||||
|
const auto matching = [&] {
|
||||||
|
return model.match(model.index(0, 0), Qt::DisplayRole, target.title());
|
||||||
|
};
|
||||||
|
QTRY_VERIFY(!matching().isEmpty());
|
||||||
|
const QModelIndex index = matching().first();
|
||||||
|
const QString uuid = index.data(TaskManager::AbstractTasksModel::WinIdList).toList().first().toString();
|
||||||
|
QVERIFY(!uuid.isEmpty());
|
||||||
|
|
||||||
|
QQuickWindow panel;
|
||||||
|
panel.setTitle(QStringLiteral("Task group geometry panel"));
|
||||||
|
panel.resize(1000, 40);
|
||||||
|
panel.show();
|
||||||
|
QTRY_VERIFY(panel.isExposed());
|
||||||
|
const auto panelTask = [&] {
|
||||||
|
return model.match(model.index(0, 0), Qt::DisplayRole, panel.title());
|
||||||
|
};
|
||||||
|
QTRY_VERIFY(!panelTask().isEmpty());
|
||||||
|
QTRY_VERIFY(panelTask().first().data(TaskManager::AbstractTasksModel::Geometry).toRect().isValid());
|
||||||
|
const QPoint origin = panelTask().first().data(TaskManager::AbstractTasksModel::Geometry).toRect().topLeft();
|
||||||
|
const QRect button(origin + QPoint(120, 0), QSize(180, 40));
|
||||||
|
QQuickItem delegate(panel.contentItem());
|
||||||
|
delegate.setPosition(QPointF(120, 0));
|
||||||
|
delegate.setSize(QSizeF(180, 40));
|
||||||
|
model.requestPublishDelegateGeometry(index, button, &delegate);
|
||||||
|
QTest::qWait(100);
|
||||||
|
|
||||||
|
auto bus = QDBusConnection::sessionBus();
|
||||||
|
const QString service = QStringLiteral("se.ajpanton.plasma-task-group-shortcuts");
|
||||||
|
QVERIFY(bus.registerService(service));
|
||||||
|
{
|
||||||
|
TaskbarGeometry geometry;
|
||||||
|
QSignalSpy results(&geometry, &TaskbarGeometry::received);
|
||||||
|
geometry.request(uuid);
|
||||||
|
QTRY_COMPARE(results.count(), 1);
|
||||||
|
QCOMPARE(results.first().first().toRectF(), QRectF(button));
|
||||||
|
}
|
||||||
|
QVERIFY(bus.unregisterService(service));
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadsWithoutTakingFocus()
|
||||||
|
{
|
||||||
|
QTest::failOnWarning(QRegularExpression(QStringLiteral(".*Failed to create .*popup.*")));
|
||||||
|
QQmlEngine engine;
|
||||||
|
QQmlComponent component(&engine, QUrl(QStringLiteral("qrc:/Preview.qml")));
|
||||||
|
std::unique_ptr<QObject> object(component.create());
|
||||||
|
QVERIFY2(object, qPrintable(component.errorString()));
|
||||||
|
auto *window = qobject_cast<QQuickWindow *>(object.get());
|
||||||
|
QVERIFY(window);
|
||||||
|
QVERIFY(!window->isVisible());
|
||||||
|
QCOMPARE(window->type(), Qt::Tool);
|
||||||
|
QVERIFY(!window->transientParent());
|
||||||
|
QVERIFY(window->flags().testFlag(Qt::FramelessWindowHint));
|
||||||
|
QVERIFY(window->flags().testFlag(Qt::WindowDoesNotAcceptFocus));
|
||||||
|
QVERIFY(window->flags().testFlag(Qt::WindowTransparentForInput));
|
||||||
|
window->show();
|
||||||
|
QTRY_VERIFY(window->isExposed());
|
||||||
|
// Wayland can reject a window with an asynchronously delivered close.
|
||||||
|
QTest::qWait(100);
|
||||||
|
QVERIFY(window->isVisible());
|
||||||
|
QVERIFY(window->isExposed());
|
||||||
|
QTRY_VERIFY(window->width() > 0);
|
||||||
|
QTRY_VERIFY(window->height() > 0);
|
||||||
|
QVERIFY(window->flags().testFlag(Qt::WindowDoesNotAcceptFocus));
|
||||||
|
window->setProperty("windowTitle", QStringLiteral("Test window"));
|
||||||
|
window->setProperty("position", 2);
|
||||||
|
window->setProperty("count", 3);
|
||||||
|
QCOMPARE(window->property("windowTitle").toString(), QStringLiteral("Test window"));
|
||||||
|
window->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
void preservesFocusOnWayland()
|
||||||
|
{
|
||||||
|
if (QGuiApplication::platformName() != QLatin1String("wayland")) {
|
||||||
|
QSKIP("Requires a Wayland compositor");
|
||||||
|
}
|
||||||
|
QQuickWindow original;
|
||||||
|
original.resize(400, 300);
|
||||||
|
original.show();
|
||||||
|
QTRY_VERIFY(original.isActive());
|
||||||
|
|
||||||
|
QQmlEngine engine;
|
||||||
|
QQmlComponent component(&engine, QUrl(QStringLiteral("qrc:/Preview.qml")));
|
||||||
|
std::unique_ptr<QObject> object(component.create());
|
||||||
|
QVERIFY2(object, qPrintable(component.errorString()));
|
||||||
|
auto *preview = qobject_cast<QQuickWindow *>(object.get());
|
||||||
|
QVERIFY(preview);
|
||||||
|
preview->show();
|
||||||
|
QTRY_VERIFY(preview->isExposed());
|
||||||
|
QTest::qWait(100);
|
||||||
|
QVERIFY(preview->isVisible());
|
||||||
|
QVERIFY(!preview->isActive());
|
||||||
|
QVERIFY(original.isActive());
|
||||||
|
preview->hide();
|
||||||
|
QTest::qWait(100);
|
||||||
|
QVERIFY(original.isActive());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QTEST_MAIN(PreviewTest)
|
||||||
|
#include "test-preview.moc"
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "windowpreview.h"
|
||||||
|
#include "previewplacement.h"
|
||||||
|
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QQmlComponent>
|
||||||
|
#include <QScreen>
|
||||||
|
#include <abstracttasksmodel.h>
|
||||||
|
|
||||||
|
WindowPreview::WindowPreview()
|
||||||
|
{
|
||||||
|
QQmlComponent component(&m_engine, QUrl(QStringLiteral("qrc:/Preview.qml")));
|
||||||
|
m_window.reset(qobject_cast<QQuickWindow *>(component.create()));
|
||||||
|
if (!m_window) {
|
||||||
|
qFatal("Cannot create window preview: %s", qPrintable(component.errorString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowPreview::show(const QModelIndex &task, int panelLocation)
|
||||||
|
{
|
||||||
|
const QVariantList ids = task.data(TaskManager::AbstractTasksModel::WinIdList).toList();
|
||||||
|
m_window->setProperty("windowId", ids.isEmpty() ? QString() : ids.first().toString());
|
||||||
|
m_window->setProperty("windowTitle", task.data(Qt::DisplayRole).toString());
|
||||||
|
m_window->setProperty("position", task.parent().isValid() ? task.row() + 1 : 1);
|
||||||
|
m_window->setProperty("count", task.parent().isValid() ? task.model()->rowCount(task.parent()) : 1);
|
||||||
|
m_panelLocation = panelLocation;
|
||||||
|
// Plasma sizes the dialog from mainItem when it becomes visible.
|
||||||
|
m_window->show();
|
||||||
|
|
||||||
|
setAnchor({});
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowPreview::setAnchor(const QRect &button)
|
||||||
|
{
|
||||||
|
if (!m_window->isVisible()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QScreen *screen = button.isValid() ? QGuiApplication::screenAt(button.center()) : nullptr;
|
||||||
|
if (!screen) {
|
||||||
|
screen = QGuiApplication::primaryScreen();
|
||||||
|
}
|
||||||
|
if (screen) {
|
||||||
|
m_window->setScreen(screen);
|
||||||
|
m_window->setPosition(previewPosition(button, screen->availableGeometry(), m_window->size(), m_panelLocation));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowPreview::hide()
|
||||||
|
{
|
||||||
|
m_window->hide();
|
||||||
|
m_window->setProperty("windowId", QString());
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QModelIndex>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
#include <QQuickWindow>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
class WindowPreview
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
WindowPreview();
|
||||||
|
void show(const QModelIndex &task, int panelLocation);
|
||||||
|
void setAnchor(const QRect &button);
|
||||||
|
void hide();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QQmlEngine m_engine;
|
||||||
|
std::unique_ptr<QQuickWindow> m_window;
|
||||||
|
int m_panelLocation = 0;
|
||||||
|
};
|
||||||
@@ -15,6 +15,18 @@ 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.cpp" "$source_dir/keyboardlayout.cpp"
|
||||||
install -m 0644 "$tool_dir/keyboardlayout.h" "$source_dir/keyboardlayout.h"
|
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/main.cpp" "$source_dir/main.cpp"
|
||||||
|
install -m 0644 "$tool_dir/pendingselection.cpp" "$source_dir/pendingselection.cpp"
|
||||||
|
install -m 0644 "$tool_dir/pendingselection.h" "$source_dir/pendingselection.h"
|
||||||
|
install -m 0644 "$tool_dir/windowpreview.cpp" "$source_dir/windowpreview.cpp"
|
||||||
|
install -m 0644 "$tool_dir/windowpreview.h" "$source_dir/windowpreview.h"
|
||||||
|
install -m 0644 "$tool_dir/Preview.qml" "$source_dir/Preview.qml"
|
||||||
|
install -m 0644 "$tool_dir/TaskbarGeometry.qml" "$source_dir/TaskbarGeometry.qml"
|
||||||
|
install -m 0644 "$tool_dir/taskbargeometry.cpp" "$source_dir/taskbargeometry.cpp"
|
||||||
|
install -m 0644 "$tool_dir/taskbargeometry.h" "$source_dir/taskbargeometry.h"
|
||||||
|
install -m 0644 "$tool_dir/previewplacement.cpp" "$source_dir/previewplacement.cpp"
|
||||||
|
install -m 0644 "$tool_dir/previewplacement.h" "$source_dir/previewplacement.h"
|
||||||
|
install -m 0644 "$tool_dir/tests/test-pendingselection.cpp" "$source_dir/tests/test-pendingselection.cpp"
|
||||||
|
install -m 0644 "$tool_dir/tests/test-preview.cpp" "$source_dir/tests/test-preview.cpp"
|
||||||
install -m 0644 "$tool_dir/taskselection.cpp" "$source_dir/taskselection.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/taskselection.h" "$source_dir/taskselection.h"
|
||||||
install -m 0644 "$tool_dir/tests/test-taskselection.cpp" \
|
install -m 0644 "$tool_dir/tests/test-taskselection.cpp" \
|
||||||
|
|||||||
Reference in New Issue
Block a user