Files

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();
}
}
}