1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-02 19:48:27 -04:00
Files
DankMaterialShell/quickshell/Modules/WorkspaceOverlays/OverviewWidget.qml
T
2026-07-15 10:10:33 -04:00

511 lines
21 KiB
QML

import QtQuick
import Quickshell
import Quickshell.Hyprland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
readonly property var log: Log.scoped("OverviewWidget")
required property var panelWindow
required property bool overviewOpen
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen)
readonly property real dpr: CompositorService.getScreenScale(panelWindow.screen)
readonly property int workspacesShown: SettingsData.overviewRows * SettingsData.overviewColumns
readonly property var allWorkspaces: Hyprland.workspaces?.values || []
readonly property var allWorkspaceIds: {
const workspaces = allWorkspaces;
if (!workspaces || workspaces.length === 0)
return [];
try {
const ids = workspaces.map(ws => ws?.id).filter(id => id !== null && id !== undefined);
return ids.sort((a, b) => a - b);
} catch (e) {
return [];
}
}
readonly property var thisMonitorWorkspaceIds: {
const workspaces = allWorkspaces;
const mon = monitor;
if (!workspaces || workspaces.length === 0 || !mon)
return [];
try {
const filtered = workspaces.filter(ws => ws?.monitor?.name === mon.name);
return filtered.map(ws => ws?.id).filter(id => id !== null && id !== undefined).sort((a, b) => a - b);
} catch (e) {
return [];
}
}
readonly property var displayedWorkspaceIds: {
if (!allWorkspaceIds || allWorkspaceIds.length === 0) {
const result = [];
for (let i = 1; i <= workspacesShown; i++) {
result.push(i);
}
return result;
}
try {
const maxExisting = Math.max(...allWorkspaceIds);
const totalNeeded = Math.max(workspacesShown, allWorkspaceIds.length);
const result = [];
for (let i = 1; i <= maxExisting; i++) {
result.push(i);
}
let nextId = maxExisting + 1;
while (result.length < totalNeeded) {
result.push(nextId);
nextId++;
}
return result;
} catch (e) {
const result = [];
for (let i = 1; i <= workspacesShown; i++) {
result.push(i);
}
return result;
}
}
readonly property int minWorkspaceId: displayedWorkspaceIds.length > 0 ? displayedWorkspaceIds[0] : 1
readonly property int maxWorkspaceId: displayedWorkspaceIds.length > 0 ? displayedWorkspaceIds[displayedWorkspaceIds.length - 1] : workspacesShown
readonly property int displayWorkspaceCount: displayedWorkspaceIds.length
readonly property int effectiveColumns: SettingsData.overviewColumns
function getWorkspaceMonitorName(workspaceId) {
if (!allWorkspaces || !workspaceId)
return "";
try {
const ws = allWorkspaces.find(w => w?.id === workspaceId);
return ws?.monitor?.name ?? "";
} catch (e) {
return "";
}
}
function workspaceHasWindows(workspaceId) {
if (!workspaceId)
return false;
try {
const workspace = allWorkspaces.find(ws => ws?.id === workspaceId);
if (!workspace)
return false;
const toplevels = workspace?.toplevels?.values || [];
return toplevels.length > 0;
} catch (e) {
return false;
}
}
function monitorIpcForWorkspace(workspaceId) {
const workspace = allWorkspaces?.find(ws => ws?.id === workspaceId);
return workspace?.monitor?.lastIpcObject ?? monitor?.lastIpcObject ?? null;
}
function monitorLogicalSize(ipc) {
if (!ipc || !ipc.width || !ipc.height)
return {
"width": monitorPhysicalWidth,
"height": monitorPhysicalHeight
};
const monScale = ipc.scale > 0 ? ipc.scale : 1;
const rotated = ((ipc.transform ?? 0) % 2) === 1;
return {
"width": (rotated ? ipc.height : ipc.width) / monScale,
"height": (rotated ? ipc.width : ipc.height) / monScale
};
}
function cellForWorkspace(workspaceId) {
const cell = gridLayout.cells.find(c => c.id === workspaceId);
if (cell)
return cell;
return {
"id": workspaceId,
"x": 0,
"y": 0,
"width": workspaceImplicitWidth,
"height": workspaceImplicitHeight
};
}
function getWorkspaceViewportBounds(workspaceId, cellWidth, cellHeight) {
const ipc = monitorIpcForWorkspace(workspaceId) ?? {};
const logical = monitorLogicalSize(ipc);
const reserved = ipc.reserved || [0, 0, 0, 0];
const x = (ipc.x ?? 0) + (reserved[0] ?? 0);
const y = (ipc.y ?? 0) + (reserved[1] ?? 0);
const width = Math.max(logical.width - (reserved[0] ?? 0) - (reserved[2] ?? 0), 1);
const height = Math.max(logical.height - (reserved[1] ?? 0) - (reserved[3] ?? 0), 1);
return {
"x": x,
"y": y,
"scale": Math.min(cellWidth / width, cellHeight / height)
};
}
property bool monitorIsFocused: monitor?.focused ?? false
property real scale: SettingsData.overviewScale
property color activeBorderColor: Theme.primary
readonly property real monitorPhysicalWidth: panelWindow.screen ? (panelWindow.screen.width / root.dpr) : (monitor?.width ?? 1920)
readonly property real monitorPhysicalHeight: panelWindow.screen ? (panelWindow.screen.height / root.dpr) : (monitor?.height ?? 1080)
property real workspaceImplicitWidth: monitorPhysicalWidth * root.scale
property real workspaceImplicitHeight: monitorPhysicalHeight * root.scale
property int workspaceZ: 0
property int windowZ: 1
property int monitorLabelZ: 2
property int windowDraggingZ: 99999
property real workspaceSpacing: 5
property int draggingFromWorkspace: -1
property int draggingTargetWorkspace: -1
readonly property var gridLayout: {
const ids = displayedWorkspaceIds;
const columns = effectiveColumns;
if (!ids || ids.length === 0 || columns < 1)
return {
"cells": [],
"width": 0,
"height": 0
};
const rows = [];
for (let i = 0; i < ids.length; i += columns) {
rows.push(ids.slice(i, i + columns).map(id => {
const logical = monitorLogicalSize(monitorIpcForWorkspace(id));
return {
"id": id,
"width": logical.width * scale,
"height": logical.height * scale
};
}));
}
const rowWidth = row => row.reduce((acc, cell) => acc + cell.width, 0) + workspaceSpacing * (row.length - 1);
const totalWidth = rows.reduce((acc, row) => Math.max(acc, rowWidth(row)), 0);
const cells = [];
let cursorY = 0;
for (const row of rows) {
const rowHeight = row.reduce((acc, cell) => Math.max(acc, cell.height), 0);
let cursorX = (totalWidth - rowWidth(row)) / 2;
for (const cell of row) {
cells.push({
"id": cell.id,
"x": cursorX,
"y": cursorY + (rowHeight - cell.height) / 2,
"width": cell.width,
"height": cell.height
});
cursorX += cell.width + workspaceSpacing;
}
cursorY += rowHeight + workspaceSpacing;
}
return {
"cells": cells,
"width": totalWidth,
"height": cursorY - workspaceSpacing
};
}
implicitWidth: overviewBackground.implicitWidth + Theme.spacingL * 2
implicitHeight: overviewBackground.implicitHeight + Theme.spacingL * 2
Component.onCompleted: {
Hyprland.refreshToplevels();
Hyprland.refreshWorkspaces();
Hyprland.refreshMonitors();
}
onOverviewOpenChanged: {
if (overviewOpen) {
Hyprland.refreshToplevels();
Hyprland.refreshWorkspaces();
Hyprland.refreshMonitors();
}
}
Rectangle {
id: overviewBackground
property real padding: 10
anchors.fill: parent
anchors.margins: Theme.spacingL
implicitWidth: workspaceGrid.implicitWidth + padding * 2
implicitHeight: workspaceGrid.implicitHeight + padding * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainer
ElevationShadow {
anchors.fill: parent
z: -1
level: Theme.elevationLevel2
fallbackOffset: 4
targetRadius: Theme.cornerRadius
targetColor: Theme.surfaceContainer
shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25
shadowEnabled: Theme.elevationEnabled
}
Item {
id: workspaceGrid
z: root.workspaceZ
anchors.centerIn: parent
implicitWidth: root.gridLayout.width
implicitHeight: root.gridLayout.height
Repeater {
model: root.gridLayout.cells.length
Rectangle {
id: workspace
required property int index
readonly property var cell: root.gridLayout.cells[index] ?? null
property int workspaceValue: cell?.id ?? -1
property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false
property var workspaceObj: (workspaceExists && Hyprland.workspaces?.values) ? Hyprland.workspaces.values.find(ws => ws?.id === workspaceValue) : null
property bool isActive: workspaceObj?.active ?? false
property bool isOnThisMonitor: (workspaceObj && root.monitor) ? (workspaceObj.monitor?.name === root.monitor.name) : true
property bool hasWindows: (workspaceValue > 0) ? root.workspaceHasWindows(workspaceValue) : false
property color defaultWorkspaceColor: workspaceExists ? Theme.surfaceContainer : Theme.withAlpha(Theme.surfaceContainer, 0.3)
property color hoveredWorkspaceColor: Qt.lighter(defaultWorkspaceColor, 1.1)
property color hoveredBorderColor: Theme.surfaceVariant
property bool hoveredWhileDragging: false
property bool shouldShowActiveIndicator: isActive && isOnThisMonitor && hasWindows
visible: workspaceValue !== -1
x: cell?.x ?? 0
y: cell?.y ?? 0
width: cell?.width ?? 0
height: cell?.height ?? 0
color: hoveredWhileDragging ? hoveredWorkspaceColor : defaultWorkspaceColor
radius: Theme.cornerRadius
border.width: 2
border.color: hoveredWhileDragging ? hoveredBorderColor : (shouldShowActiveIndicator ? root.activeBorderColor : Theme.withAlpha(root.activeBorderColor, 0))
StyledText {
anchors.centerIn: parent
text: workspace.workspaceValue
font.pixelSize: Theme.fontSizeXLarge * 6
font.weight: Font.DemiBold
color: Theme.withAlpha(Theme.surfaceText, workspace.workspaceExists ? 0.2 : 0.1)
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
MouseArea {
id: workspaceArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: {
if (root.draggingTargetWorkspace === -1) {
root.overviewOpen = false;
HyprlandService.focusWorkspace(workspace.workspaceValue);
}
}
}
DropArea {
anchors.fill: parent
onEntered: {
root.draggingTargetWorkspace = workspace.workspaceValue;
if (root.draggingFromWorkspace == root.draggingTargetWorkspace)
return;
workspace.hoveredWhileDragging = true;
}
onExited: {
workspace.hoveredWhileDragging = false;
if (root.draggingTargetWorkspace == workspace.workspaceValue)
root.draggingTargetWorkspace = -1;
}
}
}
}
}
Item {
id: windowSpace
anchors.centerIn: parent
implicitWidth: workspaceGrid.implicitWidth
implicitHeight: workspaceGrid.implicitHeight
Repeater {
model: ScriptModel {
values: {
const workspaces = root.allWorkspaces;
const minId = root.minWorkspaceId;
const maxId = root.maxWorkspaceId;
if (!workspaces || workspaces.length === 0)
return [];
try {
const result = [];
for (const workspace of workspaces) {
const wsId = workspace?.id ?? -1;
if (wsId >= minId && wsId <= maxId) {
const toplevels = workspace?.toplevels?.values || [];
for (const toplevel of toplevels) {
result.push(toplevel);
}
}
}
return result;
} catch (e) {
log.error("OverviewWidget filter error:", e);
return [];
}
}
}
delegate: OverviewWindow {
id: window
required property var modelData
overviewOpen: root.overviewOpen
readonly property int windowWorkspaceId: modelData?.workspace?.id ?? -1
readonly property var workspaceCell: root.cellForWorkspace(windowWorkspaceId)
readonly property var workspaceBounds: root.getWorkspaceViewportBounds(windowWorkspaceId, workspaceCell.width, workspaceCell.height)
toplevel: modelData
scale: root.scale
monitorDpr: root.dpr
availableWorkspaceWidth: workspaceCell.width
availableWorkspaceHeight: workspaceCell.height
contentOriginX: workspaceBounds.x
contentOriginY: workspaceBounds.y
contentScale: workspaceBounds.scale
widgetMonitorId: root.monitor.id
xOffset: workspaceCell.x
yOffset: workspaceCell.y
z: atInitPosition ? root.windowZ : root.windowDraggingZ
property bool atInitPosition: (initX == x && initY == y)
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
MouseArea {
id: dragArea
anchors.fill: parent
hoverEnabled: true
onEntered: window.hovered = true
onExited: window.hovered = false
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
drag.target: parent
onPressed: mouse => {
root.draggingFromWorkspace = windowData?.workspace.id;
window.pressed = true;
window.Drag.active = true;
window.Drag.source = window;
window.Drag.hotSpot.x = mouse.x;
window.Drag.hotSpot.y = mouse.y;
}
onReleased: {
const targetWorkspace = root.draggingTargetWorkspace;
window.pressed = false;
window.Drag.active = false;
root.draggingFromWorkspace = -1;
root.draggingTargetWorkspace = -1;
if (targetWorkspace !== -1 && targetWorkspace !== windowData?.workspace.id) {
HyprlandService.moveToWorkspace(targetWorkspace, windowData?.address, false);
Qt.callLater(() => {
Hyprland.refreshToplevels();
Hyprland.refreshWorkspaces();
Qt.callLater(() => {
window.x = window.initX;
window.y = window.initY;
});
});
} else {
window.x = window.initX;
window.y = window.initY;
}
}
onClicked: event => {
if (!windowData || !windowData.address)
return;
if (event.button === Qt.LeftButton) {
root.overviewOpen = false;
HyprlandService.focusWindow(windowData.address);
event.accepted = true;
} else if (event.button === Qt.MiddleButton) {
HyprlandService.closeWindow(windowData.address);
event.accepted = true;
}
}
}
}
}
}
Item {
id: monitorLabelSpace
anchors.centerIn: parent
implicitWidth: workspaceGrid.implicitWidth
implicitHeight: workspaceGrid.implicitHeight
z: root.monitorLabelZ
Repeater {
model: root.gridLayout.cells.length
delegate: Item {
id: labelItem
required property int index
readonly property var cell: root.gridLayout.cells[index] ?? null
property int workspaceValue: cell?.id ?? -1
property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false
property string workspaceMonitorName: (workspaceValue > 0) ? root.getWorkspaceMonitorName(workspaceValue) : ""
x: cell?.x ?? 0
y: cell?.y ?? 0
width: cell?.width ?? 0
height: cell?.height ?? 0
Rectangle {
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingS
width: monitorNameText.contentWidth + Theme.spacingS * 2
height: monitorNameText.contentHeight + Theme.spacingXS * 2
radius: Theme.cornerRadius
color: Theme.surface
visible: labelItem.workspaceExists && labelItem.workspaceMonitorName !== ""
StyledText {
id: monitorNameText
anchors.centerIn: parent
text: labelItem.workspaceMonitorName
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
}
}
}