mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
301 lines
7.7 KiB
QML
301 lines
7.7 KiB
QML
import QtQuick
|
||
import Quickshell
|
||
import Quickshell.Io
|
||
import Quickshell.Wayland
|
||
import Quickshell.WindowManager
|
||
import qs.Commons
|
||
|
||
// Generic ext-workspace-v1 + toplevel handling
|
||
Item {
|
||
id: root
|
||
|
||
property ListModel workspaces: ListModel {}
|
||
property var windows: []
|
||
property int focusedWindowIndex: -1
|
||
property var trackedToplevels: new Set()
|
||
|
||
property bool globalWorkspaces: false
|
||
|
||
property var nativeWorkspaceMap: ({})
|
||
property var connectedWorkspaces: ({})
|
||
|
||
signal workspaceChanged
|
||
signal activeWindowChanged
|
||
signal windowListChanged
|
||
signal displayScalesChanged
|
||
|
||
function initialize() {
|
||
updateWindows();
|
||
connectWorkspaceSignals();
|
||
syncWorkspaces();
|
||
Logger.i("ExtWorkspaceService", "Service started (generic ext-workspace-v1)");
|
||
}
|
||
|
||
Connections {
|
||
target: WindowManager
|
||
|
||
function onWindowsetsChanged() {
|
||
root.connectWorkspaceSignals();
|
||
Qt.callLater(root.syncWorkspaces);
|
||
}
|
||
|
||
function onWindowsetProjectionsChanged() {
|
||
Qt.callLater(root.syncWorkspaces);
|
||
}
|
||
}
|
||
|
||
Timer {
|
||
interval: 500
|
||
running: true
|
||
repeat: false
|
||
onTriggered: {
|
||
if (WindowManager.windowsets.length > 0) {
|
||
root.connectWorkspaceSignals();
|
||
root.syncWorkspaces();
|
||
}
|
||
}
|
||
}
|
||
|
||
function connectWorkspaceSignals() {
|
||
const nativeWs = WindowManager.windowsets;
|
||
const newConnected = {};
|
||
|
||
for (const ws of nativeWs) {
|
||
const key = ws.id || ws.toString();
|
||
newConnected[key] = true;
|
||
|
||
if (connectedWorkspaces[key])
|
||
continue;
|
||
|
||
ws.activeChanged.connect(() => {
|
||
Qt.callLater(root.syncWorkspaces);
|
||
});
|
||
|
||
ws.urgentChanged.connect(() => {
|
||
Qt.callLater(root.syncWorkspaces);
|
||
});
|
||
|
||
ws.shouldDisplayChanged.connect(() => {
|
||
Qt.callLater(root.syncWorkspaces);
|
||
});
|
||
|
||
ws.nameChanged.connect(() => {
|
||
Qt.callLater(root.syncWorkspaces);
|
||
});
|
||
}
|
||
|
||
connectedWorkspaces = newConnected;
|
||
}
|
||
|
||
function syncWorkspaces() {
|
||
const nativeWs = WindowManager.windowsets;
|
||
|
||
workspaces.clear();
|
||
nativeWorkspaceMap = {};
|
||
|
||
/* Per-output (projection) index: compositors expose one workspace group per
|
||
* monitor with names "1"…"9". A single global idx produced 10–18 on the
|
||
* second head because all windowsets were merged into one list. */
|
||
const perOutputNextIdx = {};
|
||
|
||
for (const ws of nativeWs) {
|
||
if (!ws.shouldDisplay) {
|
||
continue;
|
||
}
|
||
|
||
let outputName = "";
|
||
if (ws.projection) {
|
||
const projScreens = ws.projection.screens;
|
||
if (projScreens && projScreens.length > 0) {
|
||
outputName = projScreens[0].name || "";
|
||
}
|
||
}
|
||
|
||
const groupKey = outputName || "_";
|
||
let idx;
|
||
const numericName = ws.name && /^\d+$/.test(String(ws.name)) ? parseInt(ws.name, 10) : NaN;
|
||
if (!isNaN(numericName) && numericName >= 1) {
|
||
idx = numericName;
|
||
} else {
|
||
if (perOutputNextIdx[groupKey] === undefined) {
|
||
perOutputNextIdx[groupKey] = 1;
|
||
}
|
||
idx = perOutputNextIdx[groupKey]++;
|
||
}
|
||
|
||
const wsEntry = {
|
||
"id": ws.id || idx.toString(),
|
||
"idx": idx,
|
||
"name": ws.name || ("Workspace " + idx),
|
||
"output": outputName,
|
||
"isFocused": ws.active,
|
||
"isActive": true,
|
||
"isUrgent": ws.urgent,
|
||
"isOccupied": false,
|
||
"oid": ws.id || idx.toString()
|
||
};
|
||
|
||
workspaces.append(wsEntry);
|
||
nativeWorkspaceMap[wsEntry.id] = ws;
|
||
}
|
||
|
||
updateWindowWorkspaces();
|
||
workspaceChanged();
|
||
}
|
||
|
||
function updateWindowWorkspaces() {
|
||
let activeId = "";
|
||
for (let i = 0; i < workspaces.count; i++) {
|
||
const ws = workspaces.get(i);
|
||
if (ws.isFocused) {
|
||
activeId = ws.id;
|
||
break;
|
||
}
|
||
}
|
||
|
||
for (let i = 0; i < windows.length; i++) {
|
||
if (activeId) {
|
||
windows[i].workspaceId = activeId;
|
||
}
|
||
}
|
||
windowListChanged();
|
||
}
|
||
|
||
Connections {
|
||
target: ToplevelManager.toplevels
|
||
function onValuesChanged() {
|
||
updateWindows();
|
||
}
|
||
}
|
||
|
||
function connectToToplevel(toplevel) {
|
||
if (!toplevel)
|
||
return;
|
||
|
||
toplevel.activatedChanged.connect(() => {
|
||
Qt.callLater(onToplevelActivationChanged);
|
||
});
|
||
|
||
toplevel.titleChanged.connect(() => {
|
||
Qt.callLater(updateWindows);
|
||
});
|
||
}
|
||
|
||
function onToplevelActivationChanged() {
|
||
updateWindows();
|
||
activeWindowChanged();
|
||
}
|
||
|
||
function updateWindows() {
|
||
const newWindows = [];
|
||
const toplevels = ToplevelManager.toplevels?.values || [];
|
||
|
||
let focusedIdx = -1;
|
||
let idx = 0;
|
||
|
||
let activeId = "";
|
||
for (let i = 0; i < workspaces.count; i++) {
|
||
const ws = workspaces.get(i);
|
||
if (ws.isFocused) {
|
||
activeId = ws.id;
|
||
break;
|
||
}
|
||
}
|
||
|
||
for (const toplevel of toplevels) {
|
||
if (!toplevel)
|
||
continue;
|
||
|
||
if (!trackedToplevels.has(toplevel)) {
|
||
connectToToplevel(toplevel);
|
||
trackedToplevels.add(toplevel);
|
||
}
|
||
|
||
const output = (toplevel.screens && toplevel.screens.length > 0) ? (toplevel.screens[0].name || "") : "";
|
||
|
||
const windowId = (toplevel.appId || "") + ":" + idx;
|
||
|
||
newWindows.push({
|
||
"id": windowId,
|
||
"appId": toplevel.appId || "",
|
||
"title": toplevel.title || "",
|
||
"output": output,
|
||
"workspaceId": activeId || "1",
|
||
"isFocused": toplevel.activated || false,
|
||
"toplevel": toplevel
|
||
});
|
||
|
||
if (toplevel.activated) {
|
||
focusedIdx = idx;
|
||
}
|
||
idx++;
|
||
}
|
||
windows = newWindows;
|
||
focusedWindowIndex = focusedIdx;
|
||
|
||
windowListChanged();
|
||
}
|
||
|
||
function focusWindow(window) {
|
||
if (window.toplevel && typeof window.toplevel.activate === "function") {
|
||
window.toplevel.activate();
|
||
}
|
||
}
|
||
|
||
function closeWindow(window) {
|
||
if (window.toplevel && typeof window.toplevel.close === "function") {
|
||
window.toplevel.close();
|
||
}
|
||
}
|
||
|
||
function switchToWorkspace(workspace) {
|
||
const nativeWs = nativeWorkspaceMap[workspace.id] || nativeWorkspaceMap[workspace.oid];
|
||
if (nativeWs && nativeWs.canActivate) {
|
||
nativeWs.activate();
|
||
} else {
|
||
Logger.w("ExtWorkspaceService", "Cannot activate workspace: " + (workspace.name || workspace.id));
|
||
}
|
||
}
|
||
|
||
function turnOffMonitors() {
|
||
try {
|
||
Quickshell.execDetached(["wlr-randr", "--off"]);
|
||
} catch (e) {
|
||
Logger.e("ExtWorkspaceService", "Failed to turn off monitors:", e);
|
||
}
|
||
}
|
||
|
||
function turnOnMonitors() {
|
||
try {
|
||
Quickshell.execDetached(["wlr-randr", "--on"]);
|
||
} catch (e) {
|
||
Logger.e("ExtWorkspaceService", "Failed to turn on monitors:", e);
|
||
}
|
||
}
|
||
|
||
function logout() {
|
||
const sid = Quickshell.env("XDG_SESSION_ID");
|
||
try {
|
||
if (sid && sid.length > 0) {
|
||
Quickshell.execDetached(["loginctl", "terminate-session", sid]);
|
||
} else {
|
||
Logger.w("ExtWorkspaceService", "logout: XDG_SESSION_ID unset; use session menu custom command or compositor-specific backend");
|
||
}
|
||
} catch (e) {
|
||
Logger.e("ExtWorkspaceService", "Failed to logout:", e);
|
||
}
|
||
}
|
||
|
||
function cycleKeyboardLayout() {
|
||
Logger.w("ExtWorkspaceService", "Keyboard layout cycling not supported");
|
||
}
|
||
|
||
function queryDisplayScales() {
|
||
Logger.w("ExtWorkspaceService", "Display scale queries not supported via ToplevelManager");
|
||
}
|
||
|
||
function getFocusedScreen() {
|
||
return null;
|
||
}
|
||
}
|