1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-27 06:52:50 -05:00
Files
DankMaterialShell/quickshell/Modules/Dock/DockApps.qml
2026-01-26 16:05:22 -05:00

617 lines
27 KiB
QML

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
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") {
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
};
(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") {
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
});
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;
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
});
}
});
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 applyOverflow(baseResult) {
const {
items
} = baseResult;
const maxPinned = root.maxVisibleApps;
const maxRunning = root.maxVisibleRunningApps;
const hideRunning = maxRunning === 0;
const pinnedItems = items.filter(i => (i.type === "pinned" || i.type === "grouped" || i.type === "window") && i.isPinned && i.appId !== "__LAUNCHER__");
const runningItems = hideRunning ? [] : items.filter(i => (i.type === "window" || i.type === "grouped") && i.isRunning && !i.isPinned);
const pinnedOverflow = maxPinned > 0 && pinnedItems.length > maxPinned;
const runningOverflow = !hideRunning && maxRunning > 0 && runningItems.length > maxRunning;
if (!pinnedOverflow && !runningOverflow) {
root.overflowItemCount = 0;
if (hideRunning) {
return items.filter(i => !((i.type === "window" || i.type === "grouped") && i.isRunning && !i.isPinned) && i.type !== "separator");
}
return items;
}
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(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(item);
} else if (!hideRunning) {
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()
}