mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-27 15:02:50 -05:00
617 lines
27 KiB
QML
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()
|
|
}
|