Watch
1
0
Fork
You've already forked souveraine
0
souveraine/surfaces/quickshell/services/TaskbarApps.qml

214 lines
7.9 KiB
QML

pragma Singleton
import qs.modules.common
import QtQuick
import Quickshell
import Quickshell.Wayland
Singleton {
id: root
function isPinned(appId) {
return Config.options.dock.pinnedApps.indexOf(appId) !== -1;
}
function togglePin(appId) {
if (root.isPinned(appId)) {
Config.options.dock.pinnedApps = Config.options.dock.pinnedApps.filter(id => id !== appId)
} else {
Config.options.dock.pinnedApps = Config.options.dock.pinnedApps.concat([appId])
}
}
// --- Fan-out stacks ------------------------------------------------
// Backing store: Config.options.dock.stacks, a list<string> where each
// entry is "stackId|appId,appId,appId". stackId is a display label and
// the key; members are appIds. Same string-list-in-JsonAdapter shape as
// pinnedApps (nested-object arrays don't survive the adapter).
function parseStack(entry) {
const bar = entry.indexOf("|");
if (bar === -1) return { id: entry, members: [] };
const id = entry.slice(0, bar);
const rest = entry.slice(bar + 1).trim();
const members = rest.length ? rest.split(",").map(s => s.trim()).filter(s => s.length) : [];
return { id: id, members: members };
}
function stacksList() {
return (Config.options?.dock.stacks ?? []).map(root.parseStack);
}
// appId -> the stackId that contains it, or "" if none.
function stackContaining(appId) {
const low = appId.toLowerCase();
for (const s of root.stacksList()) {
if (s.members.some(m => m.toLowerCase() === low)) return s.id;
}
return "";
}
function encodeStack(id, members) {
return id + "|" + members.join(",");
}
// Rewrite the whole stacks list from a parsed [{id, members}] array,
// dropping any that end up empty.
function writeStacks(parsed) {
Config.options.dock.stacks = parsed
.filter(s => s.members.length > 0)
.map(s => root.encodeStack(s.id, s.members));
}
function addToStack(stackId, appId) {
const parsed = root.stacksList();
const existing = parsed.find(s => s.id === stackId);
if (existing) {
if (!existing.members.some(m => m.toLowerCase() === appId.toLowerCase()))
existing.members = existing.members.concat([appId]);
} else {
parsed.push({ id: stackId, members: [appId] });
}
root.writeStacks(parsed);
}
function removeFromStack(stackId, appId) {
const parsed = root.stacksList();
const existing = parsed.find(s => s.id === stackId);
if (!existing) return;
existing.members = existing.members.filter(m => m.toLowerCase() !== appId.toLowerCase());
root.writeStacks(parsed);
}
// --- Drag interactions (Tier 1) ------------------------------------
// Auto-name a fresh stack when two loose icons are combined. "Stack N"
// where N is one past the current count; rename waits for the menu.
function nextStackName() {
return "Stack " + ((Config.options?.dock.stacks?.length ?? 0) + 1);
}
// Drag `draggedAppId` onto `targetAppId` -> combine. If target is
// already a stack (stackId non-empty), add into it; else make a new
// stack containing target then dragged (target stays on top = first).
function combineIntoStack(targetAppId, draggedAppId, targetStackId) {
if (!draggedAppId || draggedAppId.toLowerCase() === targetAppId.toLowerCase()) return;
// If the dragged app is currently in some stack, pull it out first.
const from = root.stackContaining(draggedAppId);
if (from) root.removeFromStack(from, draggedAppId);
if (targetStackId) {
root.addToStack(targetStackId, draggedAppId);
} else {
const id = root.nextStackName();
const parsed = root.stacksList();
parsed.push({ id: id, members: [targetAppId, draggedAppId] });
// targetAppId was a loose pinned app; drop it from standalone
// pinned so it lives only in the stack now.
root.writeStacks(parsed);
}
}
// Reorder a member within its stack by delta (-1 up/left, +1 down/right).
// Member 0 is the collapsed/top icon.
function reorderStackMember(stackId, appId, delta) {
const parsed = root.stacksList();
const s = parsed.find(x => x.id === stackId);
if (!s) return;
const i = s.members.findIndex(m => m.toLowerCase() === appId.toLowerCase());
const j = i + delta;
if (i < 0 || j < 0 || j >= s.members.length) return;
const m = s.members.slice();
[m[i], m[j]] = [m[j], m[i]];
s.members = m;
root.writeStacks(parsed);
}
// Replace a stack's member order wholesale (arc-reorder commit).
function setStackOrder(stackId, members) {
const parsed = root.stacksList();
const s = parsed.find(x => x.id === stackId);
if (!s) return;
s.members = members;
root.writeStacks(parsed);
}
// Pull a member out of its stack back to a standalone pinned app.
function unstackMember(stackId, appId) {
root.removeFromStack(stackId, appId);
if (!root.isPinned(appId)) root.togglePin(appId);
}
property list<var> apps: {
var map = new Map();
// Fan-out stacks come first, in their configured order. Their
// member appIds are suppressed as standalone pinned entries below
// so an app lives in one place: its stack.
const stacks = root.stacksList();
const stackedMembers = new Set();
for (const s of stacks) {
for (const m of s.members) stackedMembers.add(m.toLowerCase());
map.set("STACK:" + s.id, {
pinned: true, toplevels: [], isStack: true, members: s.members
});
}
// Pinned apps (skip any already living in a stack)
const pinnedApps = Config.options?.dock.pinnedApps ?? [];
for (const appId of pinnedApps) {
if (stackedMembers.has(appId.toLowerCase())) continue;
if (!map.has(appId.toLowerCase())) map.set(appId.toLowerCase(), ({
pinned: true,
toplevels: []
}));
}
// Separator
if (map.size > 0) {
map.set("SEPARATOR", { pinned: false, toplevels: [] });
}
// Ignored apps
const ignoredRegexStrings = Config.options?.dock.ignoredAppRegexes ?? [];
const ignoredRegexes = ignoredRegexStrings.map(pattern => new RegExp(pattern, "i"));
// Open windows
for (const toplevel of ToplevelManager.toplevels.values) {
if (ignoredRegexes.some(re => re.test(toplevel.appId))) continue;
// A running app that belongs to a stack keeps its running dot on
// the stack, not as a separate icon.
if (stackedMembers.has(toplevel.appId.toLowerCase())) continue;
if (!map.has(toplevel.appId.toLowerCase())) map.set(toplevel.appId.toLowerCase(), ({
pinned: false,
toplevels: []
}));
map.get(toplevel.appId.toLowerCase()).toplevels.push(toplevel);
}
var values = [];
for (const [key, value] of map) {
values.push(appEntryComp.createObject(null, {
appId: value.isStack ? key.slice(6) : key,
toplevels: value.toplevels,
pinned: value.pinned,
isStack: value.isStack ?? false,
members: value.members ?? []
}));
}
return values;
}
component TaskbarAppEntry: QtObject {
id: wrapper
required property string appId
required property list<var> toplevels
required property bool pinned
property bool isStack: false
property list<var> members: []
}
Component {
id: appEntryComp
TaskbarAppEntry {}
}
}