diff --git a/.gitignore b/.gitignore index 50b4af4b..e4e69e73 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.tar.zst -pkg/ \ No newline at end of file +pkg/ +.vscode/ \ No newline at end of file diff --git a/src/share/sleex/modules/bar/ActiveWindow.qml b/src/share/sleex/modules/bar/ActiveWindow.qml index 8262dbfb..036123e1 100644 --- a/src/share/sleex/modules/bar/ActiveWindow.qml +++ b/src/share/sleex/modules/bar/ActiveWindow.qml @@ -1,15 +1,16 @@ import qs.modules.common import qs.modules.common.widgets +import qs.services import QtQuick import QtQuick.Layouts import Quickshell.Wayland import Quickshell.Hyprland +import Sleex.Fhtc Item { id: root required property var bar readonly property HyprlandMonitor monitor: Hyprland.monitorFor(bar.screen) - readonly property Toplevel activeWindow: ToplevelManager.activeToplevel implicitWidth: colLayout.implicitWidth @@ -26,7 +27,7 @@ Item { font.pixelSize: Appearance.font.pixelSize.smaller color: Appearance.colors.colSubtext elide: Text.ElideRight - text: root.activeWindow?.activated ? root.activeWindow?.appId : qsTr("Desktop") + text: FhtcWorkspaces.focusedWindow !== undefined ? FhtcWorkspaces.focusedWindow['app-id'] : qsTr("Desktop") } StyledText { @@ -34,7 +35,7 @@ Item { font.pixelSize: Appearance.font.pixelSize.small color: Appearance.colors.colOnLayer0 elide: Text.ElideRight - text: root.activeWindow?.activated ? root.activeWindow?.title : `${qsTr("Workspace")} ${monitor.activeWorkspace?.id}` + text: FhtcWorkspaces.focusedWindow !== undefined ? FhtcWorkspaces.focusedWindow.title : `${qsTr("Workspace")} ${FhtcWorkspaces.activeWorkspaceId + 1}` } } diff --git a/src/share/sleex/modules/bar/Workspaces.qml b/src/share/sleex/modules/bar/Workspaces.qml index 75c0806a..bc923bb8 100644 --- a/src/share/sleex/modules/bar/Workspaces.qml +++ b/src/share/sleex/modules/bar/Workspaces.qml @@ -8,28 +8,41 @@ import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Wayland -import Quickshell.Hyprland import Quickshell.Io import Quickshell.Widgets import Qt5Compat.GraphicalEffects +import Sleex.Fhtc Item { id: root required property var bar - property bool borderless: Config.options.bar.borderless - readonly property HyprlandMonitor monitor: Hyprland.monitorFor(bar.screen) - readonly property Toplevel activeWindow: ToplevelManager.activeToplevel + readonly property string screenName: bar.screen?.name ?? "" + + // Get workspaces for this screen only, sorted by ID + readonly property var screenWorkspaces: { + return Object.values(FhtcWorkspaces.workspaces) + .filter(ws => ws.output === screenName) + .sort((a, b) => a.id - b.id); + } + + // Active workspace index within this screen (0-based) + readonly property int activeWorkspaceIndex: { + if (!FhtcWorkspaces.activeWorkspace) return -1; + if (FhtcWorkspaces.activeWorkspace.output !== screenName) return -1; + // Find the index of the active workspace in our sorted screen workspaces + const idx = screenWorkspaces.findIndex(ws => ws.id === FhtcWorkspaces.activeWorkspace.id); + // Return -1 if the workspace is beyond the shown limit + if (idx >= Config.options.bar.workspaces.shown) return -1; + return idx; + } - readonly property int workspaceGroup: Math.floor((monitor.activeWorkspace?.id - 1) / Config.options.bar.workspaces.shown) property list workspaceOccupied: [] - property int widgetPadding: 0 property int horizontalPadding: 5 property int workspaceButtonWidth: 30 property real workspaceIconSize: workspaceButtonWidth * 0.6 property real workspaceIconSizeShrinked: workspaceButtonWidth * 0.55 property real workspaceIconOpacityShrinked: 1 property real workspaceIconMarginShrinked: -4 - property int workspaceIndexInGroup: (monitor.activeWorkspace?.id - 1) % Config.options.bar.workspaces.shown property bool useMaterialIcons: Config.options.bar.workspaces.useMaterialIcons @@ -37,17 +50,27 @@ Item { // Function to update workspaceOccupied function updateWorkspaceOccupied() { workspaceOccupied = Array.from({ length: Config.options.bar.workspaces.shown }, (_, i) => { - return Hyprland.workspaces.values.some(ws => ws.id === workspaceGroup * Config.options.bar.workspaces.shown + i + 1); + // Get the workspace at this index for this screen + const ws = screenWorkspaces[i]; + if (!ws) return false; + // Check if the workspace has any windows + return ws.windows && ws.windows.length > 0; }) } // Initialize workspaceOccupied when the component is created Component.onCompleted: updateWorkspaceOccupied() - // Listen for changes in Hyprland.workspaces.values + // Listen for changes in Fhtc.workspaces and windows Connections { - target: Hyprland.workspaces - function onValuesChanged() { + target: FhtcWorkspaces + function onWorkspacesChanged() { + updateWorkspaceOccupied(); + } + function onWindowsChanged() { + updateWorkspaceOccupied(); + } + function onActiveWorkspaceChanged() { updateWorkspaceOccupied(); } } @@ -59,24 +82,15 @@ Item { // Scroll to switch workspaces WheelHandler { onWheel: (event) => { - if (event.angleDelta.y < 0) - Hyprland.dispatch(`workspace +1`); - else if (event.angleDelta.y > 0) - Hyprland.dispatch(`workspace -1`); + if (event.angleDelta.y < 0) { + FhtcIpc.dispatch("focus-next-workspace", {}); + } else if (event.angleDelta.y > 0) { + FhtcIpc.dispatch("focus-previous-workspace", {}); + } } acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.BackButton - onPressed: (event) => { - if (event.button === Qt.BackButton) { - Hyprland.dispatch(`togglespecialworkspace`); - } - } - } - Item { anchors.fill: parent anchors.leftMargin: horizontalPadding @@ -99,8 +113,8 @@ Item { implicitWidth: workspaceButtonWidth implicitHeight: workspaceButtonWidth radius: Appearance.rounding.full - property var leftOccupied: (workspaceOccupied[index-1] && !(!activeWindow?.activated && monitor.activeWorkspace?.id === index)) - property var rightOccupied: (workspaceOccupied[index+1] && !(!activeWindow?.activated && monitor.activeWorkspace?.id === index+2)) + property var leftOccupied: (workspaceOccupied[index-1]) + property var rightOccupied: (workspaceOccupied[index+1]) property var radiusLeft: leftOccupied ? 0 : Appearance.rounding.full property var radiusRight: rightOccupied ? 0 : Appearance.rounding.full @@ -110,7 +124,7 @@ Item { bottomRightRadius: radiusRight color: ColorUtils.transparentize(Appearance.m3colors.m3secondaryContainer, 0.4) - opacity: (workspaceOccupied[index] && !(!activeWindow?.activated && monitor.activeWorkspace?.id === index+1)) ? 1 : 0 + opacity: (workspaceOccupied[index]) ? 1 : 0 Behavior on opacity { animation: Appearance.animation.elementMove.numberAnimation.createObject(this) @@ -132,6 +146,7 @@ Item { // Active workspace Rectangle { z: 2 + visible: activeWorkspaceIndex >= 0 // Make active ws indicator, which has a brighter color, smaller to look like it is of the same size as ws occupied highlight property real activeWorkspaceMargin: 2 implicitHeight: workspaceButtonWidth - activeWorkspaceMargin * 2 @@ -139,8 +154,8 @@ Item { color: Appearance.colors.colPrimary anchors.verticalCenter: parent.verticalCenter - property real idx1: workspaceIndexInGroup - property real idx2: workspaceIndexInGroup + property real idx1: activeWorkspaceIndex >= 0 ? activeWorkspaceIndex : 0 + property real idx2: activeWorkspaceIndex >= 0 ? activeWorkspaceIndex : 0 x: Math.min(idx1, idx2) * workspaceButtonWidth + activeWorkspaceMargin implicitWidth: Math.abs(idx1 - idx2) * workspaceButtonWidth + workspaceButtonWidth - activeWorkspaceMargin * 2 @@ -175,29 +190,38 @@ Item { Button { id: button - property int workspaceValue: workspaceGroup * Config.options.bar.workspaces.shown + index + 1 + property var workspace: screenWorkspaces[index] ?? null + property int workspaceId: workspace?.id ?? -1 Layout.fillHeight: true - onPressed: Hyprland.dispatch(`workspace ${workspaceValue}`) + onPressed: { + if (button.workspaceId >= 0) { + FhtcIpc.dispatch("focus-workspace-by-index", { "workspace_idx": button.workspaceId }); + } + } width: workspaceButtonWidth background: Item { id: workspaceButtonBackground implicitWidth: workspaceButtonWidth implicitHeight: workspaceButtonWidth + + // Get the biggest window from the workspace's window list property var biggestWindow: { - const windowsInThisWorkspace = HyprlandData.windowList.filter(w => w.workspace.id == button.workspaceValue) + if (!button.workspace || !button.workspace.windows || button.workspace.windows.length === 0) return null; + const windowIds = button.workspace.windows; + const windowsInThisWorkspace = windowIds.map(id => FhtcWorkspaces.windows[id]).filter(w => w != null); return windowsInThisWorkspace.reduce((maxWin, win) => { const maxArea = (maxWin?.size?.[0] ?? 0) * (maxWin?.size?.[1] ?? 0) const winArea = (win?.size?.[0] ?? 0) * (win?.size?.[1] ?? 0) return winArea > maxArea ? win : maxWin }, null) } - property var mainAppIconSource: Quickshell.iconPath(AppSearch.guessIcon(biggestWindow?.class), "image-missing") + property var mainAppIconSource: Quickshell.iconPath(AppSearch.guessIcon(biggestWindow?.["app-id"]), "image-missing") property string materialIconName: { if (!biggestWindow) return "" - const winClass = biggestWindow.class.toLowerCase() + const winClass = biggestWindow?.["app-id"] const map = { "language": ["firefox", "chromium", "google-chrome", "brave", "edge", "vivaldi", "qutebrowser", "librewolf", "zen-browser"], "terminal": ["foot", "kitty", "alacritty", "wezterm", "gnome-terminal", "konsole", "xfce4-terminal", "xterm"], @@ -230,9 +254,9 @@ Item { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2) - text: `${button.workspaceValue}` + text: `${index + 1}` elide: Text.ElideRight - color: (monitor.activeWorkspace?.id == button.workspaceValue) ? + color: (activeWorkspaceIndex == index) ? Appearance.m3colors.m3onPrimary : (workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer1Inactive) @@ -252,9 +276,9 @@ Item { width: workspaceButtonWidth * 0.18 height: width radius: width / 2 - color: (monitor.activeWorkspace?.id == button.workspaceValue) ? - Appearance.m3colors.m3onPrimary : - (workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer : + color: (activeWorkspaceIndex == index) ? + Appearance.m3colors.m3onPrimary : + (workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer1Inactive) Behavior on opacity { @@ -268,7 +292,7 @@ Item { opacity: !Config.options?.bar.workspaces.showAppIcons ? 0 : (workspaceButtonBackground.biggestWindow && !GlobalStates.workspaceShowNumbers && Config.options?.bar.workspaces.showAppIcons) ? 1 : workspaceButtonBackground.biggestWindow ? workspaceIconOpacityShrinked : 0 - visible: opacity > 0 + visible: opacity > 0 IconImage { id: mainAppIcon anchors.bottom: parent.bottom @@ -308,8 +332,8 @@ Item { (workspaceButtonWidth - workspaceIconSize) / 2 - 2 : workspaceIconMarginShrinked anchors.rightMargin: (!GlobalStates.workspaceShowNumbers) ? (workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked - color: (monitor.activeWorkspace?.id == button.workspaceValue) ? - Appearance.m3colors.m3onPrimary : Appearance.m3colors.m3onSecondaryContainer + color: (activeWorkspaceIndex == index) ? + Appearance.m3colors.m3onPrimary : Appearance.colors.colOnSecondaryContainer Behavior on opacity { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) diff --git a/src/share/sleex/modules/cheatsheet/Cheatsheet.qml b/src/share/sleex/modules/cheatsheet/Cheatsheet.qml index 2146fb23..4ae33395 100644 --- a/src/share/sleex/modules/cheatsheet/Cheatsheet.qml +++ b/src/share/sleex/modules/cheatsheet/Cheatsheet.qml @@ -161,5 +161,4 @@ Scope { // Scope cheatsheetLoader.active = false; } } - } diff --git a/src/share/sleex/modules/dock/Dock.qml b/src/share/sleex/modules/dock/Dock.qml index d96b82eb..78a4f234 100644 --- a/src/share/sleex/modules/dock/Dock.qml +++ b/src/share/sleex/modules/dock/Dock.qml @@ -10,7 +10,6 @@ import Quickshell.Io import Quickshell import Quickshell.Widgets import Quickshell.Wayland -import Quickshell.Hyprland Scope { // Scope id: root @@ -27,7 +26,7 @@ Scope { // Scope property bool reveal: root.pinned || (Config.options?.dock.hoverToReveal && dockMouseArea.containsMouse) || dockApps.requestDockShow - || (!ToplevelManager.activeToplevel?.activated) + || (ToplevelManager.toplevels?.length === 0) anchors { bottom: true diff --git a/src/share/sleex/modules/onScreenDisplay/OnScreenDisplayBrightness.qml b/src/share/sleex/modules/onScreenDisplay/OnScreenDisplayBrightness.qml index af6115cb..94d7018b 100644 --- a/src/share/sleex/modules/onScreenDisplay/OnScreenDisplayBrightness.qml +++ b/src/share/sleex/modules/onScreenDisplay/OnScreenDisplayBrightness.qml @@ -148,5 +148,4 @@ Scope { root.showOsdValues = false } } - } \ No newline at end of file diff --git a/src/share/sleex/modules/onScreenDisplay/OnScreenDisplayVolume.qml b/src/share/sleex/modules/onScreenDisplay/OnScreenDisplayVolume.qml index 5598f2ef..8c47e0c5 100644 --- a/src/share/sleex/modules/onScreenDisplay/OnScreenDisplayVolume.qml +++ b/src/share/sleex/modules/onScreenDisplay/OnScreenDisplayVolume.qml @@ -200,5 +200,4 @@ Scope { root.showOsdValues = false } } - } \ No newline at end of file diff --git a/src/share/sleex/modules/overview/Overview.qml b/src/share/sleex/modules/overview/Overview.qml index 5687357e..ca55ecc1 100644 --- a/src/share/sleex/modules/overview/Overview.qml +++ b/src/share/sleex/modules/overview/Overview.qml @@ -9,6 +9,7 @@ import Quickshell import Quickshell.Io import Quickshell.Wayland import Quickshell.Hyprland +import Sleex.Fhtc Scope { id: overviewScope @@ -20,14 +21,14 @@ Scope { id: root required property var modelData property string searchingText: "" - readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) - property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor.id) + required property ShellScreen screen + property bool monitorIsFocused: (FhtcMonitors.activeMonitorName === screen.name) screen: modelData visible: GlobalStates.overviewOpen && monitorIsFocused WlrLayershell.namespace: "quickshell:overview" WlrLayershell.layer: WlrLayer.Overlay - // WlrLayershell.keyboardFocus: GlobalStates.overviewOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + WlrLayershell.keyboardFocus: GlobalStates.overviewOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None color: "transparent" mask: Region { @@ -203,7 +204,7 @@ Scope { } for (let i = 0; i < overviewVariants.instances.length; i++) { let panelWindow = overviewVariants.instances[i]; - if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { + if (panelWindow.modelData.name == FhtcMonitors.activeMonitorName) { overviewScope.dontAutoCancelSearch = true; panelWindow.setSearchingText( Config.options.search.prefix.clipboard @@ -226,7 +227,7 @@ Scope { } for (let i = 0; i < overviewVariants.instances.length; i++) { let panelWindow = overviewVariants.instances[i]; - if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { + if (panelWindow.modelData.name == FhtcMonitors.activeMonitorName) { overviewScope.dontAutoCancelSearch = true; panelWindow.setSearchingText( Config.options.search.prefix.emojis @@ -237,5 +238,4 @@ Scope { } } } - } diff --git a/src/share/sleex/modules/overview/OverviewWidget.qml b/src/share/sleex/modules/overview/OverviewWidget.qml index 0b8d73ca..6894298a 100644 --- a/src/share/sleex/modules/overview/OverviewWidget.qml +++ b/src/share/sleex/modules/overview/OverviewWidget.qml @@ -11,31 +11,26 @@ import Quickshell.Io import Quickshell.Widgets import Quickshell.Wayland import Quickshell.Hyprland +import Sleex.Fhtc Item { id: root required property var panelWindow - readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen) - readonly property var toplevels: ToplevelManager.toplevels + readonly property var monitor: FhtcMonitors.activeMonitor + // readonly property var toplevels: ToplevelManager.toplevels readonly property int workspacesShown: Config.options.overview.numOfRows * Config.options.overview.numOfCols - readonly property int workspaceGroup: Math.floor((monitor.activeWorkspace?.id - 1) / workspacesShown) - property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor.id) - property var windows: HyprlandData.windowList - property var windowByAddress: HyprlandData.windowByAddress - property var windowAddresses: HyprlandData.addresses - property var monitorData: HyprlandData.monitors.find(m => m.id === root.monitor.id) + readonly property int workspaceGroup: Math.floor(((monitor["active-workspace-idx"] ?? 0)) / workspacesShown) + property bool monitorIsFocused: (FhtcMonitors.activeMonitorName === screen.name) + readonly property var focusedScreen: Quickshell.screens.find(s => s.name === FhtcMonitors.activeMonitorName) property real scale: Config.options.overview.scale property color activeBorderColor: Appearance.colors.colSecondary - property real workspaceImplicitWidth: (monitorData?.transform % 2 === 1) ? - ((monitor.height - monitorData?.reserved[0] - monitorData?.reserved[2]) * root.scale / monitor.scale) : - ((monitor.width - monitorData?.reserved[0] - monitorData?.reserved[2]) * root.scale / monitor.scale) - property real workspaceImplicitHeight: (monitorData?.transform % 2 === 1) ? - ((monitor.width - monitorData?.reserved[1] - monitorData?.reserved[3]) * root.scale / monitor.scale) : - ((monitor.height - monitorData?.reserved[1] - monitorData?.reserved[3]) * root.scale / monitor.scale) + property real workspaceImplicitWidth: (focusedScreen?.width ?? 0) * root.scale + property real workspaceImplicitHeight: ((focusedScreen?.height ?? 0) - Appearance.sizes.barHeight) * root.scale property real workspaceNumberMargin: 80 - property real workspaceNumberSize: Math.min(workspaceImplicitHeight, workspaceImplicitWidth) * monitor.scale + property real workspaceNumberSize: Math.min(workspaceImplicitHeight, workspaceImplicitWidth) * (panelWindow.screen.devicePixelRatio ?? 1) + property int workspaceZ: 0 property int windowZ: 1 property int windowDraggingZ: 99999 @@ -82,7 +77,7 @@ Item { Rectangle { // Workspace id: workspace property int colIndex: index - property int workspaceValue: root.workspaceGroup * workspacesShown + rowIndex * Config.options.overview.numOfCols + colIndex + 1 + property int workspaceValue: root.workspaceGroup * workspacesShown + rowIndex * Config.options.overview.numOfCols + colIndex property color defaultWorkspaceColor: Appearance.colors.colLayer1 // TODO: reconsider this color for a cleaner look property color hoveredWorkspaceColor: ColorUtils.mix(defaultWorkspaceColor, Appearance.colors.colLayer1Hover, 0.1) property color hoveredBorderColor: Appearance.colors.colLayer2Hover @@ -111,9 +106,8 @@ Item { acceptedButtons: Qt.LeftButton onClicked: { if (root.draggingTargetWorkspace === -1) { - // Hyprland.dispatch(`exec qs ipc call overview close`) GlobalStates.overviewOpen = false - Hyprland.dispatch(`workspace ${workspaceValue}`) + FhtcIpc.dispatch("focus-workspace-by-index", { "workspace_idx": workspaceValue }) } } } @@ -146,36 +140,29 @@ Item { Repeater { // Window repeater model: ScriptModel { values: { - // console.log(JSON.stringify(ToplevelManager.toplevels.values.map(t => t), null, 2)) - return ToplevelManager.toplevels.values.filter((toplevel) => { - const address = `0x${toplevel.HyprlandToplevel.address}` - // console.log(`Checking window with address: ${address}`) - var win = windowByAddress[address] - return (root.workspaceGroup * root.workspacesShown < win?.workspace?.id && win?.workspace?.id <= (root.workspaceGroup + 1) * root.workspacesShown) + return Object.values(FhtcWorkspaces.windows).filter((win) => { + const wsId = win?.["workspace-id"] ?? -1 + return wsId >= root.workspaceGroup * root.workspacesShown && + wsId < (root.workspaceGroup + 1) * root.workspacesShown }) } } delegate: OverviewWindow { id: window required property var modelData - property var address: `0x${modelData.HyprlandToplevel.address}` - windowData: windowByAddress[address] - toplevel: modelData - monitorData: root.monitorData + windowData: modelData + // toplevel: not yet available with fhtc compositor + monitorData: root.focusedScreen scale: root.scale availableWorkspaceWidth: root.workspaceImplicitWidth availableWorkspaceHeight: root.workspaceImplicitHeight - property int monitorId: windowData?.monitor - property var monitor: HyprlandData.monitors[monitorId] - property bool atInitPosition: (initX == x && initY == y) - restrictToWorkspace: Drag.active || atInitPosition - property int workspaceColIndex: (windowData?.workspace.id - 1) % Config.options.overview.numOfCols - property int workspaceRowIndex: Math.floor((windowData?.workspace.id - 1) % root.workspacesShown / Config.options.overview.numOfCols) - xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex - (monitor?.x * root.scale) - yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex - (monitor?.y * root.scale) + property int workspaceColIndex: (modelData?.["workspace-id"] ?? 0) % Config.options.overview.numOfCols + property int workspaceRowIndex: Math.floor((modelData?.["workspace-id"] ?? 0) % root.workspacesShown / Config.options.overview.numOfCols) + xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex - ((root.focusedScreen?.x ?? 0) * root.scale) + yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex - ((root.focusedScreen?.y ?? 0) * root.scale) Timer { id: updateWindowPosition @@ -183,12 +170,13 @@ Item { repeat: false running: false onTriggered: { - window.x = Math.round(Math.max((windowData?.at[0] - monitorData?.reserved[0]) * root.scale, 0) + xOffset) - window.y = Math.round(Math.max((windowData?.at[1] - monitorData?.reserved[1]) * root.scale, 0) + yOffset) - // console.log(`[OverviewWindow] Updated position for window ${windowData?.address} to (${window.x}, ${window.y})`) + window.x = Math.round(Math.max(modelData?.location[0] * root.scale, 0) + xOffset) + window.y = Math.round(Math.max((modelData?.location[1] - Appearance.sizes.barHeight) * root.scale, 0) + yOffset) } } + Component.onCompleted: updateWindowPosition.restart() + z: atInitPosition ? root.windowZ : root.windowDraggingZ Drag.hotSpot.x: targetWindowWidth / 2 Drag.hotSpot.y: targetWindowHeight / 2 @@ -201,35 +189,32 @@ Item { acceptedButtons: Qt.LeftButton | Qt.MiddleButton drag.target: parent onPressed: { - root.draggingFromWorkspace = windowData?.workspace.id + root.draggingFromWorkspace = modelData?.["workspace-id"] window.pressed = true window.Drag.active = true window.Drag.source = window - // console.log(`[OverviewWindow] Dragging window ${windowData?.address} from position (${window.x}, ${window.y})`) } onReleased: { const targetWorkspace = root.draggingTargetWorkspace window.pressed = false window.Drag.active = false root.draggingFromWorkspace = -1 - if (targetWorkspace !== -1 && targetWorkspace !== windowData?.workspace.id) { - Hyprland.dispatch(`movetoworkspacesilent ${targetWorkspace}, address:${window.windowData?.address}`) + if (targetWorkspace !== -1 && targetWorkspace !== modelData?.["workspace-id"]) { + FhtcIpc.dispatch("send-window-to-workspace", { "window-id": modelData?.id, "workspace-id": targetWorkspace }) updateWindowPosition.restart() - } - else { + } else { window.x = window.initX window.y = window.initY } } onClicked: (event) => { - if (!windowData) return; - + if (!modelData) return; if (event.button === Qt.LeftButton) { GlobalStates.overviewOpen = false - Hyprland.dispatch(`focuswindow address:${windowData.address}`) + FhtcIpc.dispatch("focus-window", { "window-id": modelData.id }) event.accepted = true } else if (event.button === Qt.MiddleButton) { - Hyprland.dispatch(`closewindow address:${windowData.address}`) + FhtcIpc.dispatch("close-window", { "window-id": modelData.id }) event.accepted = true } } @@ -237,7 +222,7 @@ Item { StyledToolTip { extraVisibleCondition: false alternativeVisibleCondition: dragArea.containsMouse && !window.Drag.active - text: `${windowData.title}\n[${windowData.class}] ${windowData.xwayland ? "[XWayland] " : ""}\n` + text: `${modelData.title}\n[${modelData["app-id"]}]\n` } } } @@ -245,9 +230,9 @@ Item { Rectangle { // Focused workspace indicator id: focusedWorkspaceIndicator - property int activeWorkspaceInGroup: monitor.activeWorkspace?.id - (root.workspaceGroup * root.workspacesShown) - property int activeWorkspaceRowIndex: Math.floor((activeWorkspaceInGroup - 1) / Config.options.overview.numOfCols) - property int activeWorkspaceColIndex: (activeWorkspaceInGroup - 1) % Config.options.overview.numOfCols + property int activeWorkspaceInGroup: (monitor["active-workspace-idx"] ?? 0) - (root.workspaceGroup * root.workspacesShown) + property int activeWorkspaceRowIndex: Math.floor(activeWorkspaceInGroup / Config.options.overview.numOfCols) + property int activeWorkspaceColIndex: activeWorkspaceInGroup % Config.options.overview.numOfCols x: (root.workspaceImplicitWidth + workspaceSpacing) * activeWorkspaceColIndex y: (root.workspaceImplicitHeight + workspaceSpacing) * activeWorkspaceRowIndex z: root.windowZ diff --git a/src/share/sleex/modules/overview/OverviewWindow.qml b/src/share/sleex/modules/overview/OverviewWindow.qml index e75a6487..c6410a68 100644 --- a/src/share/sleex/modules/overview/OverviewWindow.qml +++ b/src/share/sleex/modules/overview/OverviewWindow.qml @@ -22,8 +22,8 @@ Item { // Window property var availableWorkspaceWidth property var availableWorkspaceHeight property bool restrictToWorkspace: true - property real initX: Math.max((windowData?.at[0] - monitorData?.reserved[0]) * root.scale, 0) + xOffset - property real initY: Math.max((windowData?.at[1] - monitorData?.reserved[1]) * root.scale, 0) + yOffset + property real initX: Math.max(windowData?.location[0] * root.scale, 0) + xOffset + property real initY: Math.max((windowData?.location[1] - Appearance.sizes.barHeight) * root.scale, 0) + yOffset property real xOffset: 0 property real yOffset: 0 @@ -35,10 +35,10 @@ Item { // Window property var iconToWindowRatio: 0.35 property var xwaylandIndicatorToIconRatio: 0.35 property var iconToWindowRatioCompact: 0.6 - property var iconPath: Quickshell.iconPath(AppSearch.guessIcon(windowData?.class), "image-missing") + property var iconPath: Quickshell.iconPath(AppSearch.guessIcon(windowData?.["app-id"]), "image-missing") property bool compactMode: Appearance.font.pixelSize.smaller * 4 > targetWindowHeight || Appearance.font.pixelSize.smaller * 4 > targetWindowWidth - property bool indicateXWayland: (Config.options.overview.showXwaylandIndicator && windowData?.xwayland) ?? false + // property bool indicateXWayland: (Config.options.overview.showXwaylandIndicator && windowData?.xwayland) ?? false x: initX y: initY @@ -70,7 +70,7 @@ Item { // Window ScreencopyView { id: windowPreview anchors.fill: parent - captureSource: GlobalStates.overviewOpen ? root.toplevel : null + captureSource: null // GlobalStates.overviewOpen ? root.toplevel : null live: true Rectangle { diff --git a/src/share/sleex/modules/session/Session.qml b/src/share/sleex/modules/session/Session.qml index 9987efca..9d7a50bf 100644 --- a/src/share/sleex/modules/session/Session.qml +++ b/src/share/sleex/modules/session/Session.qml @@ -10,10 +10,11 @@ import Quickshell.Io import Quickshell.Widgets import Quickshell.Wayland import Quickshell.Hyprland +import Sleex.Fhtc Scope { id: root - property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) + property var focusedScreen: Quickshell.screens.find(s => s.name === FhtcMonitors.activeMonitorName) Loader { id: sessionLoader @@ -224,5 +225,4 @@ Scope { sessionLoader.active = true; } } - } diff --git a/src/share/sleex/modules/sidebarLeft/SidebarLeft.qml b/src/share/sleex/modules/sidebarLeft/SidebarLeft.qml index fbdfe662..268851bb 100644 --- a/src/share/sleex/modules/sidebarLeft/SidebarLeft.qml +++ b/src/share/sleex/modules/sidebarLeft/SidebarLeft.qml @@ -199,5 +199,4 @@ Scope { // Scope root.detach = !root.detach; } } - } diff --git a/src/share/sleex/modules/wallpaperSelector/WallpaperSelector.qml b/src/share/sleex/modules/wallpaperSelector/WallpaperSelector.qml index 4630d489..22f501e3 100644 --- a/src/share/sleex/modules/wallpaperSelector/WallpaperSelector.qml +++ b/src/share/sleex/modules/wallpaperSelector/WallpaperSelector.qml @@ -193,5 +193,4 @@ Scope { GlobalStates.wppselectorOpen = false; } } - } diff --git a/src/share/sleex/plugins/src/Sleex/fhtc/CMakeLists.txt b/src/share/sleex/plugins/src/Sleex/fhtc/CMakeLists.txt new file mode 100644 index 00000000..50cb6f15 --- /dev/null +++ b/src/share/sleex/plugins/src/Sleex/fhtc/CMakeLists.txt @@ -0,0 +1,16 @@ + +qml_module(sleex-fhtc + URI Sleex.Fhtc + SOURCES + ipc.hpp ipc.cpp + workspaces.hpp workspaces.cpp + monitors.hpp monitors.cpp + plugin.cpp +) + +target_link_libraries(sleex-fhtc + PRIVATE + Qt6::Core + Qt6::Qml + Qt6::Network +) \ No newline at end of file diff --git a/src/share/sleex/plugins/src/Sleex/fhtc/ipc.cpp b/src/share/sleex/plugins/src/Sleex/fhtc/ipc.cpp new file mode 100644 index 00000000..a2b4454f --- /dev/null +++ b/src/share/sleex/plugins/src/Sleex/fhtc/ipc.cpp @@ -0,0 +1,220 @@ +#include "ipc.hpp" +#include +#include +#include +#include +#include + +Ipc::Ipc(QObject *parent) +: QObject(parent), +m_requestSocket(new QLocalSocket(this)), +m_eventSocket(new QLocalSocket(this)) +{ + m_socketPath = qEnvironmentVariable("FHTC_SOCKET_PATH"); + if (m_socketPath.isEmpty()) { + emit error("The environment variable FHTC_SOCKET_PATH is not set."); + qWarning() << "Ipc: FHTC_SOCKET_PATH is not set."; + } + + // Connections for the request socket + connect(m_requestSocket, &QLocalSocket::connected, this, &Ipc::onRequestConnected); + connect(m_requestSocket, &QLocalSocket::readyRead, this, &Ipc::onRequestReadyRead); + connect(m_requestSocket, &QLocalSocket::errorOccurred, this, &Ipc::onRequestError); + connect(m_requestSocket, &QLocalSocket::disconnected, this, &Ipc::onRequestDisconnected); + + // Connections for the event socket + connect(m_eventSocket, &QLocalSocket::connected, this, &Ipc::onEventConnected); + connect(m_eventSocket, &QLocalSocket::readyRead, this, &Ipc::onEventReadyRead); + connect(m_eventSocket, &QLocalSocket::errorOccurred, this, &Ipc::onEventError); + connect(m_eventSocket, &QLocalSocket::disconnected, this, &Ipc::onEventDisconnected); +} + +Ipc::~Ipc() +{ + // Sockets are automatically destroyed because 'this' is their parent +} + + +void Ipc::subscribe() +{ + if (m_socketPath.isEmpty()) return; + if (m_eventSocket->state() == QLocalSocket::UnconnectedState) { + qDebug() << "Ipc: Connecting to the event socket..."; + m_eventSocket->connectToServer(m_socketPath); + } +} + +void Ipc::dispatch(const QString &name, const QVariantMap &args) +{ + if (!args.isEmpty() || name.contains("workspace")) { + QVariantMap actionBody; + actionBody.insert(name, args); + sendAction(actionBody); + } + else { + sendAction(name); + } +} + +void Ipc::sendRequest(const QVariant &request) +{ + if (m_socketPath.isEmpty()) return; + + // qDebug() << "Ipc: Queuing request:" << request; + + m_pendingRequests.enqueue(request); + if (m_requestSocket->state() == QLocalSocket::UnconnectedState) { + qDebug() << "Ipc: Connecting to the request socket..."; + m_requestSocket->connectToServer(m_socketPath); + } else if (m_requestSocket->state() == QLocalSocket::ConnectedState) { + // If already connected, send the pending request + writeJson(m_requestSocket, m_pendingRequests.dequeue()); + } + // If 'Connecting', the request will be sent in onRequestConnected +} + +void Ipc::sendAction(const QVariant &action) +{ + QVariantMap request; + request.insert("action", action); + sendRequest(request); +} + + +void Ipc::onRequestConnected() +{ + qDebug() << "Ipc: Request socket connected."; + // Send the first pending request (if any) + if (!m_pendingRequests.isEmpty()) { + writeJson(m_requestSocket, m_pendingRequests.dequeue()); + } +} + +void Ipc::onRequestReadyRead() +{ + m_requestBuffer.append(m_requestSocket->readAll()); + + // The fhtc IPC guarantees one response per request on this socket. + // We do not loop, we just wait for a complete line. + QVariant response = parseLine(m_requestBuffer); + if (!response.isNull()) { + // qDebug() << "Ipc: Received response:" << response; + + emit requestResponse(response); + + // If there are other requests, send them + if (!m_pendingRequests.isEmpty()) { + writeJson(m_requestSocket, m_pendingRequests.dequeue()); + } + } +} + +void Ipc::onRequestError(QLocalSocket::LocalSocketError socketError) +{ + Q_UNUSED(socketError); + QString msg = "Ipc (Request): " + m_requestSocket->errorString(); + qWarning() << msg; + emit error(msg); + m_pendingRequests.clear(); // Clear pending requests +} + +void Ipc::onRequestDisconnected() +{ + qDebug() << "Ipc: Request socket disconnected."; + if (!m_requestBuffer.isEmpty()) { + qWarning() << "Ipc (Request): Disconnected with data in buffer:" << m_requestBuffer; + m_requestBuffer.clear(); + } +} + + +void Ipc::onEventConnected() +{ + qDebug() << "Ipc: Event socket connected. Sending 'subscribe' request."; + emit subscribed(); + + // Send the subscribe request + QVariantMap subRequest; + subRequest.insert("subscribe", QVariant()); // "subscribe": null + writeJson(m_eventSocket, subRequest); +} + +void Ipc::onEventReadyRead() +{ + m_eventBuffer.append(m_eventSocket->readAll()); + + // There may be multiple JSON events in the buffer + while (true) { + QVariant event = parseLine(m_eventBuffer); + if (event.isNull()) { + break; // Incomplete line, wait for more data + } + emit newEvent(event); + } +} + +void Ipc::onEventError(QLocalSocket::LocalSocketError socketError) +{ + Q_UNUSED(socketError); + QString msg = "Ipc (Event): " + m_eventSocket->errorString(); + qWarning() << msg; + emit error(msg); +} + +void Ipc::onEventDisconnected() +{ + QString msg = "Ipc: Event socket disconnected. Attempting to reconnect in 5s..."; + qWarning() << msg; + emit error(msg); + + // Attempt automatic reconnection for the event socket + QTimer::singleShot(5000, this, &Ipc::subscribe); +} + +// --- Utility functions --- +void Ipc::writeJson(QLocalSocket *socket, const QVariant &data) +{ + if (socket->state() != QLocalSocket::ConnectedState) { + qWarning() << "Ipc: Attempt to write to a socket that is not connected."; + return; + } + + QJsonDocument doc = QJsonDocument::fromVariant(data); + if (doc.isNull()) { + qWarning() << "Ipc: Failed to convert QVariant to JSON."; + return; + } + + QByteArray json = doc.toJson(QJsonDocument::Compact); + json.append('\n'); // fhtc IPC is delimited by newlines + + socket->write(json); +} + +QVariant Ipc::parseLine(QByteArray &buffer) +{ + int newlineIdx = buffer.indexOf('\n'); + if (newlineIdx == -1) { + return QVariant(); // Incomplete line + } + + // Extract the line and remove it from the buffer + QByteArray line = buffer.left(newlineIdx); + buffer = buffer.mid(newlineIdx + 1); + + if (line.isEmpty()) { + return QVariant(); // Empty line, ignore + } + + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(line, &parseError); + + if (parseError.error != QJsonParseError::NoError) { + QString msg = "Ipc: JSON parse error: " + parseError.errorString() + " | Line: " + QString(line); + qWarning() << msg; + emit error(msg); + return QVariant(); + } + + return doc.toVariant(); +} diff --git a/src/share/sleex/plugins/src/Sleex/fhtc/ipc.hpp b/src/share/sleex/plugins/src/Sleex/fhtc/ipc.hpp new file mode 100644 index 00000000..ca5abb6f --- /dev/null +++ b/src/share/sleex/plugins/src/Sleex/fhtc/ipc.hpp @@ -0,0 +1,56 @@ +#ifndef IPC_H +#define IPC_H + +#include +#include +#include +#include +#include + +class Ipc : public QObject +{ + Q_OBJECT + QML_NAMED_ELEMENT(FhtcIpc) + QML_SINGLETON + +public: + explicit Ipc(QObject *parent = nullptr); + ~Ipc() override; + +signals: + void error(const QString &message); + void requestResponse(const QVariant &response); + void subscribed(); + void newEvent(const QVariant &event); + +public slots: + void subscribe(); + void sendRequest(const QVariant &request); + void sendAction(const QVariant &action); + void dispatch(const QString &name, const QVariantMap &args = {}); + +private slots: + void onRequestConnected(); + void onRequestReadyRead(); + void onRequestError(QLocalSocket::LocalSocketError socketError); + void onRequestDisconnected(); + + void onEventConnected(); + void onEventReadyRead(); + void onEventError(QLocalSocket::LocalSocketError socketError); + void onEventDisconnected(); + +private: + void writeJson(QLocalSocket* socket, const QVariant &data); + QVariant parseLine(QByteArray &buffer); + + QString m_socketPath; + QLocalSocket *m_requestSocket; + QLocalSocket *m_eventSocket; + + QByteArray m_eventBuffer; + QByteArray m_requestBuffer; + QQueue m_pendingRequests; +}; + +#endif // IPC_H \ No newline at end of file diff --git a/src/share/sleex/plugins/src/Sleex/fhtc/monitors.cpp b/src/share/sleex/plugins/src/Sleex/fhtc/monitors.cpp new file mode 100644 index 00000000..05db8968 --- /dev/null +++ b/src/share/sleex/plugins/src/Sleex/fhtc/monitors.cpp @@ -0,0 +1,46 @@ +#include "monitors.hpp" +#include + +Monitors::Monitors(QObject *parent) : QObject(parent) +{ + m_ipc = new Ipc(this); + + connect(m_ipc, &Ipc::newEvent, this, &Monitors::handleEvent); + + m_ipc->subscribe(); +} + +void Monitors::handleEvent(const QVariant &eventVar) +{ + QVariantMap event = eventVar.toMap(); + QString type = event.value("event").toString(); + QVariant data = event.value("data"); + + if (type == "space") { + QVariant data = event.value("data"); + QVariantMap monitorsMap = data.toMap().value("monitors").toMap(); + + m_monitors = monitorsMap; + + bool activeFound = false; + QMapIterator i(m_monitors); + while (i.hasNext()) { + i.next(); + QVariantMap monitor = i.value().toMap(); + if (monitor.value("active").toBool()) { + m_activeMonitor = monitor; + m_activeMonitorName = monitor.value("output").toString(); + activeFound = true; + break; + } + } + + if (!activeFound) { + m_activeMonitor = QVariant(); + } + + emit monitorsChanged(); + emit activeMonitorChanged(); + emit activeMonitorNameChanged(); + } +} \ No newline at end of file diff --git a/src/share/sleex/plugins/src/Sleex/fhtc/monitors.hpp b/src/share/sleex/plugins/src/Sleex/fhtc/monitors.hpp new file mode 100644 index 00000000..fd0a3fc6 --- /dev/null +++ b/src/share/sleex/plugins/src/Sleex/fhtc/monitors.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include +#include "ipc.hpp" + +class Monitors : public QObject +{ + Q_OBJECT + QML_NAMED_ELEMENT(FhtcMonitors) + QML_SINGLETON + + Q_PROPERTY(QVariantMap monitors READ monitors NOTIFY monitorsChanged) + Q_PROPERTY(QVariant activeMonitor READ activeMonitor NOTIFY activeMonitorChanged) + Q_PROPERTY(QString activeMonitorName READ activeMonitorName NOTIFY activeMonitorNameChanged) + +public: + explicit Monitors(QObject *parent = nullptr); + + QVariantMap monitors() const { return m_monitors; } + QVariant activeMonitor() const { return m_activeMonitor; } + QString activeMonitorName() const { return m_activeMonitorName; } + +signals: + void monitorsChanged(); + void activeMonitorChanged(); + void activeMonitorNameChanged(); + +private slots: + void handleEvent(const QVariant &eventVar); + +private: + Ipc *m_ipc; + + QVariantMap m_monitors; + QVariant m_activeMonitor; + QString m_activeMonitorName; +}; \ No newline at end of file diff --git a/src/share/sleex/plugins/src/Sleex/fhtc/plugin.cpp b/src/share/sleex/plugins/src/Sleex/fhtc/plugin.cpp new file mode 100644 index 00000000..aff6826b --- /dev/null +++ b/src/share/sleex/plugins/src/Sleex/fhtc/plugin.cpp @@ -0,0 +1,14 @@ +#include + +class SleexFhtcPlugin : public QQmlExtensionPlugin { + Q_OBJECT + Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid) + +public: + void registerTypes(const char *uri) override { + // QML_ELEMENT and QML_SINGLETON macros handle registration automatically + Q_UNUSED(uri) + } +}; + +#include "plugin.moc" \ No newline at end of file diff --git a/src/share/sleex/plugins/src/Sleex/fhtc/workspaces.cpp b/src/share/sleex/plugins/src/Sleex/fhtc/workspaces.cpp new file mode 100644 index 00000000..145f2d5a --- /dev/null +++ b/src/share/sleex/plugins/src/Sleex/fhtc/workspaces.cpp @@ -0,0 +1,94 @@ +#include "workspaces.hpp" +#include + +Workspaces::Workspaces(QObject *parent) : QObject(parent) +{ + m_ipc = new Ipc(this); + + connect(m_ipc, &Ipc::newEvent, this, &Workspaces::handleEvent); + + m_ipc->subscribe(); +} + +void Workspaces::handleEvent(const QVariant &eventVar) +{ + QVariantMap event = eventVar.toMap(); + QString type = event.value("event").toString(); + QVariant data = event.value("data"); + + if (type == "windows") { + m_windows = data.toMap(); + emit windowsChanged(); + } + else if (type == "focused-window-changed") { + QVariantMap dataMap = data.toMap(); + QVariant idVar = dataMap.value("id"); + + if (idVar.isNull() || !idVar.isValid()) { + m_focusedWindowId = -1; + m_focusedWindow = QVariant(); + } else { + m_focusedWindowId = idVar.toInt(); + m_focusedWindow = m_windows.value(QString::number(m_focusedWindowId)); + } + emit focusedWindowIdChanged(); + emit focusedWindowChanged(); + } + else if (type == "window-closed") { + int id = data.toMap().value("id").toInt(); + m_windows.remove(QString::number(id)); + emit windowsChanged(); + } + else if (type == "window-changed") { + QVariantMap win = data.toMap(); + int id = win.value("id").toInt(); + + m_windows.insert(QString::number(id), win); + emit windowsChanged(); + + if (id == m_focusedWindowId) { + m_focusedWindow = win; + emit focusedWindowChanged(); + } + } + else if (type == "workspaces") { + m_workspaces = data.toMap(); + emit workspacesChanged(); + + if (m_activeWorkspaceId != -1) { + m_activeWorkspace = m_workspaces.value(QString::number(m_activeWorkspaceId)); + emit activeWorkspaceChanged(); + } + } + else if (type == "active-workspace-changed") { + QVariantMap dataMap = data.toMap(); + QVariant idVar = dataMap.value("id"); + + if (idVar.isNull() || !idVar.isValid()) { + m_activeWorkspaceId = -1; + m_activeWorkspace = QVariant(); + } else { + m_activeWorkspaceId = idVar.toInt(); + m_activeWorkspace = m_workspaces.value(QString::number(m_activeWorkspaceId)); + } + emit activeWorkspaceIdChanged(); + emit activeWorkspaceChanged(); + } + else if (type == "workspace-changed") { + QVariantMap ws = data.toMap(); + int id = ws.value("id").toInt(); + + m_workspaces.insert(QString::number(id), ws); + emit workspacesChanged(); + + if (id == m_activeWorkspaceId) { + m_activeWorkspace = ws; + emit activeWorkspaceChanged(); + } + } + else if (type == "workspace-removed") { + int id = data.toMap().value("id").toInt(); + m_workspaces.remove(QString::number(id)); + emit workspacesChanged(); + } +} \ No newline at end of file diff --git a/src/share/sleex/plugins/src/Sleex/fhtc/workspaces.hpp b/src/share/sleex/plugins/src/Sleex/fhtc/workspaces.hpp new file mode 100644 index 00000000..72fa77bc --- /dev/null +++ b/src/share/sleex/plugins/src/Sleex/fhtc/workspaces.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include +#include "ipc.hpp" + +class Workspaces : public QObject +{ + Q_OBJECT + QML_NAMED_ELEMENT(FhtcWorkspaces) + QML_SINGLETON + + Q_PROPERTY(QVariantMap windows READ windows NOTIFY windowsChanged) + Q_PROPERTY(QVariantMap workspaces READ workspaces NOTIFY workspacesChanged) + + Q_PROPERTY(int focusedWindowId READ focusedWindowId NOTIFY focusedWindowIdChanged) + Q_PROPERTY(QVariant focusedWindow READ focusedWindow NOTIFY focusedWindowChanged) + + Q_PROPERTY(int activeWorkspaceId READ activeWorkspaceId NOTIFY activeWorkspaceIdChanged) + Q_PROPERTY(QVariant activeWorkspace READ activeWorkspace NOTIFY activeWorkspaceChanged) + +public: + explicit Workspaces(QObject *parent = nullptr); + + QVariantMap windows() const { return m_windows; } + QVariantMap workspaces() const { return m_workspaces; } + int focusedWindowId() const { return m_focusedWindowId; } + QVariant focusedWindow() const { return m_focusedWindow; } + int activeWorkspaceId() const { return m_activeWorkspaceId; } + QVariant activeWorkspace() const { return m_activeWorkspace; } + +signals: + void windowsChanged(); + void workspacesChanged(); + void focusedWindowIdChanged(); + void focusedWindowChanged(); + void activeWorkspaceIdChanged(); + void activeWorkspaceChanged(); + +private slots: + void handleEvent(const QVariant &eventVar); + +private: + Ipc *m_ipc; + + QVariantMap m_windows; + QVariantMap m_workspaces; + int m_focusedWindowId = -1; + QVariant m_focusedWindow; + int m_activeWorkspaceId = -1; + QVariant m_activeWorkspace; +}; \ No newline at end of file