1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 13:32:50 -05:00

feat: Implement Dank Launcher button on the Dock

- Configurable with custom icons/logos
- Respects light/dark theme
- Drag & Drop in place
This commit is contained in:
purian23
2026-01-22 16:52:38 -05:00
parent 3f0d0f4d95
commit 2681fe87bb
7 changed files with 651 additions and 11 deletions

View File

@@ -83,6 +83,7 @@ Singleton {
property string nightModeLocationProvider: ""
property var pinnedApps: []
property int dockLauncherPosition: 0
property var hiddenTrayIds: []
property var recentColors: []
property bool showThirdPartyPlugins: false

View File

@@ -431,6 +431,13 @@ Singleton {
property real dockBorderOpacity: 1.0
property int dockBorderThickness: 1
property bool dockIsolateDisplays: false
property bool dockLauncherEnabled: false
property string dockLauncherLogoMode: "apps"
property string dockLauncherLogoCustomPath: ""
property string dockLauncherLogoColorOverride: ""
property int dockLauncherLogoSizeOffset: 0
property real dockLauncherLogoBrightness: 0.5
property real dockLauncherLogoContrast: 1
property bool notificationOverlayEnabled: false
property int overviewRows: 2

View File

@@ -255,6 +255,13 @@ var SPEC = {
dockBorderOpacity: { def: 1.0, coerce: percentToUnit },
dockBorderThickness: { def: 1 },
dockIsolateDisplays: { def: false },
dockLauncherEnabled: { def: false },
dockLauncherLogoMode: { def: "apps" },
dockLauncherLogoCustomPath: { def: "" },
dockLauncherLogoColorOverride: { def: "" },
dockLauncherLogoSizeOffset: { def: 0 },
dockLauncherLogoBrightness: { def: 0.5, coerce: percentToUnit },
dockLauncherLogoContrast: { def: 1, coerce: percentToUnit },
notificationOverlayEnabled: { def: false },
overviewRows: { def: 2, persist: false },

View File

@@ -208,7 +208,7 @@ Item {
targetIndex = -1;
originalIndex = -1;
if (dockApps && !didReorder) {
if (dockApps) {
dockApps.draggedIndex = -1;
dockApps.dropTargetIndex = -1;
}

View File

@@ -22,18 +22,34 @@ Item {
implicitWidth: isVertical ? appLayout.height : appLayout.width
implicitHeight: isVertical ? appLayout.width : appLayout.height
function movePinnedApp(fromIndex, toIndex) {
if (fromIndex === toIndex) {
function dockIndexToPinnedIndex(dockIndex) {
if (!SettingsData.dockLauncherEnabled) {
return dockIndex;
}
const launcherPos = SessionData.dockLauncherPosition;
if (dockIndex < launcherPos) {
return dockIndex;
} else {
return dockIndex - 1;
}
}
function movePinnedApp(fromDockIndex, toDockIndex) {
const fromPinnedIndex = dockIndexToPinnedIndex(fromDockIndex);
const toPinnedIndex = dockIndexToPinnedIndex(toDockIndex);
if (fromPinnedIndex === toPinnedIndex) {
return;
}
const currentPinned = [...(SessionData.pinnedApps || [])];
if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0 || toIndex >= currentPinned.length) {
if (fromPinnedIndex < 0 || fromPinnedIndex >= currentPinned.length || toPinnedIndex < 0 || toPinnedIndex >= currentPinned.length) {
return;
}
const movedApp = currentPinned.splice(fromIndex, 1)[0];
currentPinned.splice(toIndex, 0, movedApp);
const movedApp = currentPinned.splice(fromPinnedIndex, 1)[0];
currentPinned.splice(toPinnedIndex, 0, movedApp);
SessionData.setPinnedApps(currentPinned);
}
@@ -75,6 +91,23 @@ Item {
return false;
}
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 updateModel() {
const items = [];
const pinnedApps = [...(SessionData.pinnedApps || [])];
@@ -136,6 +169,8 @@ Item {
pinnedGroups.forEach(item => items.push(item));
insertLauncher(items);
if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) {
items.push({
uniqueKey: "separator_grouped",
@@ -148,7 +183,7 @@ Item {
}
unpinnedGroups.forEach(item => items.push(item));
root.pinnedAppCount = pinnedGroups.length;
root.pinnedAppCount = pinnedGroups.length + (SettingsData.dockLauncherEnabled ? 1 : 0);
} else {
pinnedApps.forEach(rawAppId => {
const appId = Paths.moddedAppId(rawAppId);
@@ -162,7 +197,9 @@ Item {
});
});
root.pinnedAppCount = pinnedApps.length;
root.pinnedAppCount = pinnedApps.length + (SettingsData.dockLauncherEnabled ? 1 : 0);
insertLauncher(items);
if (pinnedApps.length > 0 && sortedToplevels.length > 0) {
items.push({
@@ -203,10 +240,10 @@ Item {
delegate: Item {
id: delegateItem
property alias dockButton: button
property var dockButton: itemData.type === "launcher" ? launcherButton : button
property var itemData: modelData
clip: false
z: button.dragging ? 100 : 0
z: (itemData.type === "launcher" ? launcherButton.dragging : button.dragging) ? 100 : 0
width: itemData.type === "separator" ? (root.isVertical ? root.iconSize : 8) : (root.isVertical ? root.iconSize : root.iconSize * 1.2)
height: itemData.type === "separator" ? (root.isVertical ? 8 : root.iconSize) : (root.isVertical ? root.iconSize * 1.2 : root.iconSize)
@@ -261,9 +298,22 @@ Item {
anchors.centerIn: parent
}
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: itemData.type !== "separator"
visible: itemData.type !== "separator" && itemData.type !== "launcher"
anchors.centerIn: parent
width: delegateItem.width
@@ -314,5 +364,27 @@ Item {
function onDockIsolateDisplaysChanged() {
repeater.updateModel();
}
function onDockLauncherEnabledChanged() {
root.suppressShiftAnimation = true;
root.draggedIndex = -1;
root.dropTargetIndex = -1;
repeater.updateModel();
Qt.callLater(() => {
root.suppressShiftAnimation = false;
});
}
}
Connections {
target: SessionData
function onDockLauncherPositionChanged() {
root.suppressShiftAnimation = true;
root.draggedIndex = -1;
root.dropTargetIndex = -1;
repeater.updateModel();
Qt.callLater(() => {
root.suppressShiftAnimation = false;
});
}
}
}

View File

@@ -0,0 +1,294 @@
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: true
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.dockLauncherPosition = 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.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.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
}
}
}
}
}

View File

@@ -164,6 +164,265 @@ Item {
}
}
SettingsCard {
width: parent.width
iconName: "apps"
title: I18n.tr("Launcher Button")
settingKey: "dockLauncher"
SettingsToggleRow {
settingKey: "dockLauncherEnabled"
tags: ["dock", "launcher", "button", "apps"]
text: I18n.tr("Show Launcher Button")
description: I18n.tr("Add a draggable launcher button to the dock")
checked: SettingsData.dockLauncherEnabled
onToggled: checked => SettingsData.set("dockLauncherEnabled", checked)
}
Column {
width: parent.width
spacing: Theme.spacingL
visible: SettingsData.dockLauncherEnabled
StyledText {
width: parent.width
text: I18n.tr("Long press and drag the launcher button to reposition it in the dock")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
Column {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: I18n.tr("Launcher Icon")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
Item {
width: parent.width
height: logoModeGroup.implicitHeight
clip: true
DankButtonGroup {
id: logoModeGroup
anchors.horizontalCenter: parent.horizontalCenter
buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL
minButtonWidth: parent.width < 480 ? 44 : 64
textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
model: {
const modes = [I18n.tr("Apps Icon"), I18n.tr("OS Logo"), I18n.tr("Dank")];
if (CompositorService.isNiri) {
modes.push("niri");
} else if (CompositorService.isHyprland) {
modes.push("Hyprland");
} else if (CompositorService.isDwl) {
modes.push("mango");
} else if (CompositorService.isSway) {
modes.push("Sway");
} else if (CompositorService.isScroll) {
modes.push("Scroll");
} else {
modes.push(I18n.tr("Compositor"));
}
modes.push(I18n.tr("Custom"));
return modes;
}
currentIndex: {
if (SettingsData.dockLauncherLogoMode === "apps")
return 0;
if (SettingsData.dockLauncherLogoMode === "os")
return 1;
if (SettingsData.dockLauncherLogoMode === "dank")
return 2;
if (SettingsData.dockLauncherLogoMode === "compositor")
return 3;
if (SettingsData.dockLauncherLogoMode === "custom")
return 4;
return 0;
}
onSelectionChanged: (index, selected) => {
if (!selected)
return;
switch (index) {
case 0:
SettingsData.set("dockLauncherLogoMode", "apps");
break;
case 1:
SettingsData.set("dockLauncherLogoMode", "os");
break;
case 2:
SettingsData.set("dockLauncherLogoMode", "dank");
break;
case 3:
SettingsData.set("dockLauncherLogoMode", "compositor");
break;
case 4:
SettingsData.set("dockLauncherLogoMode", "custom");
break;
}
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingL
visible: SettingsData.dockLauncherLogoMode !== "apps"
Column {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: I18n.tr("Color Override")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
Item {
width: parent.width
height: colorOverrideRow.implicitHeight
clip: true
Row {
id: colorOverrideRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
DankButtonGroup {
id: colorModeGroup
buttonPadding: parent.parent.width < 480 ? Theme.spacingS : Theme.spacingL
minButtonWidth: parent.parent.width < 480 ? 44 : 64
textSize: parent.parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")]
currentIndex: {
const override = SettingsData.dockLauncherLogoColorOverride;
if (override === "")
return 0;
if (override === "primary")
return 1;
if (override === "surface")
return 2;
return 3;
}
onSelectionChanged: (index, selected) => {
if (!selected)
return;
switch (index) {
case 0:
SettingsData.set("dockLauncherLogoColorOverride", "");
break;
case 1:
SettingsData.set("dockLauncherLogoColorOverride", "primary");
break;
case 2:
SettingsData.set("dockLauncherLogoColorOverride", "surface");
break;
case 3:
const currentOverride = SettingsData.dockLauncherLogoColorOverride;
const isPreset = currentOverride === "" || currentOverride === "primary" || currentOverride === "surface";
if (isPreset) {
SettingsData.set("dockLauncherLogoColorOverride", "#ffffff");
}
break;
}
}
}
Rectangle {
id: colorPickerCircle
visible: {
const override = SettingsData.dockLauncherLogoColorOverride;
return override !== "" && override !== "primary" && override !== "surface";
}
width: 36
height: 36
radius: 18
color: {
const override = SettingsData.dockLauncherLogoColorOverride;
if (override !== "" && override !== "primary" && override !== "surface")
return override;
return "#ffffff";
}
border.color: Theme.outline
border.width: 1
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!PopoutService.colorPickerModal)
return;
PopoutService.colorPickerModal.selectedColor = SettingsData.dockLauncherLogoColorOverride;
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Choose Dock Launcher Logo Color");
PopoutService.colorPickerModal.onColorSelectedCallback = function (selectedColor) {
SettingsData.set("dockLauncherLogoColorOverride", selectedColor);
};
PopoutService.colorPickerModal.show();
}
}
}
}
}
}
SettingsSliderRow {
settingKey: "dockLauncherLogoSizeOffset"
tags: ["dock", "launcher", "logo", "size", "offset", "scale"]
text: I18n.tr("Size Offset")
minimum: -12
maximum: 12
value: SettingsData.dockLauncherLogoSizeOffset
defaultValue: 0
onSliderValueChanged: newValue => SettingsData.set("dockLauncherLogoSizeOffset", newValue)
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: {
const override = SettingsData.dockLauncherLogoColorOverride;
return override !== "" && override !== "primary" && override !== "surface";
}
SettingsSliderRow {
settingKey: "dockLauncherLogoBrightness"
tags: ["dock", "launcher", "logo", "brightness", "color"]
text: I18n.tr("Brightness")
minimum: 0
maximum: 100
value: Math.round(SettingsData.dockLauncherLogoBrightness * 100)
unit: "%"
defaultValue: 50
onSliderValueChanged: newValue => SettingsData.set("dockLauncherLogoBrightness", newValue / 100)
}
SettingsSliderRow {
settingKey: "dockLauncherLogoContrast"
tags: ["dock", "launcher", "logo", "contrast", "color"]
text: I18n.tr("Contrast")
minimum: 0
maximum: 200
value: Math.round(SettingsData.dockLauncherLogoContrast * 100)
unit: "%"
defaultValue: 100
onSliderValueChanged: newValue => SettingsData.set("dockLauncherLogoContrast", newValue / 100)
}
}
}
}
}
SettingsCard {
width: parent.width
iconName: "photo_size_select_large"