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
|
||||
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
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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(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(LibTaskManager REQUIRED)
|
||||
find_package(Plasma REQUIRED)
|
||||
find_package(PlasmaWaylandProtocols 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
|
||||
keyboardlayout.cpp
|
||||
main.cpp
|
||||
pendingselection.cpp
|
||||
previewplacement.cpp
|
||||
taskbargeometry.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
|
||||
PRIVATE_CODE
|
||||
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
|
||||
Qt6::Gui
|
||||
Qt6::DBus
|
||||
Qt6::Quick
|
||||
Qt6::WaylandClient
|
||||
KF6::ConfigCore
|
||||
KF6::GlobalAccel
|
||||
KF6::Service
|
||||
PW::LibTaskManager
|
||||
Plasma::Plasma
|
||||
XKB::XKB
|
||||
)
|
||||
|
||||
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
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
||||
install(FILES se.ajpanton.plasma-task-group-shortcuts-autostart.desktop
|
||||
@@ -71,4 +88,14 @@ if(BUILD_TESTING)
|
||||
XKB::XKB
|
||||
)
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
|
||||
#include "keyboardlayout.h"
|
||||
#include "pendingselection.h"
|
||||
#include "taskbargeometry.h"
|
||||
#include "taskselection.h"
|
||||
#include "windowpreview.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QCommandLineOption>
|
||||
@@ -29,6 +32,7 @@
|
||||
#include <abstracttasksmodel.h>
|
||||
#include <activityinfo.h>
|
||||
#include <tasksmodel.h>
|
||||
#include <plasma/plasma.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -109,7 +113,7 @@ void rememberDisplacedShortcuts(int number, const QList<QKeySequence> &sequences
|
||||
|
||||
void restoreDisplacedShortcuts()
|
||||
{
|
||||
for (int number = 1; number <= 9; ++number) {
|
||||
for (int number = 0; number <= 9; ++number) {
|
||||
KConfigGroup group = displacedShortcutGroup(number);
|
||||
if (!group.readEntry("Saved", false)) {
|
||||
continue;
|
||||
@@ -137,10 +141,15 @@ void restoreDisplacedShortcuts()
|
||||
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"));
|
||||
KConfigGroup fallback;
|
||||
PanelConfig fallback;
|
||||
for (const QString &containmentId : containments.groupList()) {
|
||||
const KConfigGroup containment = containments.group(containmentId);
|
||||
const KConfigGroup applets = containment.group(QStringLiteral("Applets"));
|
||||
@@ -148,11 +157,12 @@ KConfigGroup taskManagerConfig(const KSharedConfig::Ptr &config)
|
||||
const KConfigGroup applet = applets.group(appletId);
|
||||
if (applet.readEntry("plugin") == QLatin1String("org.kde.plasma.taskmanager")) {
|
||||
const KConfigGroup general = applet.group(QStringLiteral("Configuration")).group(QStringLiteral("General"));
|
||||
const PanelConfig panel{general, containment.readEntry("location", int(Plasma::Types::BottomEdge))};
|
||||
if (containment.readEntry("lastScreen", -1) == 0) {
|
||||
return general;
|
||||
return panel;
|
||||
}
|
||||
if (!fallback.isValid()) {
|
||||
fallback = general;
|
||||
if (!fallback.general.isValid()) {
|
||||
fallback = panel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,6 +193,11 @@ public:
|
||||
return m_shiftPressed;
|
||||
}
|
||||
|
||||
bool metaPressed() const
|
||||
{
|
||||
return isActive() && m_metaPressed;
|
||||
}
|
||||
|
||||
Q_SIGNALS:
|
||||
void released();
|
||||
|
||||
@@ -218,20 +233,50 @@ public:
|
||||
reloadConfig();
|
||||
|
||||
connect(&m_activityInfo, &TaskManager::ActivityInfo::currentActivityChanged, this, [this] {
|
||||
m_lastShortcut = -1;
|
||||
m_selection.cancel();
|
||||
m_model.setActivity(m_activityInfo.currentActivity());
|
||||
});
|
||||
connect(&m_model, &QAbstractItemModel::modelReset, this, [this] {
|
||||
m_lastShortcut = -1;
|
||||
});
|
||||
connect(qGuiApp, &QGuiApplication::primaryScreenChanged, this, &ShortcutManager::updateScreen);
|
||||
connect(&m_modifierState, &ModifierState::released, this, [this] {
|
||||
m_lastShortcut = -1;
|
||||
m_selection.finish();
|
||||
});
|
||||
connect(&m_modifierState, &QWaylandClientExtension::activeChanged, this, [this] {
|
||||
if (!m_modifierState.isActive()) {
|
||||
m_lastShortcut = -1;
|
||||
m_selection.cancel();
|
||||
}
|
||||
});
|
||||
connect(&m_selection, &PendingSelection::activationRequested,
|
||||
&m_model, &TaskManager::TasksModel::requestActivate);
|
||||
connect(&m_selection, &PendingSelection::previewRequested, this, [this](const QModelIndex &task) {
|
||||
m_preview.show(task, m_panelLocation);
|
||||
m_taskbarGeometry.request(task.data(TaskManager::AbstractTasksModel::WinIdList).toList().value(0).toString());
|
||||
});
|
||||
connect(&m_selection, &PendingSelection::previewHidden, this, [this] {
|
||||
m_taskbarGeometry.cancel();
|
||||
m_preview.hide();
|
||||
});
|
||||
connect(&m_taskbarGeometry, &TaskbarGeometry::received, this, [this](const QRectF &geometry) {
|
||||
m_preview.setAnchor(geometry.toAlignedRect());
|
||||
});
|
||||
updateScreen();
|
||||
|
||||
for (int number = 1; number <= 9; ++number) {
|
||||
for (int number = 0; 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));
|
||||
action->setObjectName(number == 0 ? QStringLiteral("cancel task group selection")
|
||||
: QStringLiteral("activate task group %1").arg(number));
|
||||
action->setText(number == 0 ? QStringLiteral("Cancel Task Group Selection")
|
||||
: QStringLiteral("Activate Task Group %1").arg(number));
|
||||
|
||||
const QList<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);
|
||||
for (const QKeySequence &sequence : sequences) {
|
||||
KGlobalAccel::stealShortcutSystemwide(sequence);
|
||||
@@ -240,7 +285,12 @@ public:
|
||||
KGlobalAccel::self()->setShortcut(action, sequences, KGlobalAccel::NoAutoloading);
|
||||
|
||||
connect(action, &QAction::triggered, this, [this, number] {
|
||||
if (number == 0) {
|
||||
m_lastShortcut = -1;
|
||||
m_selection.cancel();
|
||||
} else {
|
||||
useShortcut(number - 1, m_modifierState.shiftPressed());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -259,11 +309,13 @@ private:
|
||||
void reloadConfig()
|
||||
{
|
||||
m_config->reparseConfiguration();
|
||||
const KConfigGroup config = taskManagerConfig(m_config);
|
||||
const PanelConfig panel = taskManagerConfig(m_config);
|
||||
const KConfigGroup config = panel.general;
|
||||
if (!config.isValid()) {
|
||||
qWarning("No Plasma Task Manager configuration found");
|
||||
return;
|
||||
}
|
||||
m_panelLocation = panel.location;
|
||||
|
||||
m_model.setLauncherList(config.readEntry("launchers", QStringList()));
|
||||
m_model.setFilterByCurrentVirtualDesktop(config.readEntry("showOnlyCurrentDesktop", true));
|
||||
@@ -286,6 +338,8 @@ private:
|
||||
|
||||
void updateScreen()
|
||||
{
|
||||
m_lastShortcut = -1;
|
||||
m_selection.cancel();
|
||||
if (const QScreen *screen = QGuiApplication::primaryScreen()) {
|
||||
m_model.setScreenGeometry(screen->geometry());
|
||||
}
|
||||
@@ -296,6 +350,8 @@ private:
|
||||
reloadConfig();
|
||||
const QVector<int> tasks = logicalTaskRows(m_model, m_separateLaunchers);
|
||||
if (row >= tasks.size()) {
|
||||
m_selection.cancel();
|
||||
m_lastShortcut = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -310,23 +366,29 @@ private:
|
||||
} else {
|
||||
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) {
|
||||
m_selection.cancel();
|
||||
m_model.requestNewInstance(m_model.index(tasks.at(row), 0));
|
||||
return;
|
||||
}
|
||||
|
||||
const QModelIndex task = taskToActivate(m_model, tasks.at(row), m_cyclePosition);
|
||||
if (task.isValid()) {
|
||||
m_model.requestActivate(task);
|
||||
}
|
||||
// A quick key release can reach us before the shortcut's D-Bus signal.
|
||||
// In that case there is no held cycle left to preview.
|
||||
m_selection.select(task, task.data(TaskManager::AbstractTasksModel::IsWindow).toBool() && held);
|
||||
}
|
||||
|
||||
KSharedConfig::Ptr m_config;
|
||||
TaskManager::ActivityInfo m_activityInfo;
|
||||
TaskManager::TasksModel m_model;
|
||||
PendingSelection m_selection{m_model};
|
||||
WindowPreview m_preview;
|
||||
TaskbarGeometry m_taskbarGeometry;
|
||||
ModifierState m_modifierState;
|
||||
int m_panelLocation = Plasma::Types::BottomEdge;
|
||||
bool m_separateLaunchers = true;
|
||||
int m_lastShortcut = -1;
|
||||
int m_cyclePosition = 0;
|
||||
@@ -342,6 +404,7 @@ int main(int argc, char **argv)
|
||||
}
|
||||
|
||||
QGuiApplication application(argc, argv);
|
||||
application.setQuitOnLastWindowClosed(false);
|
||||
QCoreApplication::setQuitLockEnabled(false);
|
||||
QCoreApplication::setApplicationName(QString::fromLatin1(componentId));
|
||||
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
|
||||
Version: 0.1.0
|
||||
Release: 15%{?dist}
|
||||
Release: 20%{?dist}
|
||||
Summary: Windows-like application-group shortcuts for the Plasma Task Manager
|
||||
|
||||
License: MIT
|
||||
@@ -17,11 +17,18 @@ BuildRequires: kf6-kglobalaccel-devel
|
||||
BuildRequires: kf6-kitemmodels-devel
|
||||
BuildRequires: kf6-kservice-devel
|
||||
BuildRequires: libxkbcommon-devel
|
||||
BuildRequires: libplasma-devel
|
||||
BuildRequires: plasma-wayland-protocols-devel
|
||||
BuildRequires: plasma-workspace-devel >= 6.7
|
||||
BuildRequires: qt6-qtbase-devel
|
||||
BuildRequires: qt6-qtdeclarative-devel
|
||||
BuildRequires: qt6-qtwayland-devel
|
||||
Requires: plasma-workspace >= 6.7
|
||||
Requires: kpipewire
|
||||
Requires: kf6-kirigami
|
||||
Requires: libplasma
|
||||
BuildRequires: kpipewire
|
||||
BuildRequires: kf6-kirigami
|
||||
Provides: fedora-tools-tool
|
||||
|
||||
%description
|
||||
@@ -50,15 +57,32 @@ install -Dpm 0644 %{SOURCE2} \
|
||||
%license %{_licensedir}/%{name}/LICENSE
|
||||
%doc %{_docdir}/%{name}/README.md
|
||||
%{_bindir}/plasma-task-group-shortcuts
|
||||
%{_datadir}/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
|
||||
* 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
|
||||
- Clarify the package summary
|
||||
|
||||
* 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
|
||||
- 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
|
||||
Icon=preferences-system-windows
|
||||
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.h" "$source_dir/keyboardlayout.h"
|
||||
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.h" "$source_dir/taskselection.h"
|
||||
install -m 0644 "$tool_dir/tests/test-taskselection.cpp" \
|
||||
|
||||
Reference in New Issue
Block a user