window switcher start

This commit is contained in:
Zacharias-Brohn
2026-01-27 14:16:23 +01:00
parent b41b8467bf
commit 159920eb99
14 changed files with 232 additions and 767 deletions
+20 -4
View File
@@ -52,11 +52,14 @@ Scope {
x: 0 x: 0
y: 34 y: 34
width: bar.width property list<Region> nullRegions: []
height: bar.screen.height - backgroundRect.implicitHeight property bool hcurrent: panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu")
width: hcurrent ? 0 : bar.width
height: hcurrent ? 0 : bar.screen.height - backgroundRect.implicitHeight
intersection: Intersection.Xor intersection: Intersection.Xor
regions: panels.popouts.hasCurrent ? None : popoutRegions.instances regions: hcurrent ? nullRegions : popoutRegions.instances
} }
Variants { Variants {
@@ -69,7 +72,7 @@ Scope {
x: modelData.x x: modelData.x
y: modelData.y + backgroundRect.implicitHeight y: modelData.y + backgroundRect.implicitHeight
width: modelData.width width: modelData.width
height: panels.popouts.hasCurrent ? modelData.height + 70 : 0 height: panels.popouts.hasCurrent ? modelData.height : 0
intersection: Intersection.Subtract intersection: Intersection.Subtract
} }
} }
@@ -110,6 +113,19 @@ Scope {
} }
} }
onPressed: event => {
var withinX = mouseX >= panels.popouts.x + 8 && mouseX < panels.popouts.x + panels.popouts.implicitWidth;
var withinY = mouseY >= panels.popouts.y + exclusionZone.implicitHeight && mouseY < panels.popouts.y + exclusionZone.implicitHeight + panels.popouts.implicitHeight;
if ( panels.popouts.hasCurrent ) {
if ( withinX && withinY ) {
} else {
panels.popouts.hasCurrent = false;
}
}
}
Panels { Panels {
id: panels id: panels
screen: bar.modelData screen: bar.modelData
+76
View File
@@ -0,0 +1,76 @@
import QtQuick
import QtQuick.Templates
import qs.Config
import qs.Modules
Slider {
id: root
required property real peak
background: Item {
CustomRect {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.topMargin: root.implicitHeight / 3
anchors.bottomMargin: root.implicitHeight / 3
implicitWidth: root.handle.x - root.implicitHeight / 6
color: DynamicColors.palette.m3primaryContainer
radius: 1000
topRightRadius: root.implicitHeight / 15
bottomRightRadius: root.implicitHeight / 15
CustomRect {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
implicitWidth: parent.width * root.peak
radius: 1000
topRightRadius: root.implicitHeight / 15
bottomRightRadius: root.implicitHeight / 15
color: DynamicColors.palette.m3primary
Behavior on implicitWidth {
Anim { duration: 50 }
}
}
}
CustomRect {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.topMargin: root.implicitHeight / 3
anchors.bottomMargin: root.implicitHeight / 3
implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 6
color: DynamicColors.tPalette.m3surfaceContainer
radius: 1000
topLeftRadius: root.implicitHeight / 15
bottomLeftRadius: root.implicitHeight / 15
}
}
handle: CustomRect {
x: root.visualPosition * root.availableWidth - implicitWidth / 2
implicitWidth: 5
implicitHeight: 15
anchors.verticalCenter: parent.verticalCenter
color: DynamicColors.palette.m3primary
radius: 1000
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: Qt.PointingHandCursor
}
}
}
+7 -1
View File
@@ -339,6 +339,11 @@ Item {
objects: appBox.modelData ? [appBox.modelData] : [] objects: appBox.modelData ? [appBox.modelData] : []
} }
PwNodePeakMonitor {
id: peak
node: appBox.modelData
}
readonly property bool isCaptureStream: { readonly property bool isCaptureStream: {
if (!modelData || !modelData.properties) if (!modelData || !modelData.properties)
return false; return false;
@@ -409,9 +414,10 @@ Item {
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.bottomMargin: 5 Layout.bottomMargin: 5
implicitHeight: 10 implicitHeight: 10
CustomSlider { CustomAudioSlider {
anchors.fill: parent anchors.fill: parent
value: appBox.modelData.audio.volume value: appBox.modelData.audio.volume
peak: peak.peak
onMoved: { onMoved: {
Audio.setAppAudioVolume(appBox.modelData, value) Audio.setAppAudioVolume(appBox.modelData, value)
console.log(icon.iconPath1, icon.iconPath2) console.log(icon.iconPath1, icon.iconPath2)
+24 -5
View File
@@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import Quickshell.Hyprland
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import qs.Modules import qs.Modules
@@ -19,7 +20,7 @@ RowLayout {
function checkPopout(x: real): void { function checkPopout(x: real): void {
const ch = childAt(x, height / 2) as WrappedLoader; const ch = childAt(x, height / 2) as WrappedLoader;
if (!ch) { if (!ch && !popouts.currentName.includes("traymenu")) {
popouts.hasCurrent = false; popouts.hasCurrent = false;
return; return;
} }
@@ -42,11 +43,11 @@ RowLayout {
const index = Math.floor((( x - top ) / item.implicitWidth ) * item.items.count ); const index = Math.floor((( x - top ) / item.implicitWidth ) * item.items.count );
const trayItem = item.items.itemAt( index ); const trayItem = item.items.itemAt( index );
if ( trayItem ) { if ( trayItem ) {
popouts.currentName = `traymenu${ index }`; // popouts.currentName = `traymenu${ index }`;
popouts.currentCenter = Qt.binding( () => trayItem.mapToItem( root, trayItem.implicitWidth / 2, 0 ).x ); // popouts.currentCenter = Qt.binding( () => trayItem.mapToItem( root, trayItem.implicitWidth / 2, 0 ).x );
popouts.hasCurrent = true; // popouts.hasCurrent = true;
} else { } else {
popouts.hasCurrent = false; // popouts.hasCurrent = false;
} }
} else if ( id === "clock" && Config.barConfig.popouts.clock ) { } else if ( id === "clock" && Config.barConfig.popouts.clock ) {
Calendar.displayYear = new Date().getFullYear(); Calendar.displayYear = new Date().getFullYear();
@@ -57,6 +58,23 @@ RowLayout {
} }
} }
GlobalShortcut {
name: "toggle-overview"
appid: "zshell"
onPressed: {
Hyprland.refreshWorkspaces();
Hyprland.refreshMonitors();
if ( root.popouts.hasCurrent && root.popouts.currentName === "overview" ) {
root.popouts.hasCurrent = false;
} else {
root.popouts.currentName = "overview";
root.popouts.currentCenter = root.width / 2;
root.popouts.hasCurrent = true;
}
}
}
Repeater { Repeater {
id: repeater id: repeater
model: Config.barConfig.entries model: Config.barConfig.entries
@@ -90,6 +108,7 @@ RowLayout {
sourceComponent: TrayWidget { sourceComponent: TrayWidget {
bar: root.bar bar: root.bar
popouts: root.popouts popouts: root.popouts
loader: root
} }
} }
} }
+10
View File
@@ -5,6 +5,7 @@ import Quickshell.Services.SystemTray
import QtQuick import QtQuick
import qs.Config import qs.Config
import qs.Modules.Calendar import qs.Modules.Calendar
import qs.Modules.WSOverview
Item { Item {
id: root id: root
@@ -77,6 +78,15 @@ Item {
wrapper: root.wrapper wrapper: root.wrapper
} }
} }
Popout {
name: "overview"
sourceComponent: OverviewPopout {
wrapper: root.wrapper
screen: root.wrapper.screen
}
}
} }
component Popout: Loader { component Popout: Loader {
+8 -3
View File
@@ -1,11 +1,13 @@
import QtQuick.Layouts
import QtQuick.Effects
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import Quickshell.Io import Quickshell.Io
import Quickshell.Widgets import Quickshell.Widgets
import qs.Modules import qs.Modules
import qs.Components
import qs.Config import qs.Config
import QtQuick.Effects
Item { Item {
id: root id: root
@@ -14,17 +16,20 @@ Item {
required property PanelWindow bar required property PanelWindow bar
required property int ind required property int ind
required property Wrapper popouts required property Wrapper popouts
required property RowLayout loader
property bool hasLoaded: false property bool hasLoaded: false
MouseArea { StateLayer {
anchors.fill: parent anchors.fill: parent
anchors.margins: 3
radius: 6
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: { onClicked: {
if ( mouse.button === Qt.LeftButton ) { if ( mouse.button === Qt.LeftButton ) {
root.item.activate(); root.item.activate();
} else if ( mouse.button === Qt.RightButton ) { } else if ( mouse.button === Qt.RightButton ) {
root.popouts.currentName = `traymenu${ root.ind }`; root.popouts.currentName = `traymenu${ root.ind }`;
root.popouts.currentCenter = Qt.binding( () => root.mapToItem( root.bar, root.implicitWidth / 2, 0 ).x ); root.popouts.currentCenter = Qt.binding( () => root.mapToItem( root.loader, root.implicitWidth / 2, 0 ).x );
root.popouts.hasCurrent = true; root.popouts.hasCurrent = true;
} }
} }
+3 -1
View File
@@ -13,6 +13,7 @@ Row {
required property PanelWindow bar required property PanelWindow bar
required property Wrapper popouts required property Wrapper popouts
required property RowLayout loader
readonly property alias items: repeater readonly property alias items: repeater
spacing: 0 spacing: 0
@@ -26,8 +27,9 @@ Row {
required property int index required property int index
ind: index ind: index
popouts: root.popouts popouts: root.popouts
loader: root.loader
implicitHeight: 34 implicitHeight: 34
implicitWidth: 28 implicitWidth: 34
item: modelData item: modelData
bar: root.bar bar: root.bar
} }
-68
View File
@@ -1,68 +0,0 @@
pragma Singleton
import Quickshell
Singleton {
id: root
function colorWithHueOf(color1, color2) {
var c1 = Qt.color(color1);
var c2 = Qt.color(color2);
var hue = c2.hsvHue;
var sat = c1.hsvSaturation;
var val = c1.hsvValue;
var alpha = c1.a;
return Qt.hsva(hue, sat, val, alpha);
}
function colorWithSaturationOf(color1, color2) {
var c1 = Qt.color(color1);
var c2 = Qt.color(color2);
var hue = c1.hsvHue;
var sat = c2.hsvSaturation;
var val = c1.hsvValue;
var alpha = c1.a;
return Qt.hsva(hue, sat, val, alpha);
}
function colorWithLightness(color, lightness) {
var c = Qt.color(color);
return Qt.hsla(c.hslHue, c.hslSaturation, lightness, c.a);
}
function colorWithLightnessOf(color1, color2) {
var c2 = Qt.color(color2);
return colorWithLightness(color1, c2.hslLightness);
}
function adaptToAccent(color1, color2) {
var c1 = Qt.color(color1);
var c2 = Qt.color(color2);
var hue = c2.hslHue;
var sat = c2.hslSaturation;
var light = c1.hslLightness;
var alpha = c1.a;
return Qt.hsla(hue, sat, light, alpha);
}
function mix(color1, color2, percentage = 0.5) {
var c1 = Qt.color(color1);
var c2 = Qt.color(color2);
return Qt.rgba(
percentage * c1.r + (1 - percentage) * c2.r,
percentage * c1.g + (1 - percentage) * c2.g,
percentage * c1.b + (1 - percentage) * c2.b,
percentage * c1.a + (1 - percentage) * c2.a
);
}
function transparentize(color, percentage = 1) {
var c = Qt.color(color);
return Qt.rgba(c.r, c.g, c.b, c.a * (1 - percentage));
}
function applyAlpha(color, alpha) {
var c = Qt.color(color);
var a = Math.max(0, Math.min(1, alpha));
return Qt.rgba(c.r, c.g, c.b, a);
}
}
-134
View File
@@ -1,134 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
Singleton {
id: root
property var windowList: []
property var addresses: []
property var windowByAddress: ({})
property var workspaces: []
property var workspaceIds: []
property var workspaceById: ({})
property var activeWorkspace: null
property var monitors: []
property var layers: ({})
function updateWindowList() {
getClients.running = true;
}
function updateLayers() {
getLayers.running = true;
}
function updateMonitors() {
getMonitors.running = true;
}
function updateWorkspaces() {
getWorkspaces.running = true;
getActiveWorkspace.running = true;
}
function updateAll() {
updateWindowList();
updateMonitors();
updateLayers();
updateWorkspaces();
}
function biggestWindowForWorkspace(workspaceId) {
const windowsInThisWorkspace = HyprlandData.windowList.filter(w => w.workspace.id == workspaceId);
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);
}
Component.onCompleted: {
updateAll();
}
Connections {
target: Hyprland
function onRawEvent(event) {
updateAll()
}
}
Process {
id: getClients
command: ["hyprctl", "clients", "-j"]
stdout: StdioCollector {
id: clientsCollector
onStreamFinished: {
root.windowList = JSON.parse(clientsCollector.text)
let tempWinByAddress = {};
for (var i = 0; i < root.windowList.length; ++i) {
var win = root.windowList[i];
tempWinByAddress[win.address] = win;
}
root.windowByAddress = tempWinByAddress;
root.addresses = root.windowList.map(win => win.address);
}
}
}
Process {
id: getMonitors
command: ["hyprctl", "monitors", "-j"]
stdout: StdioCollector {
id: monitorsCollector
onStreamFinished: {
root.monitors = JSON.parse(monitorsCollector.text);
}
}
}
Process {
id: getLayers
command: ["hyprctl", "layers", "-j"]
stdout: StdioCollector {
id: layersCollector
onStreamFinished: {
root.layers = JSON.parse(layersCollector.text);
}
}
}
Process {
id: getWorkspaces
command: ["hyprctl", "workspaces", "-j"]
stdout: StdioCollector {
id: workspacesCollector
onStreamFinished: {
root.workspaces = JSON.parse(workspacesCollector.text);
let tempWorkspaceById = {};
for (var i = 0; i < root.workspaces.length; ++i) {
var ws = root.workspaces[i];
tempWorkspaceById[ws.id] = ws;
}
root.workspaceById = tempWorkspaceById;
root.workspaceIds = root.workspaces.map(ws => ws.id);
}
}
}
Process {
id: getActiveWorkspace
command: ["hyprctl", "activeworkspace", "-j"]
stdout: StdioCollector {
id: activeWorkspaceCollector
onStreamFinished: {
root.activeWorkspace = JSON.parse(activeWorkspaceCollector.text);
}
}
}
}
-147
View File
@@ -1,147 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Config
Scope {
id: overviewScope
Variants {
id: overviewVariants
model: Quickshell.screens
PanelWindow {
id: root
required property var modelData
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id)
screen: modelData
visible: false
WlrLayershell.namespace: "quickshell:overview"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
color: "transparent"
mask: Region {
item: keyHandler
}
anchors {
top: true
bottom: true
left: true
right: true
}
HyprlandFocusGrab {
id: grab
windows: [root]
property bool canBeActive: root.monitorIsFocused
active: false
onCleared: () => {
if (!active)
root.visible = false;
}
}
implicitWidth: columnLayout.implicitWidth
implicitHeight: columnLayout.implicitHeight
Item {
id: keyHandler
anchors.fill: parent
visible: root.visible
focus: root.visible
Keys.onPressed: event => {
// close: Escape or Enter
if (event.key === Qt.Key_Escape || event.key === Qt.Key_Return) {
root.visible = false;
event.accepted = true;
return;
}
// Helper: compute current group bounds
const workspacesPerGroup = Config.overview.rows * Config.overview.columns;
const currentId = Hyprland.focusedMonitor?.activeWorkspace?.id ?? 1;
const currentGroup = Math.floor((currentId - 1) / workspacesPerGroup);
const minWorkspaceId = currentGroup * workspacesPerGroup + 1;
const maxWorkspaceId = minWorkspaceId + workspacesPerGroup - 1;
let targetId = null;
// Arrow keys and vim-style hjkl
if (event.key === Qt.Key_Left || event.key === Qt.Key_H) {
targetId = currentId - 1;
if (targetId < minWorkspaceId) targetId = maxWorkspaceId;
} else if (event.key === Qt.Key_Right || event.key === Qt.Key_L) {
targetId = currentId + 1;
if (targetId > maxWorkspaceId) targetId = minWorkspaceId;
} else if (event.key === Qt.Key_Up || event.key === Qt.Key_K) {
targetId = currentId - Config.overview.columns;
if (targetId < minWorkspaceId) targetId += workspacesPerGroup;
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_J) {
targetId = currentId + Config.overview.columns;
if (targetId > maxWorkspaceId) targetId -= workspacesPerGroup;
}
// Number keys: jump to workspace within the current group
// 1-9 map to positions 1-9, 0 maps to position 10
else if (event.key >= Qt.Key_1 && event.key <= Qt.Key_9) {
const position = event.key - Qt.Key_0; // 1-9
if (position <= workspacesPerGroup) {
targetId = minWorkspaceId + position - 1;
}
} else if (event.key === Qt.Key_0) {
// 0 = 10th workspace in the group (if group has 10+ workspaces)
if (workspacesPerGroup >= 10) {
targetId = minWorkspaceId + 9; // 10th position = offset 9
}
}
if (targetId !== null) {
Hyprland.dispatch("workspace " + targetId);
event.accepted = true;
}
}
}
ColumnLayout {
id: columnLayout
visible: root.visible
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: 100
}
Loader {
id: overviewLoader
active: true && (Config.overview.enable ?? true)
sourceComponent: OverviewWidget {
panelWindow: root
visible: true
}
}
}
IpcHandler {
target: "overview"
function toggle() {
root.visible = !root.visible;
}
function close() {
root.visible = false;
}
function open() {
root.visible = true;
}
}
}
}
}
+84
View File
@@ -0,0 +1,84 @@
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import QtQuick
import QtQuick.Layouts
import qs.Config
import qs.Components
import qs.Modules
import qs.Helpers
Item {
id: root
required property Item wrapper
required property ShellScreen screen
implicitWidth: layout.implicitWidth + 16
implicitHeight: layout.implicitHeight + 16
GridLayout {
id: layout
anchors.centerIn: parent
columnSpacing: 8
rowSpacing: 8
Repeater {
model: Hypr.workspaces
CustomRect {
id: workspacePreview
required property HyprlandWorkspace modelData
border.color: "white"
border.width: 1
radius: 8
Layout.preferredWidth: 320 + 10
Layout.preferredHeight: 180 + 10
Repeater {
model: workspacePreview.modelData.toplevels
Item {
id: preview
anchors.fill: parent
anchors.margins: 5
required property HyprlandToplevel modelData
property rect appPosition: {
let {
at: [cx, cy],
size: [cw, ch]
} = modelData.lastIpcObject;
cx -= modelData.monitor.x;
cy -= modelData.monitor.y;
return Qt.rect( (cx / 8), (cy / 8), (cw / 8), (ch / 8) )
}
CustomRect {
border.color: DynamicColors.tPalette.m3outline
border.width: 1
radius: 4
implicitWidth: preview.appPosition.width
implicitHeight: preview.appPosition.height
x: preview.appPosition.x
y: preview.appPosition.y - 3.4
ScreencopyView {
id: previewCopy
anchors.fill: parent
captureSource: preview.modelData.wayland
live: true
}
}
}
}
}
}
}
}
-298
View File
@@ -1,298 +0,0 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Config
import qs.Modules
import qs.Components
import qs.Effects
Item {
id: root
required property var panelWindow
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen)
readonly property var toplevels: ToplevelManager.toplevels
readonly property int workspacesShown: Config.overview.rows * Config.overview.columns
readonly property int workspaceGroup: Math.floor((monitor.activeWorkspace?.id - 1) / workspacesShown)
property bool monitorIsFocused: (Hyprland.focusedMonitor?.name == monitor.name)
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)
property real scale: Config.overview.scale
property color activeBorderColor: Appearance.colors.colSecondary
property real workspaceImplicitWidth: (monitorData?.transform % 2 === 1) ?
((monitor.height / monitor.scale - (monitorData?.reserved?.[0] ?? 0) - (monitorData?.reserved?.[2] ?? 0)) * root.scale) :
((monitor.width / monitor.scale - (monitorData?.reserved?.[0] ?? 0) - (monitorData?.reserved?.[2] ?? 0)) * root.scale)
property real workspaceImplicitHeight: (monitorData?.transform % 2 === 1) ?
((monitor.width / monitor.scale - (monitorData?.reserved?.[1] ?? 0) - (monitorData?.reserved?.[3] ?? 0)) * root.scale) :
((monitor.height / monitor.scale - (monitorData?.reserved?.[1] ?? 0) - (monitorData?.reserved?.[3] ?? 0)) * root.scale)
property real workspaceNumberMargin: 80
property real workspaceNumberSize: 250 * monitor.scale
property int workspaceZ: 0
property int windowZ: 1
property int windowDraggingZ: 99999
property real workspaceSpacing: 5
property int draggingFromWorkspace: -1
property int draggingTargetWorkspace: -1
implicitWidth: overviewBackground.implicitWidth + Appearance.sizes.elevationMargin * 2
implicitHeight: overviewBackground.implicitHeight + Appearance.sizes.elevationMargin * 2
property Component windowComponent: OverviewWindow {}
property list<OverviewWindow> windowWidgets: []
ShadowRect {
anchors.fill: overviewBackground
}
Rectangle { // Background
id: overviewBackground
property real padding: 10
anchors.fill: parent
anchors.margins: Appearance.sizes.elevationMargin
implicitWidth: workspaceColumnLayout.implicitWidth + padding * 2
implicitHeight: workspaceColumnLayout.implicitHeight + padding * 2
radius: Appearance.rounding.screenRounding * root.scale + padding
color: Appearance.colors.colLayer0
border.width: 1
border.color: Appearance.colors.colLayer0Border
ColumnLayout { // Workspaces
id: workspaceColumnLayout
z: root.workspaceZ
anchors.centerIn: parent
spacing: workspaceSpacing
Repeater {
model: Config.overview.rows
delegate: RowLayout {
id: row
property int rowIndex: index
spacing: workspaceSpacing
Repeater { // Workspace repeater
model: Config.overview.columns
Rectangle { // Workspace
id: workspace
property int colIndex: index
property int workspaceValue: root.workspaceGroup * workspacesShown + rowIndex * Config.overview.columns + colIndex + 1
property color defaultWorkspaceColor: DynamicColors.tPalette.m3surfaceContainerLow
property color hoveredWorkspaceColor: ColorUtils.mix(defaultWorkspaceColor, Appearance.colors.colLayer1Hover, 0.1)
property color hoveredBorderColor: DynamicColors.tPalette.m3surfaceContainer
property bool hoveredWhileDragging: false
implicitWidth: root.workspaceImplicitWidth
implicitHeight: root.workspaceImplicitHeight
color: hoveredWhileDragging ? hoveredWorkspaceColor : defaultWorkspaceColor
radius: 8
border.width: 2
border.color: hoveredWhileDragging ? hoveredBorderColor : "transparent"
CustomText {
anchors.centerIn: parent
text: workspaceValue
color: DynamicColor.tPalette.m3onSurfaceVariant
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
MouseArea {
id: workspaceArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: {
if (root.draggingTargetWorkspace === -1) {
root.panelWindow.visible = false
Hyprland.dispatch(`workspace ${workspaceValue}`)
}
}
}
DropArea {
anchors.fill: parent
onEntered: {
root.draggingTargetWorkspace = workspaceValue
if (root.draggingFromWorkspace == root.draggingTargetWorkspace) return;
hoveredWhileDragging = true
}
onExited: {
hoveredWhileDragging = false
if (root.draggingTargetWorkspace == workspaceValue) root.draggingTargetWorkspace = -1
}
}
}
}
}
}
}
Item { // Windows & focused workspace indicator
id: windowSpace
anchors.centerIn: parent
implicitWidth: workspaceColumnLayout.implicitWidth
implicitHeight: workspaceColumnLayout.implicitHeight
Repeater { // Window repeater
model: ScriptModel {
values: {
return ToplevelManager.toplevels.values.filter((toplevel) => {
const address = `0x${toplevel.HyprlandToplevel.address}`
var win = windowByAddress[address]
const inWorkspaceGroup = (root.workspaceGroup * root.workspacesShown < win?.workspace?.id && win?.workspace?.id <= (root.workspaceGroup + 1) * root.workspacesShown)
return inWorkspaceGroup;
}).sort((a, b) => {
// Proper stacking order based on Hyprland's window properties
const addrA = `0x${a.HyprlandToplevel.address}`
const addrB = `0x${b.HyprlandToplevel.address}`
const winA = windowByAddress[addrA]
const winB = windowByAddress[addrB]
// 1. Pinned windows are always on top
if (winA?.pinned !== winB?.pinned) {
return winA?.pinned ? 1 : -1
}
// 2. Floating windows above tiled windows
if (winA?.floating !== winB?.floating) {
return winA?.floating ? 1 : -1
}
// 3. Within same category, sort by focus history
// Lower focusHistoryID = more recently focused = higher in stack
return (winB?.focusHistoryID ?? 0) - (winA?.focusHistoryID ?? 0)
})
}
}
delegate: OverviewWindow {
id: window
required property var modelData
required property int index
property var overviewRoot: root.panelWindow
property int monitorId: windowData?.monitor
property var monitor: HyprlandData.monitors.find(m => m.id === monitorId)
property var address: `0x${modelData.HyprlandToplevel.address}`
windowData: windowByAddress[address]
toplevel: modelData
monitorData: monitor
// Calculate scale relative to window's source monitor
property real sourceMonitorWidth: (monitor?.transform % 2 === 1) ?
(monitor?.height ?? 1920) / (monitor?.scale ?? 1) - (monitor?.reserved?.[0] ?? 0) - (monitor?.reserved?.[2] ?? 0) :
(monitor?.width ?? 1920) / (monitor?.scale ?? 1) - (monitor?.reserved?.[0] ?? 0) - (monitor?.reserved?.[2] ?? 0)
property real sourceMonitorHeight: (monitor?.transform % 2 === 1) ?
(monitor?.width ?? 1080) / (monitor?.scale ?? 1) - (monitor?.reserved?.[1] ?? 0) - (monitor?.reserved?.[3] ?? 0) :
(monitor?.height ?? 1080) / (monitor?.scale ?? 1) - (monitor?.reserved?.[1] ?? 0) - (monitor?.reserved?.[3] ?? 0)
// Scale windows to fit the workspace size, accounting for different monitor sizes
scale: Math.min(
root.workspaceImplicitWidth / sourceMonitorWidth,
root.workspaceImplicitHeight / sourceMonitorHeight
)
availableWorkspaceWidth: root.workspaceImplicitWidth
availableWorkspaceHeight: root.workspaceImplicitHeight
widgetMonitorId: root.monitor.id
property bool atInitPosition: (initX == x && initY == y)
property int workspaceColIndex: (windowData?.workspace.id - 1) % Config.overview.columns
property int workspaceRowIndex: Math.floor((windowData?.workspace.id - 1) % root.workspacesShown / Config.overview.columns)
xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex
yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex
Timer {
id: updateWindowPosition
interval: Config.options.hacks.arbitraryRaceConditionDelay
repeat: false
running: false
onTriggered: {
window.x = Math.round(Math.max((windowData?.at[0] - (monitor?.x ?? 0) - (monitorData?.reserved?.[0] ?? 0)) * root.scale, 0) + xOffset)
window.y = Math.round(Math.max((windowData?.at[1] - (monitor?.y ?? 0) - (monitorData?.reserved?.[1] ?? 0)) * root.scale, 0) + yOffset)
}
}
z: atInitPosition ? (root.windowZ + index) : root.windowDraggingZ
Drag.hotSpot.x: targetWindowWidth / 2
Drag.hotSpot.y: targetWindowHeight / 2
MouseArea {
id: dragArea
anchors.fill: parent
hoverEnabled: true
onEntered: hovered = true
onExited: 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
if (targetWorkspace !== -1 && targetWorkspace !== windowData?.workspace.id) {
Hyprland.dispatch(`movetoworkspacesilent ${targetWorkspace}, address:${window.windowData?.address}`)
updateWindowPosition.restart()
}
else {
window.x = window.initX
window.y = window.initY
}
}
onClicked: (event) => {
if (!windowData) return;
if (event.button === Qt.LeftButton) {
GlobalStates.overviewOpen = false
Hyprland.dispatch(`focuswindow address:${windowData.address}`)
event.accepted = true
} else if (event.button === Qt.MiddleButton) {
Hyprland.dispatch(`closewindow address:${windowData.address}`)
event.accepted = true
}
}
CustomTooltip {
extraVisibleCondition: false
alternativeVisibleCondition: dragArea.containsMouse && !window.Drag.active
text: `${windowData?.title ?? "Unknown"}\n[${windowData?.class ?? "unknown"}] ${windowData?.xwayland ? "[XWayland] " : ""}`
}
}
}
}
Rectangle {
id: focusedWorkspaceIndicator
property int activeWorkspaceInGroup: monitor.activeWorkspace?.id - (root.workspaceGroup * root.workspacesShown)
property int activeWorkspaceRowIndex: Math.floor((activeWorkspaceInGroup - 1) / Config.overview.columns)
property int activeWorkspaceColIndex: (activeWorkspaceInGroup - 1) % Config.overview.columns
x: (root.workspaceImplicitWidth + workspaceSpacing) * activeWorkspaceColIndex
y: (root.workspaceImplicitHeight + workspaceSpacing) * activeWorkspaceRowIndex
z: root.windowZ
width: root.workspaceImplicitWidth
height: root.workspaceImplicitHeight
color: "transparent"
radius: Appearance.rounding.screenRounding * root.scale
border.width: 2
border.color: root.activeBorderColor
Behavior on x {
Anim {}
}
Behavior on y {
Anim {}
}
}
}
}
}
-103
View File
@@ -1,103 +0,0 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Config
import qs.Modules
Item { // Window
id: root
required property var overviewRoot
property var toplevel
property var windowData
property var monitorData
property var scale
property var availableWorkspaceWidth
property var availableWorkspaceHeight
property bool restrictToWorkspace: true
property real initX: Math.max(((windowData?.at[0] ?? 0) - (monitorData?.x ?? 0) - (monitorData?.reserved?.[0] ?? 0)) * root.scale, 0) + xOffset
property real initY: Math.max(((windowData?.at[1] ?? 0) - (monitorData?.y ?? 0) - (monitorData?.reserved?.[1] ?? 0)) * root.scale, 0) + yOffset
property real xOffset: 0
property real yOffset: 0
property int widgetMonitorId: 0
property var targetWindowWidth: (windowData?.size[0] ?? 100) * scale
property var targetWindowHeight: (windowData?.size[1] ?? 100) * scale
property bool hovered: false
property bool pressed: false
property var iconToWindowRatio: 0.25
property var xwaylandIndicatorToIconRatio: 0.35
property var iconToWindowRatioCompact: 0.45
property var entry: DesktopEntries.heuristicLookup(windowData?.class)
property var iconPath: Quickshell.iconPath(entry?.icon ?? windowData?.class ?? "application-x-executable", "image-missing")
property bool compactMode: false
property bool indicateXWayland: windowData?.xwayland ?? false
x: initX
y: initY
width: Math.min((windowData?.size[0] ?? 100) * root.scale, availableWorkspaceWidth)
height: Math.min((windowData?.size[1] ?? 100) * root.scale, availableWorkspaceHeight)
opacity: (windowData?.monitor ?? -1) == widgetMonitorId ? 1 : 0.4
clip: true
Behavior on x {
Anim {}
}
Behavior on y {
Anim {}
}
Behavior on width {
Anim {}
}
Behavior on height {
Anim {}
}
ScreencopyView {
id: windowPreview
anchors.fill: parent
captureSource: root.overviewRoot.visible ? root.toplevel : null
live: true
Rectangle {
anchors.fill: parent
radius: 8
color: pressed ? ColorUtils.transparentize(Appearance.colors.colLayer2Active, 0.5) :
hovered ? ColorUtils.transparentize(Appearance.colors.colLayer2Hover, 0.7) :
ColorUtils.transparentize(Appearance.colors.colLayer2)
border.color : ColorUtils.transparentize(Appearance.m3colors.m3outline, 0.7)
border.width : 1
}
ColumnLayout {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
spacing: 8
Image {
id: windowIcon
property var iconSize: {
return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1);
}
Layout.alignment: Qt.AlignHCenter
source: root.iconPath
width: iconSize
height: iconSize
sourceSize: Qt.size(iconSize, iconSize)
Behavior on width {
Anim {}
}
Behavior on height {
Anim {}
}
}
}
}
}
-3
View File
@@ -3,7 +3,6 @@
import Quickshell import Quickshell
import qs.Modules import qs.Modules
import qs.Modules.Lock import qs.Modules.Lock
import qs.Modules.WSOverview
import qs.Helpers import qs.Helpers
Scope { Scope {
@@ -22,6 +21,4 @@ Scope {
NotificationCenter { NotificationCenter {
} }
Overview {}
} }