mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-14 23:34:01 -04:00
453 lines
19 KiB
QML
453 lines
19 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import qs.Common
|
|
import qs.Services
|
|
import qs.Modules.Dock
|
|
import qs.Widgets
|
|
import "../../Common/settings/DockConfig.js" as DockConfig
|
|
import "ApplicationModel.js" as ApplicationModel
|
|
|
|
Item {
|
|
id: root
|
|
|
|
required property var surfaceContext
|
|
required property var options
|
|
property bool renderItems: true
|
|
property var mixedStrip: null
|
|
property int mixedDropIndex: -1
|
|
onDraggedIndexChanged: {
|
|
if (draggedIndex >= 0)
|
|
return;
|
|
mixedDropIndex = -1;
|
|
mixedStrip?.cancelDrag();
|
|
modelUpdate.schedule();
|
|
}
|
|
function updateMixedDrag(item, x, y) {
|
|
if (!mixedStrip)
|
|
return;
|
|
mixedDropIndex = mixedStrip.dropIndex(item, x, y, entry => entry.appData?.isPinned === true && !["launcher", "overflow-toggle"].includes(entry.appData.type));
|
|
mixedStrip.draggingIndex = mixedStrip.appSlot(draggedIndex);
|
|
mixedStrip.targetIndex = mixedDropIndex;
|
|
}
|
|
function dropTarget(localTarget) {
|
|
return mixedStrip?.model[mixedDropIndex]?.appIndex ?? localTarget;
|
|
}
|
|
readonly property var items: repeater.dockItems
|
|
property int desktopRevision: 0
|
|
readonly property var desktopEntries: {
|
|
desktopRevision;
|
|
const entries = {};
|
|
for (const item of items) {
|
|
if (item.isCoreApp || !item.appId || item.appId.startsWith("__") || item.appId in entries)
|
|
continue;
|
|
entries[item.appId] = DesktopEntries.heuristicLookup(item.appId);
|
|
}
|
|
return entries;
|
|
}
|
|
function refreshDesktopEntries() {
|
|
desktopRevision++;
|
|
}
|
|
Connections {
|
|
target: DesktopEntries
|
|
function onApplicationsChanged() {
|
|
root.refreshDesktopEntries();
|
|
}
|
|
}
|
|
readonly property real itemSpacing: layoutFlow.spacing
|
|
readonly property int renderedCount: repeater.count
|
|
function itemAt(index) {
|
|
return repeater.itemAt(index);
|
|
}
|
|
readonly property bool barHosted: surfaceContext.kind === "bar"
|
|
readonly property var pinnedApps: barHosted ? SessionData.barPinnedApps : SessionData.getDockPins(surfaceContext.configId)
|
|
function setPinnedApps(apps) {
|
|
if (barHosted)
|
|
SessionData.setBarPinnedApps(apps);
|
|
else
|
|
SessionData.setDockPins(surfaceContext.configId, apps);
|
|
}
|
|
function isPinnedApp(id) {
|
|
return pinnedApps.includes(id);
|
|
}
|
|
function addPinnedApp(id) {
|
|
if (id && !isPinnedApp(id))
|
|
setPinnedApps(pinnedApps.concat([id]));
|
|
}
|
|
function removePinnedApp(id) {
|
|
setPinnedApps(pinnedApps.filter(app => app !== id));
|
|
}
|
|
function settleDrag(apply) {
|
|
suppressShiftAnimation = true;
|
|
apply();
|
|
draggedIndex = -1;
|
|
dropTargetIndex = -1;
|
|
modelUpdate.schedule();
|
|
modelUpdate.flush();
|
|
Qt.callLater(() => root.suppressShiftAnimation = false);
|
|
}
|
|
property var contextMenu: null
|
|
property var trashContextMenu: 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 bool usesOverlayLayer: false
|
|
property int draggedIndex: -1
|
|
property int dropTargetIndex: -1
|
|
property bool suppressShiftAnimation: false
|
|
property int maxVisibleApps: root.options.maxVisibleApps
|
|
property int maxVisibleRunningApps: root.options.maxVisibleRunningApps
|
|
property bool overflowExpanded: false
|
|
property int overflowItemCount: 0
|
|
|
|
readonly property real targetSize: barHosted ? iconSize * (options.iconSizePercentage ?? 100) / 100 : iconSize
|
|
readonly property real indicatorLane: root.barHosted && !root.options.hideIndicators ? DockConfig.indicatorLane(root.options) : 0
|
|
readonly property real crossSize: {
|
|
// The lane is mirrored so icons stay centered on the bar.
|
|
const needed = root.targetSize + root.indicatorLane * 2;
|
|
if (!root.barHosted)
|
|
return needed;
|
|
// A bar pill cannot grow past its widget thickness, so the lane yields there.
|
|
return Math.min(needed, root.surfaceContext?.widgetThickness ?? needed);
|
|
}
|
|
readonly property var hoveredButton: {
|
|
for (let i = 0; i < repeater.count; i++) {
|
|
const button = repeater.itemAt(i)?.dockButton;
|
|
if (button?.showTooltip)
|
|
return button;
|
|
}
|
|
return null;
|
|
}
|
|
clip: false
|
|
property bool _switchingPosition: false
|
|
implicitWidth: appLayout.width
|
|
implicitHeight: appLayout.height
|
|
|
|
function focusFirst() {
|
|
for (let i = 0; i < repeater.count; i++) {
|
|
const item = repeater.itemAt(i);
|
|
if (!item?.activeFocusOnTab)
|
|
continue;
|
|
item.forceActiveFocus();
|
|
return;
|
|
}
|
|
}
|
|
function movePinnedApp(fromDockIndex, toDockIndex) {
|
|
const next = ApplicationModel.movePin(pinnedApps, items, fromDockIndex, toDockIndex);
|
|
if (next !== pinnedApps)
|
|
setPinnedApps(next);
|
|
}
|
|
|
|
Item {
|
|
id: appLayout
|
|
width: layoutFlow.width
|
|
height: layoutFlow.height
|
|
|
|
Behavior on width {
|
|
enabled: !root._switchingPosition
|
|
NumberAnimation {
|
|
duration: Theme.shortDuration
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
|
|
Behavior on height {
|
|
enabled: !root._switchingPosition
|
|
NumberAnimation {
|
|
duration: Theme.shortDuration
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
anchors.centerIn: parent
|
|
|
|
Flow {
|
|
id: layoutFlow
|
|
layoutDirection: I18n.isRtl ? Qt.RightToLeft : Qt.LeftToRight
|
|
flow: root.isVertical ? Flow.TopToBottom : Flow.LeftToRight
|
|
spacing: root.options.itemSpacing ?? Theme.spacingS
|
|
|
|
Repeater {
|
|
id: repeater
|
|
|
|
property var dockItems: []
|
|
|
|
model: ScriptModel {
|
|
values: root.renderItems ? 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;
|
|
return (AppSearchService.coreApps || []).find(app => app.builtInPluginId === appId) ?? null;
|
|
}
|
|
|
|
function buildBaseItems() {
|
|
const pinnedApps = [...(root.pinnedApps || [])];
|
|
const allToplevels = root.visibleWindows;
|
|
const sortedToplevels = (root.options.isolateDisplays && 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();
|
|
const separatePinnedAndRunning = root.options.separatePinnedAndRunningApps;
|
|
|
|
if (!separatePinnedAndRunning) {
|
|
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 identity = ApplicationModel.windowIdentity(toplevel, Paths.moddedAppId(toplevel.appId || "unknown"), AppSearchService.coreApps || []);
|
|
const appId = identity.appId;
|
|
const coreAppData = identity.coreAppData;
|
|
|
|
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);
|
|
});
|
|
|
|
if (separatePinnedAndRunning) {
|
|
pinnedApps.forEach(rawAppId => {
|
|
const appId = Paths.moddedAppId(rawAppId);
|
|
const coreAppData = getCoreAppData(appId);
|
|
pinnedGroups.push(ApplicationModel.pinnedItem(appId, coreAppData, false));
|
|
});
|
|
}
|
|
|
|
pinnedGroups.forEach(item => items.push(item));
|
|
insertLauncher(items);
|
|
|
|
if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) {
|
|
items.push(ApplicationModel.separator("separator_grouped"));
|
|
}
|
|
unpinnedGroups.forEach(item => items.push(item));
|
|
|
|
root.pinnedAppCount = pinnedGroups.length + (root.options.launcherEnabled ? 1 : 0);
|
|
return {
|
|
items,
|
|
pinnedCount: pinnedGroups.length,
|
|
runningCount: unpinnedGroups.length
|
|
};
|
|
}
|
|
|
|
function buildUngroupedItems(pinnedApps, sortedToplevels) {
|
|
const items = [];
|
|
const runningAppIds = new Set();
|
|
const windowItems = [];
|
|
const separatePinnedAndRunning = root.options.separatePinnedAndRunningApps;
|
|
|
|
sortedToplevels.forEach((toplevel, index) => {
|
|
const uniqueKey = CompositorService.windowKey(toplevel);
|
|
const identity = ApplicationModel.windowIdentity(toplevel, Paths.moddedAppId(toplevel.appId || "unknown"), AppSearchService.coreApps || []);
|
|
const finalAppId = identity.appId;
|
|
const coreAppData = identity.coreAppData;
|
|
const isCoreApp = identity.isCoreApp;
|
|
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);
|
|
if (separatePinnedAndRunning) {
|
|
items.push(ApplicationModel.pinnedItem(appId, coreAppData, false));
|
|
return;
|
|
}
|
|
|
|
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(ApplicationModel.pinnedItem(appId, coreAppData, runningAppIds.has(appId)));
|
|
}
|
|
});
|
|
|
|
root.pinnedAppCount = pinnedApps.length + (root.options.launcherEnabled ? 1 : 0);
|
|
insertLauncher(items);
|
|
|
|
if (pinnedApps.length > 0 && remainingWindowItems.length > 0) {
|
|
items.push(ApplicationModel.separator("separator_ungrouped"));
|
|
}
|
|
remainingWindowItems.forEach(item => items.push(item));
|
|
|
|
return {
|
|
items,
|
|
pinnedCount: pinnedApps.length,
|
|
runningCount: remainingWindowItems.length
|
|
};
|
|
}
|
|
|
|
function insertLauncher(targetArray) {
|
|
if (!root.options.launcherEnabled)
|
|
return;
|
|
const launcherItem = {
|
|
uniqueKey: "launcher_button",
|
|
type: "launcher",
|
|
appId: "__LAUNCHER__",
|
|
toplevel: null,
|
|
isPinned: true,
|
|
isRunning: false
|
|
};
|
|
targetArray.unshift(launcherItem);
|
|
}
|
|
|
|
function updateModel() {
|
|
const baseResult = buildBaseItems();
|
|
const overflow = ApplicationModel.overflow(baseResult.items, root.maxVisibleApps, root.maxVisibleRunningApps);
|
|
root.overflowItemCount = overflow.count;
|
|
let finalItems = overflow.items;
|
|
if (root.options.showTrash) {
|
|
finalItems.push({
|
|
uniqueKey: "trash_button",
|
|
type: "trash",
|
|
appId: "__TRASH__",
|
|
toplevel: null,
|
|
isPinned: false,
|
|
isRunning: false,
|
|
isInOverflow: false
|
|
});
|
|
}
|
|
dockItems = finalItems;
|
|
}
|
|
|
|
delegate: ApplicationItem {
|
|
strip: root
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
DTooltip {
|
|
id: tooltip
|
|
screen: root.dockScreen
|
|
}
|
|
onHoveredButtonChanged: {
|
|
tooltip.hide();
|
|
if (!barHosted || !hoveredButton || !surfaceContext.live)
|
|
return;
|
|
const position = surfaceContext.screenPoint(hoveredButton, hoveredButton.width / 2, hoveredButton.height / 2);
|
|
if (!position)
|
|
return;
|
|
const edge = options.position;
|
|
const bounds = SettingsData.getBarBounds(dockScreen, surfaceContext.thickness, edge, surfaceContext.config);
|
|
const x = isVertical ? (edge === SettingsData.Position.Left ? bounds.x + bounds.width : bounds.x) : position.x;
|
|
const y = isVertical ? position.y : (edge === SettingsData.Position.Top ? bounds.y + bounds.height + Theme.spacingS : bounds.y - Theme.listItemHeight);
|
|
tooltip.show(hoveredButton.tooltipText, x, y, dockScreen, isVertical && edge === SettingsData.Position.Left, isVertical && edge === SettingsData.Position.Right);
|
|
}
|
|
onVisibleChanged: if (!visible)
|
|
tooltip.hide()
|
|
|
|
readonly property var settingsAppIdSubstitutions: SettingsData.appIdSubstitutions
|
|
|
|
onSettingsAppIdSubstitutionsChanged: modelUpdate.schedule()
|
|
|
|
onPinnedAppsChanged: modelUpdate.schedule()
|
|
readonly property string modelOptions: JSON.stringify({
|
|
separatePinnedAndRunningApps: options.separatePinnedAndRunningApps,
|
|
isolateDisplays: options.isolateDisplays,
|
|
launcherEnabled: options.launcherEnabled,
|
|
showTrash: options.showTrash
|
|
})
|
|
onModelOptionsChanged: modelUpdate.schedule()
|
|
onDockScreenChanged: modelUpdate.schedule()
|
|
readonly property var visibleWindows: options.currentWorkspace && dockScreen ? CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, dockScreen.name) : CompositorService.sortedToplevels
|
|
onVisibleWindowsChanged: modelUpdate.schedule()
|
|
readonly property string windowMetadata: JSON.stringify(visibleWindows.map(toplevel => [toplevel.appId, ["org.quickshell", "com.danklinux.dms"].includes(toplevel.appId) ? toplevel.title : "", options.isolateDisplays ? (toplevel.screens || []).map(screen => screen.name) : null]))
|
|
onWindowMetadataChanged: modelUpdate.schedule()
|
|
readonly property var searchCoreApps: AppSearchService.coreApps
|
|
|
|
onSearchCoreAppsChanged: modelUpdate.schedule()
|
|
onGroupByAppChanged: modelUpdate.schedule()
|
|
onMaxVisibleAppsChanged: modelUpdate.schedule()
|
|
onMaxVisibleRunningAppsChanged: modelUpdate.schedule()
|
|
DeferredAction {
|
|
id: modelUpdate
|
|
onTriggered: {
|
|
if (root.draggedIndex >= 0)
|
|
return;
|
|
repeater.updateModel();
|
|
}
|
|
}
|
|
}
|