add noctalia and fuzzel
This commit is contained in:
@@ -0,0 +1,951 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Loader {
|
||||
|
||||
active: Settings.data.dock.enabled
|
||||
sourceComponent: Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
delegate: Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen modelData
|
||||
|
||||
property bool barIsReady: modelData ? BarService.isBarReady(modelData.name) : false
|
||||
|
||||
Connections {
|
||||
target: BarService
|
||||
function onBarReadyChanged(screenName) {
|
||||
if (screenName === modelData.name) {
|
||||
barIsReady = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update dock apps when window list change
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onWindowListChanged() {
|
||||
updateDockApps();
|
||||
}
|
||||
}
|
||||
|
||||
// Update dock apps when toplevels change
|
||||
Connections {
|
||||
target: ToplevelManager ? ToplevelManager.toplevels : null
|
||||
function onValuesChanged() {
|
||||
updateDockApps();
|
||||
}
|
||||
}
|
||||
|
||||
// Update dock apps when pinned apps change
|
||||
Connections {
|
||||
target: Settings.data.dock
|
||||
function onPinnedAppsChanged() {
|
||||
updateDockApps();
|
||||
}
|
||||
function onOnlySameOutputChanged() {
|
||||
updateDockApps();
|
||||
}
|
||||
function onGroupAppsChanged() {
|
||||
updateDockApps();
|
||||
}
|
||||
}
|
||||
|
||||
// Initial update when component is ready
|
||||
Component.onCompleted: {
|
||||
if (ToplevelManager) {
|
||||
updateDockApps();
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh icons and names when DesktopEntries becomes available (or updates)
|
||||
Connections {
|
||||
target: DesktopEntries.applications
|
||||
function onValuesChanged() {
|
||||
root.iconRevision++;
|
||||
root._desktopEntryIdCache = {};
|
||||
updateDockApps();
|
||||
}
|
||||
}
|
||||
|
||||
// Shared properties between peek and dock windows
|
||||
readonly property string displayMode: Settings.data.dock.displayMode
|
||||
readonly property bool autoHide: displayMode === "auto_hide"
|
||||
readonly property bool exclusive: displayMode === "exclusive"
|
||||
readonly property bool isAttachedMode: Settings.data.dock.dockType === "attached"
|
||||
readonly property int hideDelay: 500
|
||||
readonly property int showDelay: 100
|
||||
readonly property int hideAnimationDuration: Math.max(0, Math.round(Style.animationFast / (Settings.data.dock.animationSpeed || 1.0)))
|
||||
readonly property int showAnimationDuration: Math.max(0, Math.round(Style.animationFast / (Settings.data.dock.animationSpeed || 1.0)))
|
||||
readonly property int peekThickness: 1
|
||||
readonly property int indicatorThickness: Settings.data.dock.indicatorThickness || 3
|
||||
readonly property string indicatorColorKey: Settings.data.dock.indicatorColor || "primary"
|
||||
readonly property real indicatorOpacity: Settings.data.dock.indicatorOpacity !== undefined ? Settings.data.dock.indicatorOpacity : 0.6
|
||||
readonly property int iconSize: Math.round(12 + 24 * (Settings.data.dock.size ?? 1))
|
||||
readonly property int floatingMargin: Settings.data.dock.floatingRatio * Style.marginL
|
||||
readonly property int maxWidth: modelData ? modelData.width * 0.8 : 1000
|
||||
readonly property int maxHeight: modelData ? modelData.height * 0.8 : 1000
|
||||
|
||||
// Dock position properties
|
||||
readonly property string dockPosition: Settings.data.dock.position
|
||||
readonly property bool isVertical: dockPosition === "left" || dockPosition === "right"
|
||||
|
||||
// Bar detection and positioning properties
|
||||
readonly property bool hasBar: modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false
|
||||
readonly property bool barAtSameEdge: hasBar && Settings.getBarPositionForScreen(modelData?.name) === dockPosition
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(modelData?.name)
|
||||
readonly property bool barIsVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property bool barIsFramed: Settings.data.bar.barType === "framed" && hasBar
|
||||
readonly property bool barFloating: Settings.data.bar.barType === "floating"
|
||||
readonly property real barMarginH: barFloating ? Math.ceil(Settings.data.bar.marginHorizontal) : 0
|
||||
readonly property real barMarginV: barFloating ? Math.ceil(Settings.data.bar.marginVertical) : 0
|
||||
readonly property int barHeight: Style.getBarHeightForScreen(modelData?.name)
|
||||
readonly property bool staticPanelOpen: {
|
||||
if (!isAttachedMode)
|
||||
return false;
|
||||
var panel = getStaticDockPanel();
|
||||
if (panel && panel.isPanelOpen !== undefined)
|
||||
return panel.isPanelOpen;
|
||||
return false;
|
||||
}
|
||||
readonly property int peekEdgeLength: {
|
||||
const edgeSize = isVertical ? Math.round(modelData?.height || maxHeight) : Math.round(modelData?.width || maxWidth);
|
||||
const minLength = Math.max(1, Math.round(edgeSize * (Settings.data.dock.showDockIndicator ? 0.1 : 0.25)));
|
||||
return Math.max(minLength, dockIndicatorLength);
|
||||
}
|
||||
readonly property int peekCenterOffsetX: {
|
||||
if (isVertical)
|
||||
return 0;
|
||||
const edgeSize = Math.round(modelData?.width || maxWidth);
|
||||
if (barIsVertical) {
|
||||
if (barPosition === "left") {
|
||||
const availableStart = (barIsFramed ? 0 : barMarginH) + barHeight;
|
||||
const availableWidth = edgeSize - availableStart - (barIsFramed ? Settings.data.bar.frameThickness : 0);
|
||||
return Math.max(0, Math.round(availableStart + (availableWidth - peekEdgeLength) / 2));
|
||||
}
|
||||
if (barPosition === "right") {
|
||||
const availableWidth = edgeSize - (barIsFramed ? 0 : barMarginH) - barHeight - (barIsFramed ? Settings.data.bar.frameThickness : 0);
|
||||
return Math.max(0, Math.round((barIsFramed ? Settings.data.bar.frameThickness : 0) + (availableWidth - peekEdgeLength) / 2));
|
||||
}
|
||||
}
|
||||
return Math.max(0, Math.round((edgeSize - peekEdgeLength) / 2));
|
||||
}
|
||||
readonly property int peekCenterOffsetY: {
|
||||
if (!isVertical)
|
||||
return 0;
|
||||
const edgeSize = Math.round(modelData?.height || maxHeight);
|
||||
if (!barIsVertical) {
|
||||
if (barPosition === "top") {
|
||||
const availableStart = (barIsFramed ? 0 : barMarginV) + barHeight;
|
||||
const availableHeight = edgeSize - availableStart - (barIsFramed ? Settings.data.bar.frameThickness : 0);
|
||||
return Math.max(0, Math.round(availableStart + (availableHeight - peekEdgeLength) / 2));
|
||||
}
|
||||
if (barPosition === "bottom") {
|
||||
const availableHeight = edgeSize - (barIsFramed ? 0 : barMarginV) - barHeight - (barIsFramed ? Settings.data.bar.frameThickness : 0);
|
||||
return Math.max(0, Math.round((barIsFramed ? Settings.data.bar.frameThickness : 0) + (availableHeight - peekEdgeLength) / 2));
|
||||
}
|
||||
}
|
||||
return Math.max(0, Math.round((edgeSize - peekEdgeLength) / 2));
|
||||
}
|
||||
readonly property bool showDockIndicator: {
|
||||
if (!Settings.data.dock.showDockIndicator || (!autoHide && !isAttachedMode) || !hidden)
|
||||
return false;
|
||||
return !staticPanelOpen;
|
||||
}
|
||||
readonly property int dockItemCount: dockApps.length + (Settings.data.dock.showLauncherIcon ? 1 : 0)
|
||||
readonly property bool indicatorVisible: showDockIndicator && dockIndicatorLength > 0
|
||||
readonly property int dockIndicatorLength: {
|
||||
if (dockItemCount <= 0)
|
||||
return 0;
|
||||
const spacing = Style.marginS;
|
||||
const layoutLength = (iconSize * dockItemCount) + (spacing * Math.max(0, dockItemCount - 1));
|
||||
const padded = layoutLength + Style.marginXL;
|
||||
return Math.min(padded, isVertical ? maxHeight : maxWidth);
|
||||
}
|
||||
|
||||
// Shared state between windows
|
||||
property bool dockHovered: false
|
||||
property bool anyAppHovered: false
|
||||
property bool menuHovered: false
|
||||
property bool hidden: autoHide
|
||||
property bool peekHovered: false
|
||||
|
||||
// Separate property to control Loader - stays true during animations
|
||||
property bool dockLoaded: !autoHide // Start loaded if autoHide is off
|
||||
|
||||
// Track the currently open context menu
|
||||
property var currentContextMenu: null
|
||||
|
||||
// Combined model of running apps and pinned apps
|
||||
property var dockApps: []
|
||||
property var groupCycleIndices: ({})
|
||||
|
||||
// Track the session order of apps (transient reordering)
|
||||
property var sessionAppOrder: []
|
||||
|
||||
// Drag and Drop state for visual feedback
|
||||
property int dragSourceIndex: -1
|
||||
property int dragTargetIndex: -1
|
||||
|
||||
// when dragging ended but the cursor is outside the dock area, restart the timer
|
||||
onDragSourceIndexChanged: {
|
||||
if (dragSourceIndex === -1) {
|
||||
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) {
|
||||
hideTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Revision counter to force icon re-evaluation
|
||||
property int iconRevision: 0
|
||||
|
||||
// Function to close any open context menu
|
||||
function closeAllContextMenus() {
|
||||
if (currentContextMenu && currentContextMenu.visible) {
|
||||
currentContextMenu.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function getStaticDockPanel() {
|
||||
return PanelService.getPanel("staticDockPanel", modelData, false);
|
||||
}
|
||||
|
||||
function getAppKey(appData) {
|
||||
if (!appData)
|
||||
return null;
|
||||
|
||||
if (Settings.data.dock.groupApps) {
|
||||
return appData.appId;
|
||||
}
|
||||
|
||||
// Use stable appId for pinned apps to maintain their slot regardless of running state
|
||||
if (appData.type === "pinned" || appData.type === "pinned-running") {
|
||||
return appData.appId;
|
||||
}
|
||||
|
||||
// prefer toplevel object identity for unpinned running apps to distinguish instances
|
||||
if (appData.toplevel)
|
||||
return appData.toplevel;
|
||||
|
||||
// fallback to appId
|
||||
return appData.appId;
|
||||
}
|
||||
|
||||
function sortDockApps(apps) {
|
||||
if (!sessionAppOrder || sessionAppOrder.length === 0) {
|
||||
return apps;
|
||||
}
|
||||
|
||||
const sorted = [];
|
||||
const remaining = [...apps];
|
||||
|
||||
// Pick apps that are in the session order
|
||||
for (let i = 0; i < sessionAppOrder.length; i++) {
|
||||
const key = sessionAppOrder[i];
|
||||
|
||||
// Pick ALL matching apps (e.g. all instances of a pinned app)
|
||||
while (true) {
|
||||
const idx = remaining.findIndex(app => getAppKey(app) === key);
|
||||
if (idx !== -1) {
|
||||
sorted.push(remaining[idx]);
|
||||
remaining.splice(idx, 1);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append any new/remaining apps
|
||||
remaining.forEach(app => sorted.push(app));
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
function reorderApps(fromIndex, toIndex) {
|
||||
if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= dockApps.length || toIndex >= dockApps.length)
|
||||
return;
|
||||
|
||||
const list = [...dockApps];
|
||||
const item = list.splice(fromIndex, 1)[0];
|
||||
list.splice(toIndex, 0, item);
|
||||
|
||||
dockApps = list;
|
||||
sessionAppOrder = dockApps.map(getAppKey);
|
||||
savePinnedOrder();
|
||||
}
|
||||
|
||||
function savePinnedOrder() {
|
||||
const currentPinned = Settings.data.dock.pinnedApps || [];
|
||||
const newPinned = [];
|
||||
const seen = new Set();
|
||||
|
||||
// Extract pinned apps in their current visual order
|
||||
dockApps.forEach(app => {
|
||||
if (app.appId && !seen.has(app.appId)) {
|
||||
const isPinned = currentPinned.some(p => normalizeAppId(p) === normalizeAppId(app.appId));
|
||||
|
||||
if (isPinned) {
|
||||
newPinned.push(app.appId);
|
||||
seen.add(app.appId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check if any pinned apps were missed (unlikely if dockApps is correct)
|
||||
currentPinned.forEach(p => {
|
||||
if (!seen.has(p)) {
|
||||
newPinned.push(p);
|
||||
seen.add(p);
|
||||
}
|
||||
});
|
||||
|
||||
if (JSON.stringify(currentPinned) !== JSON.stringify(newPinned)) {
|
||||
Settings.data.dock.pinnedApps = newPinned;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to normalize app IDs for case-insensitive matching
|
||||
function normalizeAppId(appId) {
|
||||
if (!appId || typeof appId !== 'string')
|
||||
return "";
|
||||
let id = appId.toLowerCase().trim();
|
||||
if (id.endsWith(".desktop"))
|
||||
id = id.substring(0, id.length - 8);
|
||||
return id;
|
||||
}
|
||||
|
||||
// Helper function to check if an app ID matches a pinned app (case-insensitive)
|
||||
function isAppIdPinned(appId, pinnedApps) {
|
||||
if (!appId || !pinnedApps || pinnedApps.length === 0)
|
||||
return false;
|
||||
const normalizedId = normalizeAppId(appId);
|
||||
// Direct match
|
||||
if (pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedId))
|
||||
return true;
|
||||
// Resolve via desktop entry lookup (handles StartupWMClass != .desktop filename)
|
||||
const resolved = resolveToDesktopEntryId(appId);
|
||||
if (resolved !== appId) {
|
||||
const normalizedResolved = normalizeAppId(resolved);
|
||||
return pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedResolved);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Desktop entry ID resolution cache (cleared when DesktopEntries change)
|
||||
property var _desktopEntryIdCache: ({})
|
||||
|
||||
// Resolve a toplevel appId to its canonical .desktop entry ID via heuristic lookup.
|
||||
// This handles cases where the Wayland appId (e.g. "zen" from StartupWMClass)
|
||||
// differs from the .desktop filename (e.g. "zen-browser-bin").
|
||||
function resolveToDesktopEntryId(appId) {
|
||||
if (!appId)
|
||||
return appId;
|
||||
if (_desktopEntryIdCache.hasOwnProperty(appId))
|
||||
return _desktopEntryIdCache[appId];
|
||||
try {
|
||||
if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) {
|
||||
const entry = DesktopEntries.heuristicLookup(appId);
|
||||
if (entry && entry.id) {
|
||||
_desktopEntryIdCache[appId] = entry.id;
|
||||
return entry.id;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
_desktopEntryIdCache[appId] = appId;
|
||||
return appId;
|
||||
}
|
||||
|
||||
// Helper function to get app name from desktop entry
|
||||
function getAppNameFromDesktopEntry(appId) {
|
||||
if (!appId)
|
||||
return appId;
|
||||
|
||||
try {
|
||||
if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) {
|
||||
const entry = DesktopEntries.heuristicLookup(appId);
|
||||
if (entry && entry.name) {
|
||||
return entry.name;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof DesktopEntries !== 'undefined' && DesktopEntries.byId) {
|
||||
const entry = DesktopEntries.byId(appId);
|
||||
if (entry && entry.name) {
|
||||
return entry.name;
|
||||
}
|
||||
}
|
||||
} catch (e)
|
||||
// Fall through to return original appId
|
||||
{}
|
||||
|
||||
// Return original appId if we can't find a desktop entry
|
||||
return appId;
|
||||
}
|
||||
|
||||
function getToplevelsForEntry(appData) {
|
||||
if (!appData)
|
||||
return [];
|
||||
|
||||
if (appData.toplevels && appData.toplevels.length > 0) {
|
||||
return appData.toplevels.filter(toplevel => toplevel && (!Settings.data.dock.onlySameOutput || !toplevel.screens || toplevel.screens.includes(modelData)));
|
||||
}
|
||||
|
||||
if (!appData.toplevel)
|
||||
return [];
|
||||
|
||||
if (Settings.data.dock.onlySameOutput && appData.toplevel.screens && !appData.toplevel.screens.includes(modelData))
|
||||
return [];
|
||||
|
||||
return [appData.toplevel];
|
||||
}
|
||||
|
||||
function getPrimaryToplevelForEntry(appData) {
|
||||
const toplevels = getToplevelsForEntry(appData);
|
||||
if (toplevels.length === 0)
|
||||
return null;
|
||||
|
||||
if (ToplevelManager && ToplevelManager.activeToplevel && toplevels.includes(ToplevelManager.activeToplevel))
|
||||
return ToplevelManager.activeToplevel;
|
||||
|
||||
return toplevels[0];
|
||||
}
|
||||
|
||||
// Build grouped render model without mutating the raw toplevel list.
|
||||
function buildGroupedDockApps(apps) {
|
||||
if (!Settings.data.dock.groupApps) {
|
||||
return apps.map(app => {
|
||||
const entry = Object.assign({}, app);
|
||||
entry.toplevels = getToplevelsForEntry(app);
|
||||
return entry;
|
||||
});
|
||||
}
|
||||
|
||||
const grouped = [];
|
||||
const groupedById = new Map();
|
||||
|
||||
apps.forEach(app => {
|
||||
const appId = app.appId;
|
||||
const toplevels = getToplevelsForEntry(app);
|
||||
const existing = groupedById.get(appId);
|
||||
|
||||
if (existing) {
|
||||
toplevels.forEach(toplevel => {
|
||||
if (!existing.toplevels.includes(toplevel)) {
|
||||
existing.toplevels.push(toplevel);
|
||||
}
|
||||
});
|
||||
if (app.type === "pinned" || app.type === "pinned-running") {
|
||||
existing.isPinned = true;
|
||||
}
|
||||
} else {
|
||||
const entry = {
|
||||
"type": app.type,
|
||||
"appId": appId,
|
||||
"title": app.title,
|
||||
"toplevels": toplevels.slice(),
|
||||
"isPinned": app.type === "pinned" || app.type === "pinned-running"
|
||||
};
|
||||
grouped.push(entry);
|
||||
groupedById.set(appId, entry);
|
||||
}
|
||||
});
|
||||
|
||||
grouped.forEach(entry => {
|
||||
entry.toplevel = getPrimaryToplevelForEntry(entry);
|
||||
if (entry.toplevels.length > 0 && entry.isPinned) {
|
||||
entry.type = "pinned-running";
|
||||
} else if (entry.toplevels.length > 0) {
|
||||
entry.type = "running";
|
||||
} else {
|
||||
entry.type = "pinned";
|
||||
}
|
||||
if (entry.toplevel && entry.toplevel.title && entry.toplevel.title.trim() !== "") {
|
||||
entry.title = entry.toplevel.title;
|
||||
}
|
||||
});
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
// Function to update the combined dock apps model
|
||||
function updateDockApps() {
|
||||
const runningApps = ToplevelManager ? (ToplevelManager.toplevels.values || []) : [];
|
||||
const pinnedApps = Settings.data.dock.pinnedApps || [];
|
||||
const combined = [];
|
||||
const processedToplevels = new Set();
|
||||
const processedPinnedAppIds = new Set();
|
||||
|
||||
//push an app onto combined with the given appType
|
||||
function pushApp(appType, toplevel, appId, title) {
|
||||
// Use canonical ID for pinned apps to ensure key stability
|
||||
const canonicalId = isAppIdPinned(appId, pinnedApps) ? (pinnedApps.find(p => normalizeAppId(p) === normalizeAppId(appId)) || appId) : appId;
|
||||
|
||||
// For running apps, track by toplevel object to allow multiple instances
|
||||
if (toplevel) {
|
||||
if (processedToplevels.has(toplevel)) {
|
||||
return; // Already processed this toplevel instance
|
||||
}
|
||||
if (Settings.data.dock.onlySameOutput && toplevel.screens && !toplevel.screens.includes(modelData)) {
|
||||
return; // Filtered out by onlySameOutput setting
|
||||
}
|
||||
combined.push({
|
||||
"type": appType,
|
||||
"toplevel": toplevel,
|
||||
"toplevels": toplevel ? [toplevel] : [],
|
||||
"appId": canonicalId,
|
||||
"title": title
|
||||
});
|
||||
processedToplevels.add(toplevel);
|
||||
} else {
|
||||
// For pinned apps that aren't running, track by appId to avoid duplicates
|
||||
if (processedPinnedAppIds.has(canonicalId)) {
|
||||
return; // Already processed this pinned app
|
||||
}
|
||||
combined.push({
|
||||
"type": appType,
|
||||
"toplevel": toplevel,
|
||||
"toplevels": [],
|
||||
"appId": canonicalId,
|
||||
"title": title
|
||||
});
|
||||
processedPinnedAppIds.add(canonicalId);
|
||||
}
|
||||
}
|
||||
|
||||
function pushRunning(first) {
|
||||
runningApps.forEach(toplevel => {
|
||||
if (toplevel) {
|
||||
// Use robust matching to check if pinned
|
||||
const isPinned = isAppIdPinned(toplevel.appId, pinnedApps);
|
||||
if (!first && isPinned && processedToplevels.has(toplevel)) {
|
||||
return; // Already added by pushPinned()
|
||||
}
|
||||
pushApp((first && isPinned) ? "pinned-running" : "running", toplevel, toplevel.appId, toplevel.title);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function pushPinned() {
|
||||
pinnedApps.forEach(pinnedAppId => {
|
||||
// Find all running instances of this pinned app using robust matching
|
||||
// Also resolve toplevel appId via desktop entry lookup to handle
|
||||
// StartupWMClass != .desktop filename (e.g. zen -> zen-browser-bin)
|
||||
const normalizedPinned = normalizeAppId(pinnedAppId);
|
||||
const matchingToplevels = runningApps.filter(app => {
|
||||
if (!app)
|
||||
return false;
|
||||
if (normalizeAppId(app.appId) === normalizedPinned)
|
||||
return true;
|
||||
const resolved = resolveToDesktopEntryId(app.appId);
|
||||
return resolved !== app.appId && normalizeAppId(resolved) === normalizedPinned;
|
||||
});
|
||||
|
||||
if (matchingToplevels.length > 0) {
|
||||
// Add all running instances as pinned-running
|
||||
matchingToplevels.forEach(toplevel => {
|
||||
pushApp("pinned-running", toplevel, pinnedAppId, toplevel.title);
|
||||
});
|
||||
} else {
|
||||
// App is pinned but not running - add once
|
||||
pushApp("pinned", null, pinnedAppId, getAppNameFromDesktopEntry(pinnedAppId) || pinnedAppId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//if pinnedStatic then push all pinned and then all remaining running apps
|
||||
if (Settings.data.dock.pinnedStatic) {
|
||||
pushPinned();
|
||||
pushRunning(false);
|
||||
|
||||
//else add all running apps and then remaining pinned apps
|
||||
} else {
|
||||
pushRunning(true);
|
||||
pushPinned();
|
||||
}
|
||||
|
||||
const sortedApps = sortDockApps(combined);
|
||||
dockApps = buildGroupedDockApps(sortedApps);
|
||||
const cycleState = root.groupCycleIndices || {};
|
||||
const nextCycleState = {};
|
||||
dockApps.forEach(app => {
|
||||
if (app && app.appId && cycleState[app.appId] !== undefined) {
|
||||
nextCycleState[app.appId] = cycleState[app.appId];
|
||||
}
|
||||
});
|
||||
root.groupCycleIndices = nextCycleState;
|
||||
|
||||
// Sync session order if needed
|
||||
// Instead of resetting everything when length changes, we reconcile the keys
|
||||
if (!sessionAppOrder || sessionAppOrder.length === 0) {
|
||||
sessionAppOrder = dockApps.map(getAppKey);
|
||||
} else {
|
||||
const currentKeys = new Set(dockApps.map(getAppKey));
|
||||
const existingKeys = new Set();
|
||||
const newOrder = [];
|
||||
|
||||
// Keep existing keys that are still present
|
||||
sessionAppOrder.forEach(key => {
|
||||
if (currentKeys.has(key)) {
|
||||
newOrder.push(key);
|
||||
existingKeys.add(key);
|
||||
}
|
||||
});
|
||||
|
||||
// Add new keys at the end
|
||||
dockApps.forEach(app => {
|
||||
const key = getAppKey(app);
|
||||
if (!existingKeys.has(key)) {
|
||||
newOrder.push(key);
|
||||
existingKeys.add(key);
|
||||
}
|
||||
});
|
||||
|
||||
if (JSON.stringify(newOrder) !== JSON.stringify(sessionAppOrder)) {
|
||||
sessionAppOrder = newOrder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timer to unload dock after hide animation completes
|
||||
Timer {
|
||||
id: unloadTimer
|
||||
interval: hideAnimationDuration + 50 // Add small buffer
|
||||
onTriggered: {
|
||||
if (hidden && autoHide) {
|
||||
dockLoaded = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property alias hideTimer: hideTimer
|
||||
property alias showTimer: showTimer
|
||||
property alias unloadTimer: unloadTimer
|
||||
|
||||
// Timer for auto-hide delay
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: hideDelay
|
||||
onTriggered: {
|
||||
// do not hide if dragging
|
||||
if (root.dragSourceIndex !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Force menuHovered to false if no menu is current or visible
|
||||
if (!root.currentContextMenu || !root.currentContextMenu.visible) {
|
||||
menuHovered = false;
|
||||
}
|
||||
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) {
|
||||
if (isAttachedMode) {
|
||||
const panel = getStaticDockPanel();
|
||||
if (panel && (panel.menuHovered || (panel.currentContextMenu && panel.currentContextMenu.visible))) {
|
||||
restart();
|
||||
return;
|
||||
}
|
||||
if (panel && (panel.isDockHovered || panel.dockHovered || panel.anyAppHovered)) {
|
||||
restart();
|
||||
return;
|
||||
}
|
||||
if (panel)
|
||||
panel.close();
|
||||
} else {
|
||||
closeAllContextMenus();
|
||||
}
|
||||
hidden = true;
|
||||
unloadTimer.restart(); // Start unload timer when hiding
|
||||
} else if (autoHide && !dockHovered && !peekHovered) {
|
||||
// Restart timer if menu is closing (handles race condition)
|
||||
restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timer for show delay
|
||||
Timer {
|
||||
id: showTimer
|
||||
interval: showDelay
|
||||
onTriggered: {
|
||||
if (autoHide) {
|
||||
if (!isAttachedMode) {
|
||||
dockLoaded = true; // Load dock immediately
|
||||
}
|
||||
hidden = false; // Then trigger show animation
|
||||
unloadTimer.stop(); // Cancel any pending unload
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for autoHide setting changes
|
||||
onAutoHideChanged: {
|
||||
if (!autoHide) {
|
||||
hidden = false;
|
||||
dockLoaded = true;
|
||||
hideTimer.stop();
|
||||
showTimer.stop();
|
||||
unloadTimer.stop();
|
||||
} else {
|
||||
hidden = true;
|
||||
unloadTimer.restart(); // Schedule unload after animation
|
||||
}
|
||||
}
|
||||
|
||||
// PEEK WINDOW — only needed when dock can auto-hide or is in attached mode
|
||||
Loader {
|
||||
active: (autoHide || isAttachedMode) && (barIsReady || !hasBar) && modelData && (Settings.data.dock.monitors.length === 0 || Settings.data.dock.monitors.includes(modelData.name))
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
id: peekWindow
|
||||
|
||||
screen: modelData
|
||||
// Dynamic anchors based on dock position
|
||||
anchors.top: dockPosition === "top" || isVertical
|
||||
anchors.bottom: dockPosition === "bottom"
|
||||
anchors.left: dockPosition === "left" || !isVertical
|
||||
anchors.right: dockPosition === "right"
|
||||
focusable: false
|
||||
color: "transparent"
|
||||
|
||||
margins.top: peekCenterOffsetY
|
||||
margins.left: peekCenterOffsetX
|
||||
|
||||
WlrLayershell.namespace: "noctalia-dock-peek-" + (screen?.name || "unknown")
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
// Larger peek area when bar is at same edge, normal 1px otherwise
|
||||
implicitHeight: isVertical ? peekEdgeLength : peekThickness
|
||||
implicitWidth: isVertical ? peekThickness : peekEdgeLength
|
||||
|
||||
MouseArea {
|
||||
id: peekArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onEntered: {
|
||||
peekHovered = true;
|
||||
if (isAttachedMode) {
|
||||
if (dockItemCount <= 0)
|
||||
return;
|
||||
const panel = getStaticDockPanel();
|
||||
if (panel && !panel.isPanelOpen)
|
||||
panel.open();
|
||||
return;
|
||||
}
|
||||
if (hidden) {
|
||||
showTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
peekHovered = false;
|
||||
showTimer.stop();
|
||||
if (isAttachedMode) {
|
||||
// Start hideTimer which checks panel.isDockHovered before closing
|
||||
if (!dockHovered && !anyAppHovered && !menuHovered) {
|
||||
hideTimer.restart();
|
||||
}
|
||||
} else if (!hidden && !dockHovered && !anyAppHovered && !menuHovered) {
|
||||
hideTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DOCK INDICATOR WINDOW — only needed when dock can auto-hide/attach and indicator is enabled
|
||||
Loader {
|
||||
active: (autoHide || isAttachedMode) && Settings.data.dock.showDockIndicator && (barIsReady || !hasBar) && modelData && (Settings.data.dock.monitors.length === 0 || Settings.data.dock.monitors.includes(modelData.name))
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
id: dockIndicatorWindow
|
||||
|
||||
screen: modelData
|
||||
// Dynamic anchors based on dock position
|
||||
anchors.top: dockPosition === "top" || isVertical
|
||||
anchors.bottom: dockPosition === "bottom"
|
||||
anchors.left: dockPosition === "left" || !isVertical
|
||||
anchors.right: dockPosition === "right"
|
||||
focusable: false
|
||||
color: "transparent"
|
||||
|
||||
margins.top: peekCenterOffsetY
|
||||
margins.left: peekCenterOffsetX
|
||||
|
||||
WlrLayershell.namespace: "noctalia-dock-indicator-" + (screen?.name || "unknown")
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
implicitHeight: isVertical ? peekEdgeLength : indicatorThickness
|
||||
implicitWidth: isVertical ? indicatorThickness : peekEdgeLength
|
||||
|
||||
// Hide the window surface when indicator is not visible, so the compositor
|
||||
// can skip compositing this layer-shell surface entirely (saves GPU on NVIDIA)
|
||||
visible: indicatorRect.opacity > 0 || indicatorVisible
|
||||
|
||||
Rectangle {
|
||||
id: indicatorRect
|
||||
anchors.fill: parent
|
||||
radius: indicatorThickness
|
||||
color: Qt.alpha(Color.resolveColorKey(indicatorColorKey), indicatorOpacity)
|
||||
opacity: indicatorVisible ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Force dock reload when position changes to fix anchor/layout issues
|
||||
// Force dock reload when position/mode changes to fix anchor/layout issues
|
||||
property bool _reloading: false
|
||||
function handleReload() {
|
||||
if (!autoHide && dockLoaded && !_reloading) {
|
||||
_reloading = true;
|
||||
// Brief unload/reload cycle to reset layout
|
||||
Qt.callLater(() => {
|
||||
dockLoaded = false;
|
||||
Qt.callLater(() => {
|
||||
dockLoaded = true;
|
||||
_reloading = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onDockPositionChanged: handleReload()
|
||||
onExclusiveChanged: handleReload()
|
||||
|
||||
Loader {
|
||||
id: dockWindowLoader
|
||||
active: Settings.data.dock.enabled && !isAttachedMode && (barIsReady || !hasBar) && modelData && (Settings.data.dock.monitors.length === 0 || Settings.data.dock.monitors.includes(modelData.name)) && dockLoaded && ToplevelManager && (dockApps.length > 0)
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
id: dockWindow
|
||||
|
||||
screen: modelData
|
||||
|
||||
focusable: false
|
||||
color: "transparent"
|
||||
|
||||
WlrLayershell.namespace: "noctalia-dock-" + (screen?.name || "unknown")
|
||||
WlrLayershell.exclusionMode: exclusive ? ExclusionMode.Auto : ExclusionMode.Ignore
|
||||
|
||||
// Slide animation: content slides inside a fixed window, no margin animation.
|
||||
// Only reserve extra space for sliding when auto-hide is enabled
|
||||
property int slideDistance: autoHide ? ((isVertical ? dockContainerWrapper.contentWidth : dockContainerWrapper.contentHeight) + floatingMargin + 10) : 0
|
||||
property real slideOffset: hidden ? slideDistance : 0
|
||||
|
||||
Behavior on slideOffset {
|
||||
NumberAnimation {
|
||||
duration: hidden ? hideAnimationDuration : showAnimationDuration
|
||||
easing.type: hidden ? Easing.InCubic : Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Signed slide: positive pushes content toward its edge (off-screen)
|
||||
readonly property real slideX: dockPosition === "left" ? -slideOffset : dockPosition === "right" ? slideOffset : 0
|
||||
readonly property real slideY: dockPosition === "top" ? -slideOffset : dockPosition === "bottom" ? slideOffset : 0
|
||||
|
||||
// Blur behind dock — offset by slide so it follows the content
|
||||
BackgroundEffect.blurRegion: Settings.data.general.enableBlurBehind ? dockBlurRegion : null
|
||||
Region {
|
||||
id: dockBlurRegion
|
||||
Region {
|
||||
x: Math.round(dockContainerWrapper.x + dockContent.dockContainer.x + dockWindow.slideX)
|
||||
y: Math.round(dockContainerWrapper.y + dockContent.dockContainer.y + dockWindow.slideY)
|
||||
width: Math.round(dockContent.dockContainer.width)
|
||||
height: Math.round(dockContent.dockContainer.height)
|
||||
radius: Style.radiusL
|
||||
}
|
||||
}
|
||||
|
||||
// Window sized to fit content + slide distance so content can slide off-edge.
|
||||
// When auto-hide is disabled, slideDistance is 0 so the window (and thus
|
||||
// the exclusion zone) matches the dock content size.
|
||||
implicitWidth: dockContainerWrapper.width + (isVertical ? slideDistance : 0)
|
||||
implicitHeight: dockContainerWrapper.height + (!isVertical ? slideDistance : 0)
|
||||
|
||||
// Position based on dock setting
|
||||
anchors.top: dockPosition === "top"
|
||||
anchors.bottom: dockPosition === "bottom"
|
||||
anchors.left: dockPosition === "left"
|
||||
anchors.right: dockPosition === "right"
|
||||
|
||||
// Static margins — no animation, window stays put
|
||||
margins.top: dockPosition === "top" ? (barAtSameEdge && !exclusive ? barHeight + (barFloating ? Settings.data.bar.marginVertical : 0) + floatingMargin : floatingMargin) : 0
|
||||
margins.bottom: dockPosition === "bottom" ? (barAtSameEdge && !exclusive ? barHeight + (barFloating ? Settings.data.bar.marginVertical : 0) + floatingMargin : floatingMargin) : 0
|
||||
margins.left: dockPosition === "left" ? (barAtSameEdge && !exclusive ? barHeight + (barFloating ? Settings.data.bar.marginHorizontal : 0) + floatingMargin : floatingMargin) : 0
|
||||
margins.right: dockPosition === "right" ? (barAtSameEdge && !exclusive ? barHeight + (barFloating ? Settings.data.bar.marginHorizontal : 0) + floatingMargin : floatingMargin) : 0
|
||||
|
||||
// Container wrapper for animations
|
||||
Item {
|
||||
id: dockContainerWrapper
|
||||
|
||||
// Helper properties for orthogonal bar detection
|
||||
readonly property string screenBarPosition: Settings.getBarPositionForScreen(modelData?.name)
|
||||
readonly property bool barOnLeft: hasBar && screenBarPosition === "left" && !barFloating
|
||||
readonly property bool barOnRight: hasBar && screenBarPosition === "right" && !barFloating
|
||||
readonly property bool barOnTop: hasBar && screenBarPosition === "top" && !barFloating
|
||||
readonly property bool barOnBottom: hasBar && screenBarPosition === "bottom" && !barFloating
|
||||
|
||||
// Calculate padding needed to shift center to match exclusive mode
|
||||
readonly property int extraTop: (isVertical && !exclusive && barOnTop) ? barHeight : 0
|
||||
readonly property int extraBottom: (isVertical && !exclusive && barOnBottom) ? barHeight : 0
|
||||
readonly property int extraLeft: (!isVertical && !exclusive && barOnLeft) ? barHeight : 0
|
||||
readonly property int extraRight: (!isVertical && !exclusive && barOnRight) ? barHeight : 0
|
||||
|
||||
// Expose content size for window sizing (before slide padding)
|
||||
readonly property int contentWidth: dockContent.dockContainer.width + extraLeft + extraRight + 2
|
||||
readonly property int contentHeight: dockContent.dockContainer.height + extraTop + extraBottom + 2
|
||||
|
||||
// Add +2 buffer for fractional scaling issues
|
||||
width: contentWidth
|
||||
height: contentHeight
|
||||
|
||||
anchors.horizontalCenter: isVertical ? undefined : parent.horizontalCenter
|
||||
anchors.verticalCenter: isVertical ? parent.verticalCenter : undefined
|
||||
|
||||
anchors.top: dockPosition === "top" ? parent.top : undefined
|
||||
anchors.bottom: dockPosition === "bottom" ? parent.bottom : undefined
|
||||
anchors.left: dockPosition === "left" ? parent.left : undefined
|
||||
anchors.right: dockPosition === "right" ? parent.right : undefined
|
||||
|
||||
// Slide content inside the fixed window
|
||||
transform: Translate {
|
||||
x: dockWindow.slideX
|
||||
y: dockWindow.slideY
|
||||
}
|
||||
|
||||
// Enable layer caching to reduce GPU usage from continuous animations
|
||||
layer.enabled: true
|
||||
|
||||
DockContent {
|
||||
id: dockContent
|
||||
anchors.fill: parent
|
||||
dockRoot: root
|
||||
extraTop: dockContainerWrapper.extraTop
|
||||
extraBottom: dockContainerWrapper.extraBottom
|
||||
extraLeft: dockContainerWrapper.extraLeft
|
||||
extraRight: dockContainerWrapper.extraRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user