forked from Shinonome/dots-hyprland
dock: previews
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,44 +13,218 @@ 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
|
||||||
|
|||||||
Reference in New Issue
Block a user