summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/dms/Modules/Dock
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-hyprland-theme/theme-data/dms/Modules/Dock')
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Dock/Dock.qml671
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Dock/DockAppButton.qml670
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Dock/DockApps.qml646
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Dock/DockContextMenu.qml585
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Dock/DockLauncherButton.qml296
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/Dock/DockOverflowButton.qml75
6 files changed, 2943 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/Dock/Dock.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Dock/Dock.qml
new file mode 100644
index 0000000..136f23a
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modules/Dock/DockAppButton.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Dock/DockAppButton.qml
new file mode 100644
index 0000000..1d9fb75
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modules/Dock/DockApps.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Dock/DockApps.qml
new file mode 100644
index 0000000..dbdc517
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modules/Dock/DockContextMenu.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Dock/DockContextMenu.qml
new file mode 100644
index 0000000..183f331
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modules/Dock/DockLauncherButton.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Dock/DockLauncherButton.qml
new file mode 100644
index 0000000..84b52ba
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modules/Dock/DockOverflowButton.qml b/raveos-hyprland-theme/theme-data/dms/Modules/Dock/DockOverflowButton.qml
new file mode 100644
index 0000000..0d6b439
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/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()
+ }
+}