From 29ce5117ffd7097007c800b988a502a39805fd66 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Fri, 10 Apr 2026 10:23:36 +0200 Subject: [PATCH] Show window thumbnail on dock icon hover Hovering a running-app icon in the dock shows a live PipeWire thumbnail of the window via zkde_screencast. Falls back to the app icon when the stream isn't available. Clicking the thumbnail activates the window. Uses a tooltip-type Window so the popup renders above app windows instead of being clipped to the panel surface. --- containments/homescreens/folio/CMakeLists.txt | 1 + .../homescreens/folio/qml/FavouritesBar.qml | 161 +++++++++++++++++- .../folio/qml/PipeWireThumbnail.qml | 21 +++ 3 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 containments/homescreens/folio/qml/PipeWireThumbnail.qml diff --git a/containments/homescreens/folio/CMakeLists.txt b/containments/homescreens/folio/CMakeLists.txt index 006e800a..4d62e0d3 100644 --- a/containments/homescreens/folio/CMakeLists.txt +++ b/containments/homescreens/folio/CMakeLists.txt @@ -17,6 +17,7 @@ plasma_add_applet(org.kde.plasma.mobile.homescreen.folio qml/HomeScreenPage.qml qml/HomeScreenPages.qml qml/main.qml + qml/PipeWireThumbnail.qml qml/PlaceholderDelegate.qml qml/WidgetDragItem.qml qml/config.qml diff --git a/containments/homescreens/folio/qml/FavouritesBar.qml b/containments/homescreens/folio/qml/FavouritesBar.qml index 5ee175aa..1a0f7f4d 100644 --- a/containments/homescreens/folio/qml/FavouritesBar.qml +++ b/containments/homescreens/folio/qml/FavouritesBar.qml @@ -42,6 +42,9 @@ MouseArea { ? navButtonWidth + (root.width - 2 * navButtonWidth) / 2 : root.width / 2 + // Thumbnail popup hover tracking + property int hoveredTaskIndex: -1 + // Home button (convergence mode, left end) Rectangle { id: homeButton @@ -440,6 +443,136 @@ MouseArea { } // Running-app task icons (convergence mode only) + + Timer { + id: thumbnailShowTimer + interval: Kirigami.Units.toolTipDelay + onTriggered: { + thumbnailPopup.visible = true + } + } + + Timer { + id: thumbnailHideTimer + interval: 300 + onTriggered: { + thumbnailPopup.visible = false + root.hoveredTaskIndex = -1 + } + } + + Window { + id: thumbnailPopup + + property var targetDelegate: null + property string windowTitle: "" + property string windowUuid: "" + property bool popupHovered: false + + function open() { visible = true } + function close() { visible = false } + readonly property bool opened: visible + + flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowDoesNotAcceptFocus + color: "transparent" + width: Kirigami.Units.gridUnit * 16 + height: popupContent.implicitHeight + 2 * Kirigami.Units.smallSpacing + + // Position above the hovered dock icon, in global coordinates + x: { + if (!targetDelegate) return 0 + var delegateGlobal = targetDelegate.mapToGlobal(0, 0) + return Math.max(0, delegateGlobal.x + (targetDelegate.width - width) / 2) + } + y: { + if (!targetDelegate) return 0 + var delegateGlobal = targetDelegate.mapToGlobal(0, 0) + return delegateGlobal.y - height - Kirigami.Units.smallSpacing + } + + onVisibleChanged: { + if (!visible) { + windowUuid = "" + targetDelegate = null + } + } + + Rectangle { + anchors.fill: parent + color: Kirigami.Theme.backgroundColor + border.color: Qt.rgba( + Kirigami.Theme.textColor.r, + Kirigami.Theme.textColor.g, + Kirigami.Theme.textColor.b, 0.2) + border.width: 1 + radius: Kirigami.Units.cornerRadius + + MouseArea { + id: popupHoverArea + anchors.fill: parent + hoverEnabled: true + + onContainsMouseChanged: { + thumbnailPopup.popupHovered = containsMouse + if (containsMouse) { + thumbnailHideTimer.stop() + } else if (root.hoveredTaskIndex < 0) { + thumbnailHideTimer.restart() + } + } + + onClicked: { + if (thumbnailPopup.targetDelegate) { + tasksModel.requestActivate( + tasksModel.makeModelIndex(thumbnailPopup.targetDelegate.index)) + thumbnailPopup.close() + } + } + + Column { + id: popupContent + anchors.fill: parent + anchors.margins: Kirigami.Units.smallSpacing + spacing: Kirigami.Units.smallSpacing + + Item { + width: parent.width + height: width * 9 / 16 + + Loader { + id: pipeWireLoader + active: thumbnailPopup.visible + && thumbnailPopup.windowUuid !== "" + anchors.fill: parent + sourceComponent: PipeWireThumbnail { + windowUuid: thumbnailPopup.windowUuid + } + } + + Kirigami.Icon { + anchors.centerIn: parent + width: Kirigami.Units.iconSizes.huge + height: width + source: thumbnailPopup.targetDelegate + ? thumbnailPopup.targetDelegate.model.decoration + : "" + visible: !pipeWireLoader.item + || !pipeWireLoader.item.hasThumbnail + } + } + + PC3.Label { + width: parent.width + text: thumbnailPopup.windowTitle + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + maximumLineCount: 1 + } + } + } + } + } + Repeater { id: taskRepeater model: root.convergenceMode ? tasksModel : null @@ -498,7 +631,7 @@ MouseArea { visible: taskDelegate.model.IsActive === true } - // Click to activate + // Click to activate, hover for thumbnail preview MouseArea { id: taskMouseArea anchors.fill: parent @@ -506,17 +639,35 @@ MouseArea { acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: (mouse) => { if (mouse.button === Qt.RightButton) { + thumbnailPopup.close() + thumbnailShowTimer.stop() taskContextMenu.popup(); } else { + thumbnailPopup.close() tasksModel.requestActivate(tasksModel.makeModelIndex(taskDelegate.index)); } } + onContainsMouseChanged: { + if (containsMouse) { + thumbnailHideTimer.stop() + thumbnailPopup.targetDelegate = taskDelegate + thumbnailPopup.windowTitle = taskDelegate.model.display || "" + var winIds = taskDelegate.model.WinIdList + thumbnailPopup.windowUuid = (winIds && winIds.length > 0) ? winIds[0] : "" + root.hoveredTaskIndex = taskDelegate.index + if (!thumbnailPopup.opened) { + thumbnailShowTimer.restart() + } + } else { + root.hoveredTaskIndex = -1 + if (!thumbnailPopup.popupHovered) { + thumbnailShowTimer.stop() + thumbnailHideTimer.restart() + } + } + } } - Controls.ToolTip.text: taskDelegate.model.display || "" - Controls.ToolTip.visible: taskMouseArea.containsMouse && (taskDelegate.model.display || "") !== "" - Controls.ToolTip.delay: Kirigami.Units.toolTipDelay - Controls.Menu { id: taskContextMenu Controls.MenuItem { diff --git a/containments/homescreens/folio/qml/PipeWireThumbnail.qml b/containments/homescreens/folio/qml/PipeWireThumbnail.qml new file mode 100644 index 00000000..b7b2aba6 --- /dev/null +++ b/containments/homescreens/folio/qml/PipeWireThumbnail.qml @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import org.kde.pipewire as PipeWire +import org.kde.taskmanager as TaskManager + +PipeWire.PipeWireSourceItem { + id: pipeWireSourceItem + + property string windowUuid: "" + readonly property alias hasThumbnail: pipeWireSourceItem.ready + + anchors.fill: parent + nodeId: waylandItem.nodeId + + TaskManager.ScreencastingRequest { + id: waylandItem + uuid: pipeWireSourceItem.windowUuid + } +}