From 3cd8865a50e084d35e94388dce5b55dd5a190674 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Fri, 30 May 2025 11:27:57 +0200 Subject: [PATCH] dock: previews --- .../modules/common/widgets/GroupButton.qml | 9 +- .../modules/common/widgets/RippleButton.qml | 7 +- .config/quickshell/modules/dock/Dock.qml | 7 +- .../quickshell/modules/dock/DockAppButton.qml | 44 ++++ .config/quickshell/modules/dock/DockApps.qml | 240 +++++++++++++++--- .../quickshell/modules/dock/DockButton.qml | 2 +- 6 files changed, 267 insertions(+), 42 deletions(-) create mode 100644 .config/quickshell/modules/dock/DockAppButton.qml diff --git a/.config/quickshell/modules/common/widgets/GroupButton.qml b/.config/quickshell/modules/common/widgets/GroupButton.qml index a369ec16e..1f0776194 100644 --- a/.config/quickshell/modules/common/widgets/GroupButton.qml +++ b/.config/quickshell/modules/common/widgets/GroupButton.qml @@ -19,6 +19,7 @@ Button { property real buttonRadius: Appearance?.rounding?.small ?? 4 property real buttonRadiusPressed: buttonRadius property var altAction + property var middleClickAction property bool bounce: true property real baseWidth: contentItem.implicitWidth + padding * 2 property real baseHeight: contentItem.implicitHeight + padding * 2 @@ -67,9 +68,13 @@ Button { MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton onPressed: (event) => { - if(event.button === Qt.RightButton) { + if (event.button === Qt.MiddleButton) { + if (root.middleClickAction) root.middleClickAction(); + return; + } + if (event.button === Qt.RightButton) { if (root.altAction) root.altAction(); return; } diff --git a/.config/quickshell/modules/common/widgets/RippleButton.qml b/.config/quickshell/modules/common/widgets/RippleButton.qml index e80a319c9..3e4868405 100644 --- a/.config/quickshell/modules/common/widgets/RippleButton.qml +++ b/.config/quickshell/modules/common/widgets/RippleButton.qml @@ -21,6 +21,7 @@ Button { property int rippleDuration: 1200 property bool rippleEnabled: true property var altAction + property var middleClickAction property color colBackground: ColorUtils.transparentize(Appearance?.colors.colLayer1Hover, 1) || "transparent" property color colBackgroundHover: Appearance?.colors.colLayer1Hover ?? "#E5DFED" @@ -58,12 +59,16 @@ Button { MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton onPressed: (event) => { if(event.button === Qt.RightButton) { if (root.altAction) root.altAction(); return; } + if(event.button === Qt.MiddleButton) { + if (root.middleClickAction) root.middleClickAction(); + return; + } root.down = true if (!root.rippleEnabled) return; const {x,y} = event diff --git a/.config/quickshell/modules/dock/Dock.qml b/.config/quickshell/modules/dock/Dock.qml index 66b5172bf..777aa6461 100644 --- a/.config/quickshell/modules/dock/Dock.qml +++ b/.config/quickshell/modules/dock/Dock.qml @@ -23,7 +23,7 @@ Scope { // Scope id: dockRoot screen: modelData - property bool reveal: root.pinned || dockMouseArea.containsMouse + property bool reveal: root.pinned || dockMouseArea.containsMouse || dockApps.requestDockShow anchors { bottom: true @@ -35,9 +35,6 @@ Scope { // Scope cheatsheetLoader.active = false } exclusiveZone: root.pinned ? implicitHeight - Appearance.sizes.hyprlandGapsOut : 0 - Component.onCompleted: { - console.log(ConfigOptions.dock.hoverRegionHeight) - } implicitWidth: dockBackground.implicitWidth WlrLayershell.namespace: "quickshell:dock" @@ -114,7 +111,7 @@ Scope { // Scope } } DockSeparator {} - DockApps {} + DockApps { id: dockApps } DockSeparator {} DockButton { onClicked: Hyprland.dispatch("global quickshell:overviewToggle") diff --git a/.config/quickshell/modules/dock/DockAppButton.qml b/.config/quickshell/modules/dock/DockAppButton.qml new file mode 100644 index 000000000..7ff11bae1 --- /dev/null +++ b/.config/quickshell/modules/dock/DockAppButton.qml @@ -0,0 +1,44 @@ +import "root:/" +import "root:/services" +import "root:/modules/common" +import "root:/modules/common/widgets" +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell.Io +import Quickshell +import Quickshell.Widgets +import Quickshell.Wayland +import Quickshell.Hyprland + +DockButton { + id: appButton + required property var appToplevel + property var appListRoot + property int lastFocused: -1 + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + onEntered: { + appListRoot.lastHoveredButton = appButton + appListRoot.buttonHovered = true + lastFocused = appToplevel.toplevels.length - 1 + } + onExited: { + if (appListRoot.lastHoveredButton === appButton) { + appListRoot.buttonHovered = false + } + } + } + onClicked: { + lastFocused = (lastFocused + 1) % appToplevel.toplevels.length + appToplevel.toplevels[lastFocused].activate() + } + contentItem: IconImage { + id: iconImage + source: Quickshell.iconPath(AppSearch.guessIcon(appToplevel.appId), "image-missing") + } +} diff --git a/.config/quickshell/modules/dock/DockApps.qml b/.config/quickshell/modules/dock/DockApps.qml index 1383f41f0..7704a8cf2 100644 --- a/.config/quickshell/modules/dock/DockApps.qml +++ b/.config/quickshell/modules/dock/DockApps.qml @@ -2,7 +2,7 @@ import "root:/" import "root:/services" import "root:/modules/common" import "root:/modules/common/widgets" -import "root:/modules/common/functions/icons.js" as Icons +import Qt5Compat.GraphicalEffects import QtQuick import QtQuick.Controls import QtQuick.Effects @@ -13,44 +13,218 @@ import Quickshell.Widgets import Quickshell.Wayland import Quickshell.Hyprland -RowLayout { - readonly property list windowList: HyprlandData.windowList - readonly property list apps: { - let uniqueClasses = new Set() - for (let window of windowList) { - if (window.class && window.class.trim() !== "") { - uniqueClasses.add(window.class) - } - } - return Array.from(uniqueClasses) - } - readonly property var windowsByApp: { - let grouped = {} - for (let window of windowList) { - if (window.class && window.class.trim() !== "") { - if (!grouped[window.class]) { - grouped[window.class] = [] +Item { + id: root + property real maxWindowPreviewHeight: 200 + property real maxWindowPreviewWidth: 350 + property Item lastHoveredButton + property bool buttonHovered: false + property bool requestDockShow: previewPopup.show + + implicitWidth: rowLayout.implicitWidth + implicitHeight: rowLayout.implicitHeight + + RowLayout { + id: rowLayout + spacing: 2 + + Repeater { + model: ScriptModel { + objectProp: "appId" + values: { + var map = new Map(); + + for (const toplevel of ToplevelManager.toplevels.values) { + if (!map.has(toplevel.appId.toLowerCase())) map.set(toplevel.appId.toLowerCase(), []); + map.get(toplevel.appId.toLowerCase()).push(toplevel); + } + + var values = []; + + for (const [key, value] of map) { + values.push({ appId: key, toplevels: value }); + } + + return values; } - grouped[window.class].push(window) + } + delegate: DockAppButton { + required property var modelData + appToplevel: modelData + appListRoot: root } } - return grouped } - Repeater { - model: apps - delegate: DockButton { - required property string modelData - property int lastFocusedIndex: -1 - contentItem: IconImage { - source: Quickshell.iconPath(Icons.noKnowledgeIconGuess(modelData), "image-missing") + PopupWindow { + id: previewPopup + property var appTopLevel: root.lastHoveredButton?.appToplevel + property bool allPreviewsReady: false + Connections { + target: root + onLastHoveredButtonChanged: previewPopup.allPreviewsReady = false; // Reset readiness when the hovered button changes + } + function updatePreviewReadiness() { + for(var i = 0; i < previewRowLayout.children.length; i++) { + const view = previewRowLayout.children[i]; + if (view.hasContent === false) { + allPreviewsReady = false; + return; + } } - onClicked: () => { - lastFocusedIndex = (lastFocusedIndex + 1) % windowsByApp[modelData].length - const targetWindow = windowsByApp[modelData][lastFocusedIndex]; - const targetAddress = targetWindow.address; - Hyprland.dispatch(`focuswindow address:${targetAddress}`); + allPreviewsReady = true; + } + property bool shouldShow: { + const hoverConditions = (popupMouseArea.containsMouse || root.buttonHovered) + return hoverConditions && allPreviewsReady; + } + property bool show: false + + onShouldShowChanged: { + if (shouldShow) { + // show = true; + updateTimer.restart(); + } else { + updateTimer.restart(); + } + } + Timer { + id: updateTimer + interval: 100 + onTriggered: { + previewPopup.show = previewPopup.shouldShow + } + } + anchor { + window: root.QsWindow.window + rect: { + if (root.lastHoveredButton === null) return; // Don't update + const parentWindow = root.QsWindow.window + const mappedPosition = parentWindow.mapFromItem(root.lastHoveredButton, root.lastHoveredButton.width / 2, root.lastHoveredButton.height / 2) + const modifiedX = mappedPosition.x - implicitWidth / 2 + const modifiedY = 0 + return Qt.rect(modifiedX, modifiedY, implicitWidth, implicitHeight) + } + gravity: Edges.Top + edges: Edges.Top + } + visible: popupBackground.visible + color: "transparent" + implicitWidth: root.QsWindow.window.width + implicitHeight: popupBackground.implicitHeight + Appearance.sizes.elevationMargin * 2 + + MouseArea { + id: popupMouseArea + anchors.bottom: parent.bottom + implicitWidth: popupBackground.implicitWidth + Appearance.sizes.elevationMargin * 2 + implicitHeight: popupBackground.implicitHeight + Appearance.sizes.elevationMargin * 2 + anchors.horizontalCenter: parent.horizontalCenter + hoverEnabled: true + StyledRectangularShadow { + target: popupBackground + opacity: previewPopup.show ? 1 : 0 + visible: opacity > 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + } + Rectangle { + id: popupBackground + property real padding: 5 + opacity: previewPopup.show ? 1 : 0 + visible: opacity > 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + color: Appearance.colors.colLayer1 + radius: Appearance.rounding.normal + anchors.bottom: parent.bottom + anchors.bottomMargin: Appearance.sizes.elevationMargin + anchors.horizontalCenter: parent.horizontalCenter + implicitWidth: previewRowLayout.implicitWidth + padding * 2 + implicitHeight: root.maxWindowPreviewHeight + padding * 2 + + RowLayout { + id: previewRowLayout + anchors.centerIn: parent + Repeater { + model: previewPopup.appTopLevel?.toplevels ?? [] + RippleButton { + id: windowButton + required property var modelData + padding: 0 + middleClickAction: () => { + windowButton.modelData?.close(); + } + onClicked: { + windowButton.modelData?.activate(); + } + contentItem: Item { + implicitWidth: screencopyView.implicitWidth + implicitHeight: screencopyView.implicitHeight + ScreencopyView { + id: screencopyView + anchors.centerIn: parent + captureSource: previewPopup ? windowButton.modelData : null + live: true + paintCursor: true + constraintSize: Qt.size(root.maxWindowPreviewWidth, root.maxWindowPreviewHeight) + onHasContentChanged: { + previewPopup.updatePreviewReadiness(); + } + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: screencopyView.width + height: screencopyView.height + radius: Appearance.rounding.small + } + } + } + ButtonGroup { + contentWidth: parent.width - anchors.margins * 2 + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: 3 + } + WrapperRectangle { + Layout.fillWidth: true + color: Appearance.m3colors.m3surfaceContainer + radius: Appearance.rounding.small + margin: 5 + StyledText { + Layout.fillWidth: true + font.pixelSize: Appearance.font.pixelSize.small + text: windowButton.modelData?.title + elide: Text.ElideRight + color: Appearance.m3colors.m3onSurface + } + } + GroupButton { + id: closeButton + colBackground: Appearance.m3colors.m3surfaceContainer + baseWidth: 30 + baseHeight: 30 + buttonRadius: Appearance.rounding.full + contentItem: MaterialSymbol { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + text: "close" + iconSize: Appearance.font.pixelSize.normal + color: Appearance.m3colors.m3onSurface + } + onClicked: { + windowButton.modelData?.close(); + } + } + } + } + } + } + } } } } -} \ No newline at end of file +} diff --git a/.config/quickshell/modules/dock/DockButton.qml b/.config/quickshell/modules/dock/DockButton.qml index f80661582..6c3010bf6 100644 --- a/.config/quickshell/modules/dock/DockButton.qml +++ b/.config/quickshell/modules/dock/DockButton.qml @@ -7,7 +7,7 @@ import QtQuick.Layouts RippleButton { Layout.fillHeight: true - implicitWidth: background.height + implicitWidth: implicitHeight - topInset - bottomInset buttonRadius: Appearance.rounding.normal topInset: dockVisualBackground.margin + dockRow.padding