diff options
| author | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
|---|---|---|
| committer | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
| commit | 70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 (patch) | |
| tree | ab1123d4067c1b086dd6faa7ee4ea643236b565a /raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock | |
| parent | 5d94c0a7d44a2255b81815a52a7056a94a39842d (diff) | |
| download | RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.tar.gz RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.zip | |
Replaced file structures for themes in the correct format
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock')
6 files changed, 2943 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock/Dock.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock/Dock.qml new file mode 100644 index 0000000..136f23a --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock/Dock.qml @@ -0,0 +1,671 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Shapes +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import qs.Common +import qs.Services +import qs.Widgets + +Variants { + id: dockVariants + model: SettingsData.getFilteredScreens("dock") + + property var contextMenu + + delegate: PanelWindow { + id: dock + + WindowBlur { + targetWindow: dock + blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x + blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y + blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0 + blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0 + blurRadius: Theme.cornerRadius + } + + WlrLayershell.namespace: "dms:dock" + + readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right + + anchors { + top: !isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top) : true + bottom: !isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom) : true + left: !isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Left) + right: !isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Right) + } + + property var modelData: item + property bool autoHide: SettingsData.dockAutoHide || SettingsData.dockSmartAutoHide + property real backgroundTransparency: SettingsData.dockTransparency + property bool groupByApp: SettingsData.dockGroupByApp + readonly property int borderThickness: SettingsData.dockBorderEnabled ? SettingsData.dockBorderThickness : 0 + + readonly property int hasApps: dockApps.implicitWidth > 0 || dockApps.implicitHeight > 0 + + readonly property real widgetHeight: SettingsData.dockIconSize + readonly property real effectiveBarHeight: widgetHeight + SettingsData.dockSpacing * 2 + 10 + borderThickness * 2 + function getBarHeight(barConfig) { + if (!barConfig) + return 0; + const innerPadding = barConfig.innerPadding ?? 4; + const widgetThickness = Math.max(20, 26 + innerPadding * 0.6); + const barThickness = Math.max(widgetThickness + innerPadding + 4, Theme.barHeight - 4 - (8 - innerPadding)); + const spacing = barConfig.spacing ?? 4; + const bottomGap = barConfig.bottomGap ?? 0; + return barThickness + spacing + bottomGap; + } + + readonly property real barSpacing: { + const defaultBar = SettingsData.barConfigs[0] || SettingsData.getBarConfig("default"); + if (!defaultBar) + return 0; + + const barPos = defaultBar.position ?? SettingsData.Position.Top; + const barIsHorizontal = (barPos === SettingsData.Position.Top || barPos === SettingsData.Position.Bottom); + const barIsVertical = (barPos === SettingsData.Position.Left || barPos === SettingsData.Position.Right); + const samePosition = (SettingsData.dockPosition === barPos); + const dockIsHorizontal = !isVertical; + const dockIsVertical = isVertical; + + if (!(defaultBar.visible ?? true)) + return 0; + const spacing = defaultBar.spacing ?? 4; + const bottomGap = defaultBar.bottomGap ?? 0; + if (dockIsHorizontal && barIsHorizontal && samePosition) { + return spacing + effectiveBarHeight + bottomGap; + } + if (dockIsVertical && barIsVertical && samePosition) { + return spacing + effectiveBarHeight + bottomGap; + } + return 0; + } + + readonly property real adjacentTopBarHeight: { + if (!isVertical || autoHide) + return 0; + const screenName = dock.modelData?.name ?? ""; + const topBar = SettingsData.barConfigs.find(bc => { + if (!bc.enabled || bc.autoHide || !(bc.visible ?? true)) + return false; + if (bc.position !== SettingsData.Position.Top && bc.position !== 0) + return false; + const onThisScreen = bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all") || bc.screenPreferences.includes(screenName); + return onThisScreen; + }); + return getBarHeight(topBar); + } + + readonly property real adjacentLeftBarWidth: { + if (isVertical || autoHide) + return 0; + const screenName = dock.modelData?.name ?? ""; + const leftBar = SettingsData.barConfigs.find(bc => { + if (!bc.enabled || bc.autoHide || !(bc.visible ?? true)) + return false; + if (bc.position !== SettingsData.Position.Left && bc.position !== 2) + return false; + const onThisScreen = bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all") || bc.screenPreferences.includes(screenName); + return onThisScreen; + }); + return getBarHeight(leftBar); + } + + readonly property real dockMargin: SettingsData.dockSpacing + readonly property real positionSpacing: barSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1 + function px(v) { + return Math.round(v * _dpr) / _dpr; + } + + property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData) + property bool revealSticky: false + + readonly property bool shouldHideForWindows: { + if (!SettingsData.dockSmartAutoHide) + return false; + if (!CompositorService.isNiri && !CompositorService.isHyprland) + return false; + + const screenName = dock.modelData?.name ?? ""; + const dockThickness = effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin; + const screenWidth = dock.screen?.width ?? 0; + const screenHeight = dock.screen?.height ?? 0; + + if (CompositorService.isNiri) { + NiriService.windows; + + let currentWorkspaceId = null; + for (let i = 0; i < NiriService.allWorkspaces.length; i++) { + const ws = NiriService.allWorkspaces[i]; + if (ws.output === screenName && ws.is_active) { + currentWorkspaceId = ws.id; + break; + } + } + + if (currentWorkspaceId === null) + return false; + + for (let i = 0; i < NiriService.windows.length; i++) { + const win = NiriService.windows[i]; + if (win.workspace_id !== currentWorkspaceId) + continue; + + // Get window position and size from layout data + const tilePos = win.layout?.tile_pos_in_workspace_view; + const winSize = win.layout?.window_size || win.layout?.tile_size; + + if (tilePos && winSize) { + const winX = tilePos[0]; + const winY = tilePos[1]; + const winW = winSize[0]; + const winH = winSize[1]; + + switch (SettingsData.dockPosition) { + case SettingsData.Position.Top: + if (winY < dockThickness) + return true; + break; + case SettingsData.Position.Bottom: + if (winY + winH > screenHeight - dockThickness) + return true; + break; + case SettingsData.Position.Left: + if (winX < dockThickness) + return true; + break; + case SettingsData.Position.Right: + if (winX + winW > screenWidth - dockThickness) + return true; + break; + } + } else if (!win.is_floating) { + return true; + } + } + + return false; + } + + // Hyprland implementation + Hyprland.focusedWorkspace; + const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName); + + if (filtered.length === 0) + return false; + + for (let i = 0; i < filtered.length; i++) { + const toplevel = filtered[i]; + + let hyprToplevel = null; + if (Hyprland.toplevels) { + const hyprToplevels = Array.from(Hyprland.toplevels.values); + for (let j = 0; j < hyprToplevels.length; j++) { + if (hyprToplevels[j].wayland === toplevel) { + hyprToplevel = hyprToplevels[j]; + break; + } + } + } + + if (!hyprToplevel?.lastIpcObject) + continue; + + const ipc = hyprToplevel.lastIpcObject; + const at = ipc.at; + const size = ipc.size; + if (!at || !size) + continue; + + const monX = hyprToplevel.monitor?.x ?? 0; + const monY = hyprToplevel.monitor?.y ?? 0; + + const winX = at[0] - monX; + const winY = at[1] - monY; + const winW = size[0]; + const winH = size[1]; + + switch (SettingsData.dockPosition) { + case SettingsData.Position.Top: + if (winY < dockThickness) + return true; + break; + case SettingsData.Position.Bottom: + if (winY + winH > screenHeight - dockThickness) + return true; + break; + case SettingsData.Position.Left: + if (winX < dockThickness) + return true; + break; + case SettingsData.Position.Right: + if (winX + winW > screenWidth - dockThickness) + return true; + break; + } + } + + return false; + } + + Timer { + id: revealHold + interval: 250 + repeat: false + onTriggered: dock.revealSticky = false + } + + property bool reveal: { + if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) { + return true; + } + + // Smart auto-hide: show dock when no windows overlap, hide when they do + if (SettingsData.dockSmartAutoHide) { + if (shouldHideForWindows) + return dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky; + return true; // No overlapping windows - show dock + } + + // Regular auto-hide: always hide unless hovering + return !autoHide || dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky; + } + + onContextMenuOpenChanged: { + if (!contextMenuOpen && autoHide && !dockMouseArea.containsMouse) { + revealSticky = true; + revealHold.restart(); + } + } + + Connections { + target: SettingsData + function onDockTransparencyChanged() { + dock.backgroundTransparency = SettingsData.dockTransparency; + } + } + + screen: modelData + visible: { + if (CompositorService.isNiri && NiriService.inOverview) { + return SettingsData.dockOpenOnOverview; + } + return SettingsData.showDock; + } + color: "transparent" + + exclusiveZone: { + if (!SettingsData.showDock || autoHide) + return -1; + if (barSpacing > 0) + return -1; + return px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin); + } + + property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35) + + implicitWidth: isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0 + implicitHeight: !isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0 + + Item { + id: maskItem + parent: dock.contentItem + visible: false + readonly property bool expanded: dock.reveal + x: { + const baseX = dockCore.x + dockMouseArea.x; + if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right) + return baseX - (expanded ? animationHeadroom + borderThickness : 0); + return baseX - (expanded ? borderThickness : 0); + } + y: { + const baseY = dockCore.y + dockMouseArea.y; + if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom) + return baseY - (expanded ? animationHeadroom + borderThickness : 0); + return baseY - (expanded ? borderThickness : 0); + } + width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 : 0) + height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 : 0) + } + + mask: Region { + item: maskItem + } + + property var hoveredButton: { + if (!dockApps.children[0]) { + return null; + } + const layoutItem = dockApps.children[0]; + const flowLayout = layoutItem.children[0]; + let repeater = null; + for (var i = 0; i < flowLayout.children.length; i++) { + const child = flowLayout.children[i]; + if (child && typeof child.count !== "undefined" && typeof child.itemAt === "function") { + repeater = child; + break; + } + } + if (!repeater || !repeater.itemAt) { + return null; + } + for (var i = 0; i < repeater.count; i++) { + const item = repeater.itemAt(i); + if (item && item.dockButton && item.dockButton.showTooltip) { + return item.dockButton; + } + } + return null; + } + + DankTooltip { + id: dockTooltip + targetScreen: dock.screen + } + + Timer { + id: tooltipRevealDelay + interval: 250 + repeat: false + onTriggered: dock.showTooltipForHoveredButton() + } + + function showTooltipForHoveredButton() { + dockTooltip.hide(); + if (!dock.hoveredButton || !dock.reveal || slideXAnimation.running || slideYAnimation.running) + return; + + const buttonGlobalPos = dock.hoveredButton.mapToGlobal(0, 0); + const tooltipText = dock.hoveredButton.tooltipText || ""; + if (!tooltipText) + return; + + const screenX = dock.screen ? (dock.screen.x || 0) : 0; + const screenY = dock.screen ? (dock.screen.y || 0) : 0; + const screenHeight = dock.screen ? dock.screen.height : 0; + + const gap = Theme.spacingS; + const bgMargin = barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness; + const btnW = dock.hoveredButton.width; + const btnH = dock.hoveredButton.height; + + if (!dock.isVertical) { + const isBottom = SettingsData.dockPosition === SettingsData.Position.Bottom; + const globalX = buttonGlobalPos.x + btnW / 2 + adjacentLeftBarWidth; + const tooltipHeight = 32; + const totalFromEdge = bgMargin + dockBackground.height + dock.borderThickness + gap; + const screenRelativeY = isBottom ? (screenHeight - totalFromEdge - tooltipHeight) : totalFromEdge; + dockTooltip.show(tooltipText, globalX, screenRelativeY, dock.screen, false, false); + return; + } + + const isLeft = SettingsData.dockPosition === SettingsData.Position.Left; + const screenWidth = dock.screen ? dock.screen.width : 0; + const totalFromEdge = bgMargin + dockBackground.width + dock.borderThickness + gap; + const tooltipX = isLeft ? (screenX + totalFromEdge) : (screenX + screenWidth - totalFromEdge); + const screenRelativeY = buttonGlobalPos.y - screenY + btnH / 2 + adjacentTopBarHeight; + dockTooltip.show(tooltipText, tooltipX, screenRelativeY, dock.screen, isLeft, !isLeft); + } + + Connections { + target: dock + function onRevealChanged() { + if (!dock.reveal) { + tooltipRevealDelay.stop(); + dockTooltip.hide(); + } else { + tooltipRevealDelay.restart(); + } + } + + function onHoveredButtonChanged() { + dock.showTooltipForHoveredButton(); + } + } + + Item { + id: dockCore + anchors.fill: parent + x: isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? animationHeadroom : 0 + y: !isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? animationHeadroom : 0 + + Connections { + target: dockMouseArea + function onContainsMouseChanged() { + if (dockMouseArea.containsMouse) { + dock.revealSticky = true; + revealHold.stop(); + } else { + if (dock.autoHide && !dock.contextMenuOpen) { + revealHold.restart(); + } + } + } + } + + MouseArea { + id: dockMouseArea + property real currentScreen: modelData ? modelData : dock.screen + property real screenWidth: currentScreen ? currentScreen.geometry.width : 1920 + property real screenHeight: currentScreen ? currentScreen.geometry.height : 1080 + property real maxDockWidth: screenWidth * 0.98 + property real maxDockHeight: screenHeight * 0.98 + + height: { + if (dock.isVertical) { + // Keep the taller hit area regardless of the reveal state to prevent shrinking loop + return Math.min(Math.max(dockBackground.height + 64, 200), maxDockHeight); + } + return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1; + } + width: { + if (dock.isVertical) { + return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1; + } + // Keep the wider hit area regardless of the reveal state to prevent shrinking loop + return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth); + } + anchors { + top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? undefined : parent.top) : undefined + bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined + horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined + left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.left) : undefined + right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined + verticalCenter: dock.isVertical ? parent.verticalCenter : undefined + } + hoverEnabled: true + acceptedButtons: Qt.NoButton + + Behavior on height { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + + Behavior on width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + + Item { + id: dockContainer + anchors.fill: parent + clip: false + + transform: Translate { + id: dockSlide + x: { + if (!dock.isVertical) + return 0; + if (dock.reveal) + return 0; + const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10; + if (SettingsData.dockPosition === SettingsData.Position.Right) { + return hideDistance; + } else { + return -hideDistance; + } + } + y: { + if (dock.isVertical) + return 0; + if (dock.reveal) + return 0; + const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10; + if (SettingsData.dockPosition === SettingsData.Position.Bottom) { + return hideDistance; + } else { + return -hideDistance; + } + } + + Behavior on x { + NumberAnimation { + id: slideXAnimation + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + + Behavior on y { + NumberAnimation { + id: slideYAnimation + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + } + + Item { + id: dockBackground + objectName: "dockBackground" + anchors { + top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? parent.top : undefined) : undefined + bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined + horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined + left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined) : undefined + right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined + verticalCenter: dock.isVertical ? parent.verticalCenter : undefined + } + anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0 + anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0 + anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0 + anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0 + + implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2) + implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2) + width: implicitWidth + height: implicitHeight + + layer.enabled: true + clip: false + + Rectangle { + anchors.fill: parent + color: Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency) + radius: Theme.cornerRadius + } + + Rectangle { + anchors.fill: parent + color: "transparent" + radius: Theme.cornerRadius + border.color: BlurService.borderColor + border.width: BlurService.borderWidth + z: 100 + } + } + + Shape { + id: dockBorderShape + x: dockBackground.x - borderThickness + y: dockBackground.y - borderThickness + width: dockBackground.width + borderThickness * 2 + height: dockBackground.height + borderThickness * 2 + visible: SettingsData.dockBorderEnabled && dock.hasApps + preferredRendererType: Shape.CurveRenderer + + readonly property real borderThickness: Math.max(1, dock.borderThickness) + readonly property real i: borderThickness / 2 + readonly property real cr: Theme.cornerRadius + readonly property real w: dockBackground.width + readonly property real h: dockBackground.height + + readonly property color borderColor: { + const opacity = SettingsData.dockBorderOpacity; + switch (SettingsData.dockBorderColor) { + case "secondary": + return Theme.withAlpha(Theme.secondary, opacity); + case "primary": + return Theme.withAlpha(Theme.primary, opacity); + default: + return Theme.withAlpha(Theme.surfaceText, opacity); + } + } + + ShapePath { + fillColor: "transparent" + strokeColor: dockBorderShape.borderColor + strokeWidth: dockBorderShape.borderThickness + joinStyle: ShapePath.RoundJoin + capStyle: ShapePath.FlatCap + + PathSvg { + path: { + const bt = dockBorderShape.borderThickness; + const i = dockBorderShape.i; + const cr = dockBorderShape.cr + bt - i; + const w = dockBorderShape.w; + const h = dockBorderShape.h; + + let d = `M ${i + cr} ${i}`; + d += ` L ${i + w + 2 * (bt - i) - cr} ${i}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${i + w + 2 * (bt - i)} ${i + cr}`; + d += ` L ${i + w + 2 * (bt - i)} ${i + h + 2 * (bt - i) - cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${i + w + 2 * (bt - i) - cr} ${i + h + 2 * (bt - i)}`; + d += ` L ${i + cr} ${i + h + 2 * (bt - i)}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${i} ${i + h + 2 * (bt - i) - cr}`; + d += ` L ${i} ${i + cr}`; + if (cr > 0) + d += ` A ${cr} ${cr} 0 0 1 ${i + cr} ${i}`; + d += " Z"; + return d; + } + } + } + } + + DockApps { + id: dockApps + + anchors.top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? dockBackground.top : undefined) : undefined + anchors.bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? dockBackground.bottom : undefined) : undefined + anchors.horizontalCenter: !dock.isVertical ? dockBackground.horizontalCenter : undefined + anchors.left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? dockBackground.left : undefined) : undefined + anchors.right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? dockBackground.right : undefined) : undefined + anchors.verticalCenter: dock.isVertical ? dockBackground.verticalCenter : undefined + anchors.topMargin: !dock.isVertical ? SettingsData.dockSpacing : 0 + anchors.bottomMargin: !dock.isVertical ? SettingsData.dockSpacing : 0 + anchors.leftMargin: dock.isVertical ? SettingsData.dockSpacing : 0 + anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0 + + contextMenu: dockVariants.contextMenu + groupByApp: dock.groupByApp + isVertical: dock.isVertical + dockScreen: dock.screen + iconSize: dock.widgetHeight + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock/DockAppButton.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock/DockAppButton.qml new file mode 100644 index 0000000..1d9fb75 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock/DockAppButton.qml @@ -0,0 +1,670 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + clip: false + property var appData + property var contextMenu: null + property var dockApps: null + property int index: -1 + property var parentDockScreen: null + property bool longPressing: false + property bool dragging: false + property point dragStartPos: Qt.point(0, 0) + property real dragAxisOffset: 0 + property int targetIndex: -1 + property int originalIndex: -1 + property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right + property bool showWindowTitle: false + property string windowTitle: "" + property bool isHovered: mouseArea.containsMouse && !dragging + property bool showTooltip: mouseArea.containsMouse && !dragging + property var cachedDesktopEntry: null + property real actualIconSize: 40 + property bool shouldShowIndicator: { + if (!appData) + return false; + if (appData.type === "window") + return true; + if (appData.type === "grouped") + return appData.windowCount > 0; + return appData.isRunning; + } + readonly property string coreIconColorOverride: SettingsData.dockLauncherLogoColorOverride + readonly property bool coreIconHasCustomColor: coreIconColorOverride !== "" && coreIconColorOverride !== "primary" && coreIconColorOverride !== "surface" + readonly property color effectiveCoreIconColor: { + if (coreIconColorOverride === "primary") + return Theme.primary; + if (coreIconColorOverride === "surface") + return Theme.surfaceText; + if (coreIconColorOverride !== "") + return coreIconColorOverride; + return Theme.surfaceText; + } + readonly property real effectiveCoreIconBrightness: coreIconHasCustomColor ? SettingsData.dockLauncherLogoBrightness : 0.0 + readonly property real effectiveCoreIconContrast: coreIconHasCustomColor ? SettingsData.dockLauncherLogoContrast : 0.0 + + function updateDesktopEntry() { + if (!appData || appData.appId === "__SEPARATOR__") { + cachedDesktopEntry = null; + return; + } + if (appData.isCoreApp) { + cachedDesktopEntry = null; + return; + } + const moddedId = Paths.moddedAppId(appData.appId); + cachedDesktopEntry = DesktopEntries.heuristicLookup(moddedId); + } + + Component.onCompleted: updateDesktopEntry() + + onAppDataChanged: updateDesktopEntry() + + Connections { + target: DesktopEntries + function onApplicationsChanged() { + updateDesktopEntry(); + } + } + + Connections { + target: SettingsData + function onAppIdSubstitutionsChanged() { + updateDesktopEntry(); + } + } + property bool isWindowFocused: { + if (!appData) { + return false; + } + + if (appData.type === "window") { + const toplevel = getToplevelObject(); + if (!toplevel) { + return false; + } + return toplevel.activated; + } else if (appData.type === "grouped") { + // For grouped apps, check if any window is focused + const allToplevels = ToplevelManager.toplevels.values; + for (let i = 0; i < allToplevels.length; i++) { + const toplevel = allToplevels[i]; + if (toplevel.appId === appData.appId && toplevel.activated) { + return true; + } + } + } + + return false; + } + property string tooltipText: { + if (!appData || !appData.appId) { + return ""; + } + + let appName; + if (appData.isCoreApp && appData.coreAppData) { + appName = appData.coreAppData.name || appData.appId; + } else { + appName = Paths.getAppName(appData.appId, cachedDesktopEntry); + } + + if ((appData.type === "window" && showWindowTitle) || (appData.type === "grouped" && appData.windowTitle)) { + const title = appData.type === "window" ? windowTitle : appData.windowTitle; + return appName + (title ? " • " + title : ""); + } + + return appName; + } + + function getToplevelObject() { + return appData?.toplevel || null; + } + + function getGroupedToplevels() { + return appData?.allWindows?.map(w => w.toplevel).filter(t => t !== null) || []; + } + + function getHyprToplevelForWayland(waylandToplevel) { + if (!waylandToplevel || !CompositorService.isHyprland || !Hyprland.toplevels) + return null; + const hyprToplevels = Array.from(Hyprland.toplevels.values); + for (let i = 0; i < hyprToplevels.length; i++) { + if (hyprToplevels[i].wayland === waylandToplevel) + return hyprToplevels[i]; + } + return null; + } + + function getSpecialWorkspaceName(waylandToplevel) { + const hyprToplevel = getHyprToplevelForWayland(waylandToplevel); + if (!hyprToplevel) + return ""; + const wsName = String(hyprToplevel.lastIpcObject?.workspace?.name || hyprToplevel.workspace?.name || ""); + if (!wsName.startsWith("special:")) + return ""; + return wsName.slice("special:".length); + } + + function restoreSpecialWorkspaceWindow(waylandToplevel) { + if (!SettingsData.dockRestoreSpecialWorkspaceOnClick || !CompositorService.isHyprland || !waylandToplevel) + return false; + + const specialName = getSpecialWorkspaceName(waylandToplevel); + if (!specialName) + return false; + + Hyprland.dispatch("togglespecialworkspace " + specialName); + Qt.callLater(() => waylandToplevel.activate()); + return true; + } + onIsHoveredChanged: { + if (mouseArea.pressed || dragging) + return; + if (isHovered) { + exitAnimation.stop(); + if (!bounceAnimation.running) { + bounceAnimation.restart(); + } + } else { + bounceAnimation.stop(); + exitAnimation.restart(); + } + } + + readonly property bool animateX: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right + readonly property real animationDistance: actualIconSize + readonly property real animationDirection: { + if (SettingsData.dockPosition === SettingsData.Position.Bottom) + return -1; + if (SettingsData.dockPosition === SettingsData.Position.Top) + return 1; + if (SettingsData.dockPosition === SettingsData.Position.Right) + return -1; + if (SettingsData.dockPosition === SettingsData.Position.Left) + return 1; + return -1; + } + + SequentialAnimation { + id: bounceAnimation + + running: false + + NumberAnimation { + target: root + property: "hoverAnimOffset" + to: animationDirection * animationDistance * 0.25 + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasizedAccel + } + + NumberAnimation { + target: root + property: "hoverAnimOffset" + to: animationDirection * animationDistance * 0.2 + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasizedDecel + } + } + + NumberAnimation { + id: exitAnimation + + running: false + target: root + property: "hoverAnimOffset" + to: 0 + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasizedDecel + } + + Timer { + id: longPressTimer + + interval: 500 + repeat: false + onTriggered: { + if (appData && appData.isPinned) { + longPressing = true; + } + } + } + + MouseArea { + id: mouseArea + + anchors.fill: parent + hoverEnabled: true + enabled: true + preventStealing: dragging || longPressing + cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onPressed: mouse => { + if (mouse.button === Qt.LeftButton && appData && appData.isPinned) { + dragStartPos = Qt.point(mouse.x, mouse.y); + longPressTimer.start(); + } + } + onReleased: mouse => { + longPressTimer.stop(); + + const wasDragging = dragging; + const didReorder = wasDragging && targetIndex >= 0 && targetIndex !== originalIndex && dockApps; + + if (didReorder) + dockApps.movePinnedApp(originalIndex, targetIndex); + + longPressing = false; + dragging = false; + dragAxisOffset = 0; + targetIndex = -1; + originalIndex = -1; + + if (dockApps) { + dockApps.draggedIndex = -1; + dockApps.dropTargetIndex = -1; + } + + if (wasDragging || mouse.button !== Qt.LeftButton) + return; + + handleLeftClick(); + } + + function handleLeftClick() { + if (!appData) + return; + + switch (appData.type) { + case "pinned": + if (!appData.appId) + return; + if (appData.isCoreApp && appData.coreAppData) { + AppSearchService.executeCoreApp(appData.coreAppData); + return; + } + const pinnedEntry = cachedDesktopEntry; + if (pinnedEntry) { + AppUsageHistoryData.addAppUsage({ + "id": appData.appId, + "name": pinnedEntry.name || appData.appId, + "icon": pinnedEntry.icon ? String(pinnedEntry.icon) : "", + "exec": pinnedEntry.exec || "", + "comment": pinnedEntry.comment || "" + }); + } + SessionService.launchDesktopEntry(pinnedEntry); + break; + case "window": + const windowToplevel = getToplevelObject(); + if (windowToplevel) { + if (restoreSpecialWorkspaceWindow(windowToplevel)) + return; + windowToplevel.activate(); + } + break; + case "grouped": + if (appData.windowCount === 0) { + if (!appData.appId) + return; + if (appData.isCoreApp && appData.coreAppData) { + AppSearchService.executeCoreApp(appData.coreAppData); + return; + } + const groupedEntry = cachedDesktopEntry; + if (groupedEntry) { + AppUsageHistoryData.addAppUsage({ + "id": appData.appId, + "name": groupedEntry.name || appData.appId, + "icon": groupedEntry.icon ? String(groupedEntry.icon) : "", + "exec": groupedEntry.exec || "", + "comment": groupedEntry.comment || "" + }); + } + SessionService.launchDesktopEntry(groupedEntry); + } else if (appData.windowCount === 1) { + const groupedToplevel = getToplevelObject(); + if (groupedToplevel) { + if (restoreSpecialWorkspaceWindow(groupedToplevel)) + return; + groupedToplevel.activate(); + } + } else if (contextMenu) { + const shouldHidePin = appData.appId === "org.quickshell" || appData.appId === "com.danklinux.dms"; + contextMenu.showForButton(root, appData, root.height + 25, shouldHidePin, cachedDesktopEntry, parentDockScreen, dockApps); + } + break; + } + } + onPositionChanged: mouse => { + if (longPressing && !dragging) { + const distance = Math.sqrt(Math.pow(mouse.x - dragStartPos.x, 2) + Math.pow(mouse.y - dragStartPos.y, 2)); + if (distance > 5) { + dragging = true; + targetIndex = index; + originalIndex = index; + if (dockApps) { + dockApps.draggedIndex = index; + dockApps.dropTargetIndex = index; + } + } + } + + if (!dragging || !dockApps) + return; + + const axisOffset = isVertical ? (mouse.y - dragStartPos.y) : (mouse.x - dragStartPos.x); + dragAxisOffset = axisOffset; + + const spacing = Math.min(8, Math.max(4, actualIconSize * 0.08)); + const itemSize = actualIconSize * 1.2 + spacing; + const slotOffset = Math.round(axisOffset / itemSize); + const newTargetIndex = Math.max(0, Math.min(dockApps.pinnedAppCount - 1, originalIndex + slotOffset)); + + if (newTargetIndex !== targetIndex) { + targetIndex = newTargetIndex; + dockApps.dropTargetIndex = newTargetIndex; + } + } + onClicked: mouse => { + if (!appData) + return; + + if (mouse.button === Qt.MiddleButton) { + switch (appData.type) { + case "window": + appData.toplevel?.close(); + break; + case "grouped": + if (contextMenu) { + const shouldHidePin = appData.appId === "org.quickshell" || appData.appId === "com.danklinux.dms"; + contextMenu.showForButton(root, appData, root.height, shouldHidePin, cachedDesktopEntry, parentDockScreen, dockApps); + } + break; + default: + if (!appData.appId) + return; + const desktopEntry = cachedDesktopEntry; + if (desktopEntry) { + AppUsageHistoryData.addAppUsage({ + "id": appData.appId, + "name": desktopEntry.name || appData.appId, + "icon": desktopEntry.icon ? String(desktopEntry.icon) : "", + "exec": desktopEntry.exec || "", + "comment": desktopEntry.comment || "" + }); + } + SessionService.launchDesktopEntry(desktopEntry); + break; + } + } else if (mouse.button === Qt.RightButton) { + if (!contextMenu) + return; + const shouldHidePin = appData.appId === "org.quickshell" || appData.appId === "com.danklinux.dms"; + contextMenu.showForButton(root, appData, root.height, shouldHidePin, cachedDesktopEntry, parentDockScreen, dockApps); + } + } + } + + property real hoverAnimOffset: 0 + + Item { + id: visualContent + anchors.fill: parent + + transform: Translate { + id: iconTransform + x: { + if (dragging && !isVertical) + return dragAxisOffset; + if (!dragging && isVertical) + return hoverAnimOffset; + return 0; + } + y: { + if (dragging && isVertical) + return dragAxisOffset; + if (!dragging && !isVertical) + return hoverAnimOffset; + return 0; + } + } + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) + border.width: 2 + border.color: Theme.primary + visible: dragging + z: -1 + } + + AppIconRenderer { + id: coreIcon + + anchors.centerIn: parent + iconSize: actualIconSize + iconValue: appData && appData.isCoreApp && appData.coreAppData ? (appData.coreAppData.icon || "") : "" + colorOverride: effectiveCoreIconColor + brightnessOverride: effectiveCoreIconBrightness + contrastOverride: effectiveCoreIconContrast + fallbackText: "?" + visible: iconValue !== "" + } + + IconImage { + id: iconImg + + anchors.centerIn: parent + implicitSize: appData && (appData.appId === "org.quickshell" || appData.appId === "com.danklinux.dms") ? actualIconSize * 0.85 : actualIconSize + source: { + if (!appData || appData.appId === "__SEPARATOR__") { + return ""; + } + if (appData.isCoreApp && appData.coreAppData) { + return ""; + } + return Paths.getAppIcon(appData.appId, cachedDesktopEntry); + } + mipmap: true + smooth: true + asynchronous: true + visible: status === Image.Ready && !coreIcon.visible + layer.enabled: appData && (appData.appId === "org.quickshell" || appData.appId === "com.danklinux.dms") + layer.smooth: true + layer.mipmap: true + layer.effect: MultiEffect { + saturation: 0 + colorization: 1 + colorizationColor: Theme.primary + } + } + + Rectangle { + width: actualIconSize + height: actualIconSize + anchors.centerIn: parent + visible: !coreIcon.visible && iconImg.status !== Image.Ready && appData && appData.appId && !Paths.isSteamApp(appData.appId) + color: Theme.surfaceLight + radius: Theme.cornerRadius + border.width: 1 + border.color: Theme.primarySelected + + Text { + anchors.centerIn: parent + text: { + if (!appData || !appData.appId) { + return "?"; + } + + let appName; + if (appData.isCoreApp && appData.coreAppData) { + appName = appData.coreAppData.name || appData.appId; + } else { + appName = Paths.getAppName(appData.appId, cachedDesktopEntry); + } + return appName.charAt(0).toUpperCase(); + } + font.pixelSize: Math.max(8, parent.width * 0.35) + color: Theme.primary + font.weight: Font.Bold + } + } + + DankIcon { + anchors.centerIn: parent + size: actualIconSize + name: "sports_esports" + color: Theme.surfaceText + visible: !coreIcon.visible && iconImg.status !== Image.Ready && appData && appData.appId && Paths.isSteamApp(appData.appId) + } + + Loader { + anchors.horizontalCenter: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.horizontalCenter + anchors.verticalCenter: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right ? parent.verticalCenter : undefined + anchors.bottom: SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined + anchors.top: SettingsData.dockPosition === SettingsData.Position.Top ? parent.top : undefined + anchors.left: SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined + anchors.right: SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined + anchors.bottomMargin: SettingsData.dockPosition === SettingsData.Position.Bottom ? -(SettingsData.dockSpacing / 2 + 1.4) : 0 + anchors.topMargin: SettingsData.dockPosition === SettingsData.Position.Top ? -(SettingsData.dockSpacing / 2 + 1.4) : 0 + anchors.leftMargin: SettingsData.dockPosition === SettingsData.Position.Left ? -(SettingsData.dockSpacing / 2 + 1.4) : 0 + anchors.rightMargin: SettingsData.dockPosition === SettingsData.Position.Right ? -(SettingsData.dockSpacing / 2 + 1.4) : 0 + + sourceComponent: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right ? columnIndicator : rowIndicator + + visible: root.shouldShowIndicator + } + } + + Component { + id: rowIndicator + + Row { + spacing: 2 + + Repeater { + model: { + if (!appData) + return 0; + if (appData.type === "grouped") { + return Math.min(appData.windowCount, 4); + } else if (appData.type === "window" || appData.isRunning) { + return 1; + } + return 0; + } + + Rectangle { + width: { + if (SettingsData.dockIndicatorStyle === "circle") { + return Math.max(4, actualIconSize * 0.1); + } + return appData && appData.type === "grouped" && appData.windowCount > 1 ? Math.max(3, actualIconSize * 0.1) : Math.max(6, actualIconSize * 0.2); + } + height: { + if (SettingsData.dockIndicatorStyle === "circle") { + return Math.max(4, actualIconSize * 0.1); + } + return Math.max(2, actualIconSize * 0.05); + } + radius: SettingsData.dockIndicatorStyle === "circle" ? width / 2 : Theme.cornerRadius + color: { + if (!appData) { + return "transparent"; + } + + if (appData.type !== "grouped" || appData.windowCount === 1) { + if (isWindowFocused) { + return Theme.primary; + } + return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6); + } + + if (appData.type === "grouped" && appData.windowCount > 1) { + const groupToplevels = getGroupedToplevels(); + if (index < groupToplevels.length && groupToplevels[index].activated) { + return Theme.primary; + } + } + + return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6); + } + } + } + } + } + + Component { + id: columnIndicator + + Column { + spacing: 2 + + Repeater { + model: { + if (!appData) + return 0; + if (appData.type === "grouped") { + return Math.min(appData.windowCount, 4); + } else if (appData.type === "window" || appData.isRunning) { + return 1; + } + return 0; + } + + Rectangle { + width: { + if (SettingsData.dockIndicatorStyle === "circle") { + return Math.max(4, actualIconSize * 0.1); + } + return Math.max(2, actualIconSize * 0.05); + } + height: { + if (SettingsData.dockIndicatorStyle === "circle") { + return Math.max(4, actualIconSize * 0.1); + } + return appData && appData.type === "grouped" && appData.windowCount > 1 ? Math.max(3, actualIconSize * 0.1) : Math.max(6, actualIconSize * 0.2); + } + radius: SettingsData.dockIndicatorStyle === "circle" ? width / 2 : Theme.cornerRadius + color: { + if (!appData) { + return "transparent"; + } + + if (appData.type !== "grouped" || appData.windowCount === 1) { + if (isWindowFocused) { + return Theme.primary; + } + return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6); + } + + if (appData.type === "grouped" && appData.windowCount > 1) { + const groupToplevels = getGroupedToplevels(); + if (index < groupToplevels.length && groupToplevels[index].activated) { + return Theme.primary; + } + } + + return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6); + } + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock/DockApps.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock/DockApps.qml new file mode 100644 index 0000000..dbdc517 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock/DockApps.qml @@ -0,0 +1,646 @@ +import QtQuick +import Quickshell +import Quickshell.Hyprland +import qs.Common +import qs.Services + +Item { + id: root + + property var contextMenu: null + property bool requestDockShow: false + property int pinnedAppCount: 0 + property bool groupByApp: false + property bool isVertical: false + property var dockScreen: null + property real iconSize: 40 + property int draggedIndex: -1 + property int dropTargetIndex: -1 + property bool suppressShiftAnimation: false + property int maxVisibleApps: SettingsData.dockMaxVisibleApps + property int maxVisibleRunningApps: SettingsData.dockMaxVisibleRunningApps + property bool overflowExpanded: false + property int overflowItemCount: 0 + + readonly property real baseImplicitWidth: isVertical ? baseAppHeight : baseAppWidth + readonly property real baseImplicitHeight: isVertical ? baseAppWidth : baseAppHeight + readonly property real baseAppWidth: { + let count = 0; + const items = repeater.dockItems; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (item.isInOverflow) + continue; + if (item.type === "separator") { + count += 8 / (iconSize * 1.2); + } else { + count += 1; + } + } + return count * (iconSize * 1.2) + Math.max(0, count - 1) * layoutFlow.spacing; + } + readonly property real baseAppHeight: iconSize + + clip: false + implicitWidth: isVertical ? appLayout.height : appLayout.width + implicitHeight: isVertical ? appLayout.width : appLayout.height + + function dockIndexToPinnedIndex(dockIndex) { + if (!SettingsData.dockLauncherEnabled) + return dockIndex; + + const launcherPos = SessionData.dockLauncherPosition; + return dockIndex < launcherPos ? dockIndex : dockIndex - 1; + } + + function movePinnedApp(fromDockIndex, toDockIndex) { + const fromPinnedIndex = dockIndexToPinnedIndex(fromDockIndex); + const toPinnedIndex = dockIndexToPinnedIndex(toDockIndex); + + if (fromPinnedIndex === toPinnedIndex) + return; + + const currentPinned = [...(SessionData.pinnedApps || [])]; + if (fromPinnedIndex < 0 || fromPinnedIndex >= currentPinned.length || toPinnedIndex < 0 || toPinnedIndex >= currentPinned.length) + return; + + const movedApp = currentPinned.splice(fromPinnedIndex, 1)[0]; + currentPinned.splice(toPinnedIndex, 0, movedApp); + SessionData.setPinnedApps(currentPinned); + } + + Item { + id: appLayout + width: layoutFlow.width + height: layoutFlow.height + + Behavior on width { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + + Behavior on height { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + anchors.horizontalCenter: root.isVertical ? undefined : parent.horizontalCenter + anchors.verticalCenter: root.isVertical ? parent.verticalCenter : undefined + anchors.left: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined + anchors.right: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined + anchors.top: root.isVertical ? undefined : parent.top + + Flow { + id: layoutFlow + flow: root.isVertical ? Flow.TopToBottom : Flow.LeftToRight + spacing: Math.min(8, Math.max(4, root.iconSize * 0.08)) + + Repeater { + id: repeater + + property var dockItems: [] + + model: ScriptModel { + values: repeater.dockItems + objectProp: "uniqueKey" + } + + Component.onCompleted: updateModel() + + function isOnScreen(toplevel, screenName) { + if (!toplevel.screens) + return false; + for (let i = 0; i < toplevel.screens.length; i++) { + if (toplevel.screens[i]?.name === screenName) + return true; + } + return false; + } + + function getCoreAppData(appId) { + if (typeof AppSearchService === "undefined") + return null; + const coreApps = AppSearchService.coreApps || []; + for (let i = 0; i < coreApps.length; i++) { + if (coreApps[i].builtInPluginId === appId) + return coreApps[i]; + } + return null; + } + + function getCoreAppDataByTitle(windowTitle) { + if (typeof AppSearchService === "undefined" || !windowTitle) + return null; + const coreApps = AppSearchService.coreApps || []; + for (let i = 0; i < coreApps.length; i++) { + if (coreApps[i].name === windowTitle) + return coreApps[i]; + } + return null; + } + + function buildBaseItems() { + const items = []; + const pinnedApps = [...(SessionData.pinnedApps || [])]; + const allToplevels = CompositorService.sortedToplevels; + const sortedToplevels = (SettingsData.dockIsolateDisplays && root.dockScreen) ? allToplevels.filter(t => isOnScreen(t, root.dockScreen.name)) : allToplevels; + + if (root.groupByApp) { + return buildGroupedItems(pinnedApps, sortedToplevels); + } + return buildUngroupedItems(pinnedApps, sortedToplevels); + } + + function buildGroupedItems(pinnedApps, sortedToplevels) { + const items = []; + const appGroups = new Map(); + + pinnedApps.forEach(rawAppId => { + const appId = Paths.moddedAppId(rawAppId); + const coreAppData = getCoreAppData(appId); + appGroups.set(appId, { + appId: appId, + isPinned: true, + windows: [], + isCoreApp: coreAppData !== null, + coreAppData: coreAppData + }); + }); + + sortedToplevels.forEach((toplevel, index) => { + const rawAppId = toplevel.appId || "unknown"; + let appId = Paths.moddedAppId(rawAppId); + let coreAppData = null; + + if (rawAppId === "org.quickshell" || rawAppId === "com.danklinux.dms") { + coreAppData = getCoreAppDataByTitle(toplevel.title); + if (coreAppData) + appId = coreAppData.builtInPluginId; + } + + if (!appGroups.has(appId)) { + appGroups.set(appId, { + appId: appId, + isPinned: false, + windows: [], + isCoreApp: coreAppData !== null, + coreAppData: coreAppData + }); + } + appGroups.get(appId).windows.push({ + toplevel: toplevel, + index: index + }); + }); + + const pinnedGroups = []; + const unpinnedGroups = []; + + appGroups.forEach((group, appId) => { + const firstWindow = group.windows.length > 0 ? group.windows[0] : null; + const item = { + uniqueKey: "grouped_" + appId, + type: "grouped", + appId: appId, + toplevel: firstWindow ? firstWindow.toplevel : null, + isPinned: group.isPinned, + isRunning: group.windows.length > 0, + windowCount: group.windows.length, + allWindows: group.windows, + isCoreApp: group.isCoreApp || false, + coreAppData: group.coreAppData || null, + isInOverflow: false + }; + (group.isPinned ? pinnedGroups : unpinnedGroups).push(item); + }); + + pinnedGroups.forEach(item => items.push(item)); + insertLauncher(items); + + if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) { + items.push(createSeparator("separator_grouped")); + } + unpinnedGroups.forEach(item => items.push(item)); + + root.pinnedAppCount = pinnedGroups.length + (SettingsData.dockLauncherEnabled ? 1 : 0); + return { + items, + pinnedCount: pinnedGroups.length, + runningCount: unpinnedGroups.length + }; + } + + function buildUngroupedItems(pinnedApps, sortedToplevels) { + const items = []; + const runningAppIds = new Set(); + const windowItems = []; + + sortedToplevels.forEach((toplevel, index) => { + let uniqueKey = "window_" + index; + if (CompositorService.isHyprland && Hyprland.toplevels) { + const hyprlandToplevels = Array.from(Hyprland.toplevels.values); + for (let i = 0; i < hyprlandToplevels.length; i++) { + if (hyprlandToplevels[i].wayland === toplevel) { + uniqueKey = "window_" + hyprlandToplevels[i].address; + break; + } + } + } + + const rawAppId = toplevel.appId || "unknown"; + const moddedAppId = Paths.moddedAppId(rawAppId); + let coreAppData = null; + let isCoreApp = false; + + if (rawAppId === "org.quickshell" || rawAppId === "com.danklinux.dms") { + coreAppData = getCoreAppDataByTitle(toplevel.title); + if (coreAppData) + isCoreApp = true; + } + + const finalAppId = isCoreApp ? coreAppData.builtInPluginId : moddedAppId; + windowItems.push({ + uniqueKey: uniqueKey, + type: "window", + appId: finalAppId, + toplevel: toplevel, + isPinned: false, + isRunning: true, + isCoreApp: isCoreApp, + coreAppData: coreAppData, + isInOverflow: false + }); + runningAppIds.add(finalAppId); + }); + + const remainingWindowItems = windowItems.slice(); + + pinnedApps.forEach(rawAppId => { + const appId = Paths.moddedAppId(rawAppId); + const coreAppData = getCoreAppData(appId); + const matchIndex = remainingWindowItems.findIndex(item => item.appId === appId); + + if (matchIndex !== -1) { + const windowItem = remainingWindowItems.splice(matchIndex, 1)[0]; + windowItem.isPinned = true; + windowItem.uniqueKey = "pinned_" + appId; + if (!windowItem.isCoreApp && coreAppData) { + windowItem.isCoreApp = true; + windowItem.coreAppData = coreAppData; + } + items.push(windowItem); + } else { + items.push({ + uniqueKey: "pinned_" + appId, + type: "pinned", + appId: appId, + toplevel: null, + isPinned: true, + isRunning: runningAppIds.has(appId), + isCoreApp: coreAppData !== null, + coreAppData: coreAppData, + isInOverflow: false + }); + } + }); + + root.pinnedAppCount = pinnedApps.length + (SettingsData.dockLauncherEnabled ? 1 : 0); + insertLauncher(items); + + if (pinnedApps.length > 0 && remainingWindowItems.length > 0) { + items.push(createSeparator("separator_ungrouped")); + } + remainingWindowItems.forEach(item => items.push(item)); + + return { + items, + pinnedCount: pinnedApps.length, + runningCount: remainingWindowItems.length + }; + } + + function insertLauncher(targetArray) { + if (!SettingsData.dockLauncherEnabled) + return; + const launcherItem = { + uniqueKey: "launcher_button", + type: "launcher", + appId: "__LAUNCHER__", + toplevel: null, + isPinned: true, + isRunning: false + }; + const pos = Math.max(0, Math.min(SessionData.dockLauncherPosition, targetArray.length)); + targetArray.splice(pos, 0, launcherItem); + } + + function createSeparator(key) { + return { + uniqueKey: key, + type: "separator", + appId: "__SEPARATOR__", + toplevel: null, + isPinned: false, + isRunning: false + }; + } + + function markAsOverflow(item) { + return { + uniqueKey: item.uniqueKey, + type: item.type, + appId: item.appId, + toplevel: item.toplevel, + isPinned: item.isPinned, + isRunning: item.isRunning, + windowCount: item.windowCount, + allWindows: item.allWindows, + isCoreApp: item.isCoreApp, + coreAppData: item.coreAppData, + isInOverflow: true + }; + } + + function markAsVisible(item) { + return { + uniqueKey: item.uniqueKey, + type: item.type, + appId: item.appId, + toplevel: item.toplevel, + isPinned: item.isPinned, + isRunning: item.isRunning, + windowCount: item.windowCount, + allWindows: item.allWindows, + isCoreApp: item.isCoreApp, + coreAppData: item.coreAppData, + isInOverflow: false + }; + } + + function applyOverflow(baseResult) { + const { + items + } = baseResult; + const maxPinned = root.maxVisibleApps; + const maxRunning = root.maxVisibleRunningApps; + + const pinnedItems = items.filter(i => (i.type === "pinned" || i.type === "grouped" || i.type === "window") && i.isPinned && i.appId !== "__LAUNCHER__"); + const runningItems = items.filter(i => (i.type === "window" || i.type === "grouped") && i.isRunning && !i.isPinned); + + const pinnedOverflow = maxPinned > 0 && pinnedItems.length > maxPinned; + const runningOverflow = maxRunning > 0 && runningItems.length > maxRunning; + + if (!pinnedOverflow && !runningOverflow) { + root.overflowItemCount = 0; + return items.map(i => markAsVisible(i)); + } + + const visiblePinnedKeys = new Set(pinnedOverflow ? pinnedItems.slice(0, maxPinned).map(i => i.uniqueKey) : pinnedItems.map(i => i.uniqueKey)); + const visibleRunningKeys = new Set(runningOverflow ? runningItems.slice(0, maxRunning).map(i => i.uniqueKey) : runningItems.map(i => i.uniqueKey)); + + const overflowPinnedCount = pinnedOverflow ? pinnedItems.length - maxPinned : 0; + const overflowRunningCount = runningOverflow ? runningItems.length - maxRunning : 0; + const totalOverflow = overflowPinnedCount + overflowRunningCount; + root.overflowItemCount = totalOverflow; + + const finalItems = []; + let addedSeparator = false; + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + switch (item.type) { + case "launcher": + finalItems.push(item); + break; + case "separator": + break; + case "pinned": + case "grouped": + case "window": + if (item.isPinned && item.appId !== "__LAUNCHER__") { + if (visiblePinnedKeys.has(item.uniqueKey)) { + finalItems.push(markAsVisible(item)); + } else { + finalItems.push(markAsOverflow(item)); + } + } else if (item.isRunning && !item.isPinned) { + if (!addedSeparator && finalItems.length > 0) { + finalItems.push(createSeparator("separator_overflow")); + addedSeparator = true; + } + if (visibleRunningKeys.has(item.uniqueKey)) { + finalItems.push(markAsVisible(item)); + } else { + finalItems.push(markAsOverflow(item)); + } + } + break; + } + } + + if (totalOverflow > 0) { + const toggleIndex = finalItems.findIndex(i => i.type === "separator"); + const insertPos = toggleIndex >= 0 ? toggleIndex : finalItems.length; + finalItems.splice(insertPos, 0, { + uniqueKey: "overflow_toggle", + type: "overflow-toggle", + appId: "__OVERFLOW_TOGGLE__", + toplevel: null, + isPinned: false, + isRunning: false, + overflowCount: totalOverflow + }); + } + + return finalItems; + } + + function updateModel() { + const baseResult = buildBaseItems(); + dockItems = applyOverflow(baseResult); + } + + delegate: Item { + id: delegateItem + + property var dockButton: itemData.type === "launcher" ? launcherButton : button + property var itemData: modelData + readonly property bool isOverflowToggle: itemData.type === "overflow-toggle" + readonly property bool isInOverflow: itemData.isInOverflow === true + + clip: false + z: (itemData.type === "launcher" ? launcherButton.dragging : button.dragging) ? 100 : 0 + visible: !isInOverflow || root.overflowExpanded + opacity: (isInOverflow && !root.overflowExpanded) ? 0 : 1 + scale: (isInOverflow && !root.overflowExpanded) ? 0.8 : 1 + + width: (isInOverflow && !root.overflowExpanded) ? 0 : (itemData.type === "separator" ? (root.isVertical ? root.iconSize : 8) : (root.isVertical ? root.iconSize : root.iconSize * 1.2)) + height: (isInOverflow && !root.overflowExpanded) ? 0 : (itemData.type === "separator" ? (root.isVertical ? 8 : root.iconSize) : (root.isVertical ? root.iconSize * 1.2 : root.iconSize)) + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + + property real shiftOffset: { + if (root.draggedIndex < 0 || !itemData.isPinned || itemData.type === "separator") + return 0; + if (model.index === root.draggedIndex) + return 0; + + const dragIdx = root.draggedIndex; + const dropIdx = root.dropTargetIndex; + const myIdx = model.index; + const shiftAmount = root.iconSize * 1.2 + layoutFlow.spacing; + + if (dropIdx < 0) + return 0; + if (dragIdx < dropIdx && myIdx > dragIdx && myIdx <= dropIdx) + return -shiftAmount; + if (dragIdx > dropIdx && myIdx >= dropIdx && myIdx < dragIdx) + return shiftAmount; + return 0; + } + + transform: Translate { + x: root.isVertical ? 0 : delegateItem.shiftOffset + y: root.isVertical ? delegateItem.shiftOffset : 0 + + Behavior on x { + enabled: !root.suppressShiftAnimation + NumberAnimation { + duration: 150 + easing.type: Easing.OutCubic + } + } + + Behavior on y { + enabled: !root.suppressShiftAnimation + NumberAnimation { + duration: 150 + easing.type: Easing.OutCubic + } + } + } + + Rectangle { + visible: itemData.type === "separator" + width: root.isVertical ? root.iconSize * 0.5 : 2 + height: root.isVertical ? 2 : root.iconSize * 0.5 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) + radius: 1 + anchors.centerIn: parent + } + + DockOverflowButton { + id: overflowButton + visible: isOverflowToggle + anchors.centerIn: parent + width: delegateItem.width + height: delegateItem.height + actualIconSize: root.iconSize + overflowCount: itemData.overflowCount || 0 + overflowExpanded: root.overflowExpanded + isVertical: root.isVertical + onClicked: root.overflowExpanded = !root.overflowExpanded + } + + DockLauncherButton { + id: launcherButton + visible: itemData.type === "launcher" + anchors.centerIn: parent + width: delegateItem.width + height: delegateItem.height + actualIconSize: root.iconSize + dockApps: root + index: model.index + } + + DockAppButton { + id: button + visible: !isOverflowToggle && itemData.type !== "separator" && itemData.type !== "launcher" + anchors.centerIn: parent + width: delegateItem.width + height: delegateItem.height + actualIconSize: root.iconSize + appData: itemData + contextMenu: root.contextMenu + dockApps: root + index: model.index + parentDockScreen: root.dockScreen + showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped" + windowTitle: { + const title = itemData?.toplevel?.title || "(Unnamed)"; + return title.length > 50 ? title.substring(0, 47) + "..." : title; + } + } + } + } + } + } + + Connections { + target: CompositorService + function onToplevelsChanged() { + repeater.updateModel(); + } + } + + Connections { + target: SessionData + function onPinnedAppsChanged() { + root.suppressShiftAnimation = true; + root.draggedIndex = -1; + root.dropTargetIndex = -1; + repeater.updateModel(); + Qt.callLater(() => { + root.suppressShiftAnimation = false; + }); + } + function onDockLauncherPositionChanged() { + root.suppressShiftAnimation = true; + root.draggedIndex = -1; + root.dropTargetIndex = -1; + repeater.updateModel(); + Qt.callLater(() => { + root.suppressShiftAnimation = false; + }); + } + } + + Connections { + target: SettingsData + function onDockIsolateDisplaysChanged() { + repeater.updateModel(); + } + function onDockLauncherEnabledChanged() { + root.suppressShiftAnimation = true; + root.draggedIndex = -1; + root.dropTargetIndex = -1; + repeater.updateModel(); + Qt.callLater(() => { + root.suppressShiftAnimation = false; + }); + } + function onDockMaxVisibleAppsChanged() { + repeater.updateModel(); + } + function onDockMaxVisibleRunningAppsChanged() { + repeater.updateModel(); + } + } + + onGroupByAppChanged: repeater.updateModel() +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock/DockContextMenu.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock/DockContextMenu.qml new file mode 100644 index 0000000..183f331 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock/DockContextMenu.qml @@ -0,0 +1,585 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Common +import qs.Services +import qs.Widgets + +PanelWindow { + id: root + + WindowBlur { + targetWindow: root + blurX: menuContainer.x + blurY: menuContainer.y + blurWidth: root.visible ? menuContainer.width : 0 + blurHeight: root.visible ? menuContainer.height : 0 + blurRadius: Theme.cornerRadius + } + + WlrLayershell.namespace: "dms:dock-context-menu" + + property var appData: null + property var anchorItem: null + property real dockVisibleHeight: 40 + property int margin: 10 + property bool hidePin: false + property var desktopEntry: null + property bool isDmsWindow: appData?.appId === "org.quickshell" || appData?.appId === "com.danklinux.dms" + property var dockApps: null + + function showForButton(button, data, dockHeight, hidePinOption, entry, dockScreen, parentDockApps) { + if (dockScreen) { + root.screen = dockScreen; + } + + anchorItem = button; + appData = data; + dockVisibleHeight = dockHeight || 40; + hidePin = hidePinOption || false; + desktopEntry = entry || null; + dockApps = parentDockApps || null; + + visible = true; + } + function close() { + visible = false; + } + + screen: null + visible: false + WlrLayershell.layer: WlrLayershell.Overlay + WlrLayershell.exclusiveZone: -1 + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + color: "transparent" + anchors { + top: true + left: true + right: true + bottom: true + } + + property point anchorPos: Qt.point(screen.width / 2, screen.height - 100) + + onAnchorItemChanged: updatePosition() + onVisibleChanged: { + if (visible) { + updatePosition(); + } + } + + function updatePosition() { + if (!anchorItem) { + anchorPos = Qt.point(screen.width / 2, screen.height - 100); + return; + } + + const dockWindow = anchorItem.Window.window; + if (!dockWindow) { + anchorPos = Qt.point(screen.width / 2, screen.height - 100); + return; + } + + const buttonPosInDock = anchorItem.mapToItem(dockWindow.contentItem, 0, 0); + let actualDockHeight = root.dockVisibleHeight; + + function findDockBackground(item) { + if (item.objectName === "dockBackground") { + return item; + } + for (var i = 0; i < item.children.length; i++) { + const found = findDockBackground(item.children[i]); + if (found) { + return found; + } + } + return null; + } + + const dockBackground = findDockBackground(dockWindow.contentItem); + let actualDockWidth = dockWindow.width; + if (dockBackground) { + actualDockHeight = dockBackground.height; + actualDockWidth = dockBackground.width; + } + + const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right; + const dockMargin = SettingsData.dockMargin + 16; + let buttonScreenX, buttonScreenY; + + if (isVertical) { + const dockContentHeight = dockWindow.height; + const screenHeight = root.screen.height; + const dockTopMargin = Math.round((screenHeight - dockContentHeight) / 2); + buttonScreenY = dockTopMargin + buttonPosInDock.y + anchorItem.height / 2; + + if (SettingsData.dockPosition === SettingsData.Position.Right) { + buttonScreenX = root.screen.width - actualDockWidth - dockMargin - 20; + } else { + buttonScreenX = actualDockWidth + dockMargin + 20; + } + } else { + const isDockAtBottom = SettingsData.dockPosition === SettingsData.Position.Bottom; + + if (isDockAtBottom) { + buttonScreenY = root.screen.height - actualDockHeight - dockMargin - 20; + } else { + buttonScreenY = actualDockHeight + dockMargin + 20; + } + + const dockContentWidth = dockWindow.width; + const screenWidth = root.screen.width; + const dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2); + buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2; + } + + anchorPos = Qt.point(buttonScreenX, buttonScreenY); + } + + Rectangle { + id: menuContainer + + x: { + const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right; + if (isVertical) { + const isDockAtRight = SettingsData.dockPosition === SettingsData.Position.Right; + if (isDockAtRight) { + return Math.max(10, root.anchorPos.x - width + 30); + } else { + return Math.min(root.width - width - 10, root.anchorPos.x - 30); + } + } else { + const left = 10; + const right = root.width - width - 10; + const want = root.anchorPos.x - width / 2; + return Math.max(left, Math.min(right, want)); + } + } + y: { + const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right; + if (isVertical) { + const top = 10; + const bottom = root.height - height - 10; + const want = root.anchorPos.y - height / 2; + return Math.max(top, Math.min(bottom, want)); + } else { + const isDockAtBottom = SettingsData.dockPosition === SettingsData.Position.Bottom; + if (isDockAtBottom) { + return Math.max(10, root.anchorPos.y - height + 30); + } else { + return Math.min(root.height - height - 10, root.anchorPos.y - 30); + } + } + } + + width: Math.min(400, Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)) + height: menuColumn.implicitHeight + Theme.spacingS * 2 + color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + radius: Theme.cornerRadius + border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: BlurService.enabled ? BlurService.borderWidth : 1 + + opacity: root.visible ? 1 : 0 + visible: opacity > 0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + + Rectangle { + anchors.fill: parent + anchors.topMargin: 4 + anchors.leftMargin: 2 + anchors.rightMargin: -2 + anchors.bottomMargin: -4 + radius: parent.radius + color: Qt.rgba(0, 0, 0, 0.15) + z: -1 + } + + Column { + id: menuColumn + width: parent.width - Theme.spacingS * 2 + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Theme.spacingS + spacing: 1 + + // Window list for grouped apps + Repeater { + model: { + if (!root.appData || root.appData.type !== "grouped") + return []; + + const toplevels = []; + const allToplevels = ToplevelManager.toplevels.values; + for (let i = 0; i < allToplevels.length; i++) { + const toplevel = allToplevels[i]; + if (toplevel.appId === root.appData.appId) { + toplevels.push(toplevel); + } + } + return toplevels; + } + + Rectangle { + width: parent.width + height: 28 + radius: Theme.cornerRadius + color: windowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + StyledText { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.right: closeButton.left + anchors.rightMargin: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + text: (modelData && modelData.title) ? modelData.title : I18n.tr("(Unnamed)") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + + Rectangle { + id: closeButton + anchors.right: parent.right + anchors.rightMargin: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + width: 20 + height: 20 + radius: 10 + color: closeMouseArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2) : "transparent" + + DankIcon { + anchors.centerIn: parent + name: "close" + size: 12 + color: closeMouseArea.containsMouse ? Theme.error : Theme.surfaceText + } + + MouseArea { + id: closeMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (modelData && modelData.close) { + modelData.close(); + } + root.close(); + } + } + } + + DankRipple { + id: windowRipple + rippleColor: Theme.surfaceText + cornerRadius: Theme.cornerRadius + } + + MouseArea { + id: windowArea + anchors.fill: parent + anchors.rightMargin: 24 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: mouse => windowRipple.trigger(mouse.x, mouse.y) + onClicked: { + if (modelData && modelData.activate) { + modelData.activate(); + } + root.close(); + } + } + } + } + + Rectangle { + visible: { + if (!root.appData) + return false; + if (root.appData.type !== "grouped") + return false; + return root.appData.windowCount > 0; + } + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + } + + Repeater { + model: root.desktopEntry && root.desktopEntry.actions ? root.desktopEntry.actions : [] + + Rectangle { + width: parent.width + height: 28 + radius: Theme.cornerRadius + color: actionArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + Item { + anchors.verticalCenter: parent.verticalCenter + width: 16 + height: 16 + visible: modelData.icon && modelData.icon !== "" + + IconImage { + anchors.fill: parent + source: modelData.icon ? Paths.resolveIconPath(modelData.icon) : "" + smooth: true + asynchronous: true + visible: status === Image.Ready + } + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: modelData.name || "" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + } + + DankRipple { + id: actionRipple + rippleColor: Theme.surfaceText + cornerRadius: Theme.cornerRadius + } + + MouseArea { + id: actionArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: mouse => actionRipple.trigger(mouse.x, mouse.y) + onClicked: { + if (modelData) { + SessionService.launchDesktopAction(root.desktopEntry, modelData); + } + root.close(); + } + } + } + } + + Rectangle { + visible: { + if (!root.desktopEntry?.actions || root.desktopEntry.actions.length === 0) { + return false; + } + return !root.hidePin || (!root.isDmsWindow && root.desktopEntry && SessionService.nvidiaCommand); + } + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + } + + Rectangle { + visible: !root.hidePin + width: parent.width + height: 28 + radius: Theme.cornerRadius + color: pinArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + DankIcon { + anchors.verticalCenter: parent.verticalCenter + name: root.appData && root.appData.isPinned ? "keep_off" : "push_pin" + size: 14 + color: Theme.surfaceText + opacity: 0.7 + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: root.appData && root.appData.isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + } + + DankRipple { + id: pinRipple + rippleColor: Theme.surfaceText + cornerRadius: Theme.cornerRadius + } + + MouseArea { + id: pinArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y) + onClicked: { + if (!root.appData) + return; + + if (root.appData.isPinned) { + SessionData.removePinnedApp(root.appData.appId); + } else { + SessionData.addPinnedApp(root.appData.appId); + } + root.close(); + } + } + } + + Rectangle { + visible: { + const hasNvidia = !root.isDmsWindow && root.desktopEntry && SessionService.nvidiaCommand; + const hasWindow = root.appData && (root.appData.type === "window" || (root.appData.type === "grouped" && root.appData.windowCount > 0)); + const hasPinOption = !root.hidePin; + const hasContentAbove = hasPinOption || hasNvidia; + return hasContentAbove && hasWindow; + } + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + } + + Rectangle { + visible: !root.isDmsWindow && root.desktopEntry && SessionService.nvidiaCommand + width: parent.width + height: 28 + radius: Theme.cornerRadius + color: nvidiaArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + DankIcon { + anchors.verticalCenter: parent.verticalCenter + name: "memory" + size: 14 + color: Theme.surfaceText + opacity: 0.7 + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("Launch on dGPU") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + } + + DankRipple { + id: nvidiaRipple + rippleColor: Theme.surfaceText + cornerRadius: Theme.cornerRadius + } + + MouseArea { + id: nvidiaArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: mouse => nvidiaRipple.trigger(mouse.x, mouse.y) + onClicked: { + if (root.desktopEntry) { + SessionService.launchDesktopEntry(root.desktopEntry, true); + } + root.close(); + } + } + } + + Rectangle { + visible: root.appData && (root.appData.type === "window" || (root.appData.type === "grouped" && root.appData.windowCount > 0)) + width: parent.width + height: 28 + radius: Theme.cornerRadius + color: closeArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.right: parent.right + anchors.rightMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + DankIcon { + anchors.verticalCenter: parent.verticalCenter + name: "close" + size: 14 + color: closeArea.containsMouse ? Theme.error : Theme.surfaceText + opacity: 0.7 + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: root.appData && root.appData.type === "grouped" ? I18n.tr("Close All Windows") : I18n.tr("Close Window") + font.pixelSize: Theme.fontSizeSmall + color: closeArea.containsMouse ? Theme.error : Theme.surfaceText + font.weight: Font.Normal + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + } + + DankRipple { + id: closeRipple + rippleColor: Theme.error + cornerRadius: Theme.cornerRadius + } + + MouseArea { + id: closeArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: mouse => closeRipple.trigger(mouse.x, mouse.y) + onClicked: { + if (root.appData?.type === "window") { + root.appData?.toplevel?.close(); + } else if (root.appData?.type === "grouped") { + root.appData?.allWindows?.forEach(window => window.toplevel?.close()); + } + root.close(); + } + } + } + } + } + + MouseArea { + anchors.fill: parent + z: -1 + onClicked: root.close() + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock/DockLauncherButton.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock/DockLauncherButton.qml new file mode 100644 index 0000000..84b52ba --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock/DockLauncherButton.qml @@ -0,0 +1,296 @@ +import QtQuick +import QtQuick.Effects +import Quickshell.Widgets +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + clip: false + property var dockApps: null + property int index: -1 + property bool longPressing: false + property bool dragging: false + property point dragStartPos: Qt.point(0, 0) + property real dragAxisOffset: 0 + property int targetIndex: -1 + property int originalIndex: -1 + property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right + property bool isHovered: mouseArea.containsMouse && !dragging + property bool showTooltip: mouseArea.containsMouse && !dragging + property real actualIconSize: 40 + + readonly property string tooltipText: I18n.tr("Applications") + + readonly property color effectiveLogoColor: { + const override = SettingsData.dockLauncherLogoColorOverride; + if (override === "primary") + return Theme.primary; + if (override === "surface") + return Theme.surfaceText; + if (override !== "") + return override; + return Theme.surfaceText; + } + + onIsHoveredChanged: { + if (mouseArea.pressed || dragging) + return; + if (isHovered) { + exitAnimation.stop(); + if (!bounceAnimation.running) { + bounceAnimation.restart(); + } + } else { + bounceAnimation.stop(); + exitAnimation.restart(); + } + } + + readonly property bool animateX: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right + readonly property real animationDistance: actualIconSize + readonly property real animationDirection: { + if (SettingsData.dockPosition === SettingsData.Position.Bottom) + return -1; + if (SettingsData.dockPosition === SettingsData.Position.Top) + return 1; + if (SettingsData.dockPosition === SettingsData.Position.Right) + return -1; + if (SettingsData.dockPosition === SettingsData.Position.Left) + return 1; + return -1; + } + + SequentialAnimation { + id: bounceAnimation + + running: false + + NumberAnimation { + target: root + property: "hoverAnimOffset" + to: animationDirection * animationDistance * 0.25 + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasizedAccel + } + + NumberAnimation { + target: root + property: "hoverAnimOffset" + to: animationDirection * animationDistance * 0.2 + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasizedDecel + } + } + + NumberAnimation { + id: exitAnimation + + running: false + target: root + property: "hoverAnimOffset" + to: 0 + duration: Anims.durShort + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasizedDecel + } + + Timer { + id: longPressTimer + + interval: 500 + repeat: false + onTriggered: { + longPressing = true; + } + } + + MouseArea { + id: mouseArea + + anchors.fill: parent + hoverEnabled: true + enabled: true + preventStealing: dragging || longPressing + cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + onPressed: mouse => { + if (mouse.button === Qt.LeftButton) { + dragStartPos = Qt.point(mouse.x, mouse.y); + longPressTimer.start(); + } + } + onReleased: mouse => { + longPressTimer.stop(); + + const wasDragging = dragging; + const didReorder = wasDragging && targetIndex >= 0 && dockApps; + + if (didReorder) { + SessionData.setDockLauncherPosition(targetIndex); + } + + longPressing = false; + dragging = false; + dragAxisOffset = 0; + targetIndex = -1; + originalIndex = -1; + + if (dockApps) { + dockApps.draggedIndex = -1; + dockApps.dropTargetIndex = -1; + } + + if (wasDragging || mouse.button !== Qt.LeftButton) + return; + + PopoutService.toggleDankLauncherV2(); + } + onPositionChanged: mouse => { + if (longPressing && !dragging) { + const distance = Math.sqrt(Math.pow(mouse.x - dragStartPos.x, 2) + Math.pow(mouse.y - dragStartPos.y, 2)); + if (distance > 5) { + dragging = true; + targetIndex = index; + originalIndex = index; + if (dockApps) { + dockApps.draggedIndex = index; + dockApps.dropTargetIndex = index; + } + } + } + + if (!dragging || !dockApps) + return; + + const axisOffset = isVertical ? (mouse.y - dragStartPos.y) : (mouse.x - dragStartPos.x); + dragAxisOffset = axisOffset; + + const spacing = Math.min(8, Math.max(4, actualIconSize * 0.08)); + const itemSize = actualIconSize * 1.2 + spacing; + const slotOffset = Math.round(axisOffset / itemSize); + const newTargetIndex = Math.max(0, Math.min(dockApps.pinnedAppCount, originalIndex + slotOffset)); + + if (newTargetIndex !== targetIndex) { + targetIndex = newTargetIndex; + dockApps.dropTargetIndex = newTargetIndex; + } + } + } + + property real hoverAnimOffset: 0 + + Item { + id: visualContent + anchors.fill: parent + + transform: Translate { + x: dragging && !isVertical ? dragAxisOffset : (!dragging && isVertical ? hoverAnimOffset : 0) + y: dragging && isVertical ? dragAxisOffset : (!dragging && !isVertical ? hoverAnimOffset : 0) + } + + Item { + anchors.centerIn: parent + width: actualIconSize + height: actualIconSize + + DankIcon { + visible: SettingsData.dockLauncherLogoMode === "apps" + anchors.centerIn: parent + name: "apps" + size: actualIconSize - 4 + color: Theme.widgetIconColor + } + + SystemLogo { + visible: SettingsData.dockLauncherLogoMode === "os" + anchors.centerIn: parent + width: actualIconSize + SettingsData.dockLauncherLogoSizeOffset + height: actualIconSize + SettingsData.dockLauncherLogoSizeOffset + colorOverride: effectiveLogoColor + brightnessOverride: SettingsData.dockLauncherLogoBrightness + contrastOverride: SettingsData.dockLauncherLogoContrast + } + + IconImage { + visible: SettingsData.dockLauncherLogoMode === "dank" + anchors.centerIn: parent + width: actualIconSize + SettingsData.dockLauncherLogoSizeOffset + height: actualIconSize + SettingsData.dockLauncherLogoSizeOffset + smooth: true + mipmap: true + asynchronous: true + source: "file://" + Theme.shellDir + "/assets/danklogo.svg" + layer.enabled: effectiveLogoColor !== "" + layer.smooth: true + layer.mipmap: true + layer.effect: MultiEffect { + saturation: 0 + colorization: 1 + colorizationColor: effectiveLogoColor + } + } + + IconImage { + visible: SettingsData.dockLauncherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc) + anchors.centerIn: parent + width: actualIconSize + SettingsData.dockLauncherLogoSizeOffset + height: actualIconSize + SettingsData.dockLauncherLogoSizeOffset + smooth: true + asynchronous: true + source: { + if (CompositorService.isNiri) { + return "file://" + Theme.shellDir + "/assets/niri.svg"; + } else if (CompositorService.isHyprland) { + return "file://" + Theme.shellDir + "/assets/hyprland.svg"; + } else if (CompositorService.isDwl) { + return "file://" + Theme.shellDir + "/assets/mango.png"; + } else if (CompositorService.isSway) { + return "file://" + Theme.shellDir + "/assets/sway.svg"; + } else if (CompositorService.isScroll) { + return "file://" + Theme.shellDir + "/assets/sway.svg"; + } else if (CompositorService.isMiracle) { + return "file://" + Theme.shellDir + "/assets/miraclewm.svg"; + } else if (CompositorService.isLabwc) { + return "file://" + Theme.shellDir + "/assets/labwc.png"; + } + return ""; + } + layer.enabled: effectiveLogoColor !== "" + layer.effect: MultiEffect { + saturation: 0 + colorization: 1 + colorizationColor: effectiveLogoColor + brightness: { + SettingsData.dockLauncherLogoBrightness; + } + contrast: { + SettingsData.dockLauncherLogoContrast; + } + } + } + + IconImage { + visible: SettingsData.dockLauncherLogoMode === "custom" && SettingsData.dockLauncherLogoCustomPath !== "" + anchors.centerIn: parent + width: actualIconSize + SettingsData.dockLauncherLogoSizeOffset + height: actualIconSize + SettingsData.dockLauncherLogoSizeOffset + smooth: true + asynchronous: true + source: SettingsData.dockLauncherLogoCustomPath ? "file://" + SettingsData.dockLauncherLogoCustomPath.replace("file://", "") : "" + layer.enabled: effectiveLogoColor !== "" + layer.effect: MultiEffect { + saturation: 0 + colorization: 1 + colorizationColor: effectiveLogoColor + brightness: SettingsData.dockLauncherLogoBrightness + contrast: SettingsData.dockLauncherLogoContrast + } + } + } + } +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock/DockOverflowButton.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock/DockOverflowButton.qml new file mode 100644 index 0000000..0d6b439 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modules/Dock/DockOverflowButton.qml @@ -0,0 +1,75 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Item { + id: root + + property real actualIconSize: 40 + property int overflowCount: 0 + property bool overflowExpanded: false + property bool isVertical: false + + signal clicked + + Rectangle { + id: buttonBackground + anchors.centerIn: parent + width: actualIconSize + height: actualIconSize + radius: Theme.cornerRadius + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, mouseArea.containsMouse ? 0.2 : 0.1) + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + } + } + + DankIcon { + id: arrowIcon + anchors.centerIn: parent + size: actualIconSize * 0.6 + name: "expand_more" + color: Theme.surfaceText + rotation: isVertical ? (overflowExpanded ? 180 : 0) : (overflowExpanded ? 90 : -90) + + Behavior on rotation { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Easing.OutCubic + } + } + } + } + + Rectangle { + visible: overflowCount > 0 && !overflowExpanded && SettingsData.dockShowOverflowBadge + anchors.right: buttonBackground.right + anchors.top: buttonBackground.top + anchors.rightMargin: -4 + anchors.topMargin: -4 + width: Math.max(18, badgeText.width + 8) + height: 18 + radius: 9 + color: Theme.primary + z: 10 + + StyledText { + id: badgeText + anchors.centerIn: parent + text: `+${overflowCount}` + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.onPrimary + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.clicked() + } +} |