dock: previews

This commit is contained in:
end-4
2025-05-30 11:27:57 +02:00
parent 198bcc6a3a
commit 3cd8865a50
6 changed files with 267 additions and 42 deletions
@@ -19,6 +19,7 @@ Button {
property real buttonRadius: Appearance?.rounding?.small ?? 4 property real buttonRadius: Appearance?.rounding?.small ?? 4
property real buttonRadiusPressed: buttonRadius property real buttonRadiusPressed: buttonRadius
property var altAction property var altAction
property var middleClickAction
property bool bounce: true property bool bounce: true
property real baseWidth: contentItem.implicitWidth + padding * 2 property real baseWidth: contentItem.implicitWidth + padding * 2
property real baseHeight: contentItem.implicitHeight + padding * 2 property real baseHeight: contentItem.implicitHeight + padding * 2
@@ -67,9 +68,13 @@ Button {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: (event) => { 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(); if (root.altAction) root.altAction();
return; return;
} }
@@ -21,6 +21,7 @@ Button {
property int rippleDuration: 1200 property int rippleDuration: 1200
property bool rippleEnabled: true property bool rippleEnabled: true
property var altAction property var altAction
property var middleClickAction
property color colBackground: ColorUtils.transparentize(Appearance?.colors.colLayer1Hover, 1) || "transparent" property color colBackground: ColorUtils.transparentize(Appearance?.colors.colLayer1Hover, 1) || "transparent"
property color colBackgroundHover: Appearance?.colors.colLayer1Hover ?? "#E5DFED" property color colBackgroundHover: Appearance?.colors.colLayer1Hover ?? "#E5DFED"
@@ -58,12 +59,16 @@ Button {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: (event) => { onPressed: (event) => {
if(event.button === Qt.RightButton) { if(event.button === Qt.RightButton) {
if (root.altAction) root.altAction(); if (root.altAction) root.altAction();
return; return;
} }
if(event.button === Qt.MiddleButton) {
if (root.middleClickAction) root.middleClickAction();
return;
}
root.down = true root.down = true
if (!root.rippleEnabled) return; if (!root.rippleEnabled) return;
const {x,y} = event const {x,y} = event
+2 -5
View File
@@ -23,7 +23,7 @@ Scope { // Scope
id: dockRoot id: dockRoot
screen: modelData screen: modelData
property bool reveal: root.pinned || dockMouseArea.containsMouse property bool reveal: root.pinned || dockMouseArea.containsMouse || dockApps.requestDockShow
anchors { anchors {
bottom: true bottom: true
@@ -35,9 +35,6 @@ Scope { // Scope
cheatsheetLoader.active = false cheatsheetLoader.active = false
} }
exclusiveZone: root.pinned ? implicitHeight - Appearance.sizes.hyprlandGapsOut : 0 exclusiveZone: root.pinned ? implicitHeight - Appearance.sizes.hyprlandGapsOut : 0
Component.onCompleted: {
console.log(ConfigOptions.dock.hoverRegionHeight)
}
implicitWidth: dockBackground.implicitWidth implicitWidth: dockBackground.implicitWidth
WlrLayershell.namespace: "quickshell:dock" WlrLayershell.namespace: "quickshell:dock"
@@ -114,7 +111,7 @@ Scope { // Scope
} }
} }
DockSeparator {} DockSeparator {}
DockApps {} DockApps { id: dockApps }
DockSeparator {} DockSeparator {}
DockButton { DockButton {
onClicked: Hyprland.dispatch("global quickshell:overviewToggle") onClicked: Hyprland.dispatch("global quickshell:overviewToggle")
@@ -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")
}
}
+206 -32
View File
@@ -2,7 +2,7 @@ import "root:/"
import "root:/services" import "root:/services"
import "root:/modules/common" import "root:/modules/common"
import "root:/modules/common/widgets" import "root:/modules/common/widgets"
import "root:/modules/common/functions/icons.js" as Icons import Qt5Compat.GraphicalEffects
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Effects import QtQuick.Effects
@@ -13,43 +13,217 @@ import Quickshell.Widgets
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
RowLayout { Item {
readonly property list<var> windowList: HyprlandData.windowList id: root
readonly property list<string> apps: { property real maxWindowPreviewHeight: 200
let uniqueClasses = new Set() property real maxWindowPreviewWidth: 350
for (let window of windowList) { property Item lastHoveredButton
if (window.class && window.class.trim() !== "") { property bool buttonHovered: false
uniqueClasses.add(window.class) property bool requestDockShow: previewPopup.show
}
} implicitWidth: rowLayout.implicitWidth
return Array.from(uniqueClasses) implicitHeight: rowLayout.implicitHeight
}
readonly property var windowsByApp: { RowLayout {
let grouped = {} id: rowLayout
for (let window of windowList) { spacing: 2
if (window.class && window.class.trim() !== "") {
if (!grouped[window.class]) { Repeater {
grouped[window.class] = [] 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 { PopupWindow {
model: apps id: previewPopup
delegate: DockButton { property var appTopLevel: root.lastHoveredButton?.appToplevel
required property string modelData property bool allPreviewsReady: false
property int lastFocusedIndex: -1 Connections {
contentItem: IconImage { target: root
source: Quickshell.iconPath(Icons.noKnowledgeIconGuess(modelData), "image-missing") 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: () => { allPreviewsReady = true;
lastFocusedIndex = (lastFocusedIndex + 1) % windowsByApp[modelData].length }
const targetWindow = windowsByApp[modelData][lastFocusedIndex]; property bool shouldShow: {
const targetAddress = targetWindow.address; const hoverConditions = (popupMouseArea.containsMouse || root.buttonHovered)
Hyprland.dispatch(`focuswindow address:${targetAddress}`); 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();
}
}
}
}
}
}
}
} }
} }
} }
@@ -7,7 +7,7 @@ import QtQuick.Layouts
RippleButton { RippleButton {
Layout.fillHeight: true Layout.fillHeight: true
implicitWidth: background.height implicitWidth: implicitHeight - topInset - bottomInset
buttonRadius: Appearance.rounding.normal buttonRadius: Appearance.rounding.normal
topInset: dockVisualBackground.margin + dockRow.padding topInset: dockVisualBackground.margin + dockRow.padding