Krolyxon
Commit b7d60e8

Branches

Refs →

aurora.configquickshellauroraOverview.qml

270 lines · 12.1 KB · QML

1import QtQuick
2import QtQuick.Layouts
3import Quickshell
4import Quickshell.Wayland
5import Quickshell.Hyprland
6
7// Window overview for one monitor: its 10 workspaces as scaled boxes with live window previews.
8// Click a window = focus, click a workspace = switch, drag a window onto another box = move.
9// Type to filter by title/class, Enter focuses the first match, Esc closes.
10PanelWindow {
11 id: win
12 property bool opened: false
13 signal closeRequested()
14
15 visible: opened
16 anchors { top: true; bottom: true; left: true; right: true }
17 exclusionMode: ExclusionMode.Ignore
18 color: "transparent"
19 WlrLayershell.layer: WlrLayer.Overlay
20 WlrLayershell.namespace: "aurora-overview"
21 WlrLayershell.keyboardFocus: opened ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
22
23 readonly property var mon: screen ? Hyprland.monitorFor(screen) : null
24 readonly property var activeWs: mon ? mon.activeWorkspace : null
25 // same 10-per-monitor block as Workspaces.qml (split-monitor-workspaces)
26 readonly property int offset: activeWs && activeWs.id > 0 ? Math.floor((activeWs.id - 1) / 10) * 10 : 0
27 readonly property real monW: mon ? mon.width / mon.scale : 1920
28 readonly property real monH: mon ? mon.height / mon.scale : 1080
29
30 onOpenedChanged: {
31 if (!opened) return
32 filter.text = ""
33 Hyprland.refreshToplevels()
34 Hyprland.refreshMonitors()
35 filter.forceActiveFocus()
36 }
37
38 function windowsOn(id) {
39 var l = Hyprland.toplevels.values, out = []
40 for (var i = 0; i < l.length; i++) if (l[i].workspace && l[i].workspace.id === id) out.push(l[i])
41 return out
42 }
43 function cls(t) { return t.lastIpcObject && t.lastIpcObject.class ? t.lastIpcObject.class : "" }
44 function matches(t) {
45 var q = filter.text.toLowerCase()
46 return q === "" || String(t.title).toLowerCase().indexOf(q) >= 0 || cls(t).toLowerCase().indexOf(q) >= 0
47 }
48 function firstMatch() {
49 for (var i = 1; i <= 10; i++) {
50 var l = windowsOn(offset + i)
51 for (var j = 0; j < l.length; j++) if (matches(l[j])) return l[j]
52 }
53 return null
54 }
55 function iconFor(t) {
56 var e = DesktopEntries.heuristicLookup(cls(t))
57 return Quickshell.iconPath(e ? e.icon : cls(t).toLowerCase(), true)
58 }
59 property int dragWs: -1 // box whose window is being dragged (raised above the others)
60 Timer { id: resync; interval: 300; onTriggered: Hyprland.refreshToplevels() }
61
62 function focus(t) { Hypr.focusWindow(t.address); closeRequested() }
63
64 // box under a point in grid coordinates (for drops)
65 function wsAt(px, py) {
66 for (var i = 0; i < boxes.count; i++) {
67 var b = boxes.itemAt(i)
68 var p = b.mapFromItem(grid, px, py)
69 if (p.x >= 0 && p.y >= 0 && p.x <= b.width && p.y <= b.height) return b.wsId
70 }
71 return -1
72 }
73
74 Rectangle {
75 anchors.fill: parent
76 color: Theme.scrim
77 MouseArea { anchors.fill: parent; onClicked: win.closeRequested() }
78 }
79
80 ColumnLayout {
81 anchors.fill: parent
82 anchors.margins: 40
83 anchors.topMargin: Theme.barHeight + 24
84 spacing: 16
85
86 RowLayout {
87 Layout.alignment: Qt.AlignHCenter
88 spacing: 8
89 Rectangle {
90 implicitWidth: 520
91 implicitHeight: 56
92 radius: 28
93 color: Theme.s2
94 Icon {
95 id: lens
96 x: 20
97 anchors.verticalCenter: parent.verticalCenter
98 text: Theme.iSearch
99 color: Theme.grey
100 font.pixelSize: 19
101 }
102 Text {
103 anchors.left: filter.left
104 anchors.verticalCenter: parent.verticalCenter
105 visible: filter.text === ""
106 text: "Filter windows"
107 color: Theme.mute
108 font: filter.font
109 }
110 TextInput {
111 id: filter
112 anchors.left: lens.right
113 anchors.leftMargin: 14
114 anchors.right: parent.right
115 anchors.rightMargin: 20
116 anchors.verticalCenter: parent.verticalCenter
117 color: Theme.fg
118 selectionColor: Theme.fill
119 selectedTextColor: Theme.fillFg
120 font.family: Theme.ui
121 font.pixelSize: 18
122 clip: true
123 cursorDelegate: Rectangle {
124 width: 2
125 color: Theme.live
126 visible: filter.activeFocus
127 Timer { running: filter.activeFocus; interval: 500; repeat: true; onTriggered: parent.opacity = 1 - parent.opacity }
128 }
129 Keys.onEscapePressed: win.closeRequested()
130 Keys.onReturnPressed: { var t = win.firstMatch(); if (t) win.focus(t) }
131 Keys.onEnterPressed: { var t = win.firstMatch(); if (t) win.focus(t) }
132 }
133 }
134 Row {
135 Layout.leftMargin: 18
136 spacing: 28
137 Repeater {
138 model: ["click focus", "drag move", "↵ first match", "esc close"]
139 Lb { required property string modelData; text: modelData }
140 }
141 }
142 }
143
144 Item {
145 id: grid
146 Layout.fillWidth: true
147 Layout.fillHeight: true
148
149 readonly property int cols: 5
150 readonly property int gap: 16
151 // largest box with the monitor's aspect ratio that fits 5 x 2
152 readonly property real boxW: Math.min((width - gap * (cols - 1)) / cols,
153 ((height - gap - 2 * 40) / 2) * win.monW / win.monH)
154 readonly property real boxH: boxW * win.monH / win.monW
155 readonly property real sc: boxW / win.monW
156
157 Repeater {
158 id: boxes
159 model: 10
160 delegate: Item {
161 id: box
162 required property int index
163 readonly property int wsId: win.offset + index + 1
164 readonly property bool current: win.activeWs !== null && win.activeWs.id === wsId
165 readonly property real originX: (grid.width - (grid.boxW * grid.cols + grid.gap * (grid.cols - 1))) / 2
166 x: originX + (index % grid.cols) * (grid.boxW + grid.gap)
167 y: Math.floor(index / grid.cols) * (grid.boxH + grid.gap + 40)
168 width: grid.boxW
169 height: grid.boxH + 40
170 z: win.dragWs === wsId ? 10 : 0
171
172 Row {
173 x: 6
174 spacing: 10
175 Num {
176 text: box.index + 1
177 font.pixelSize: 28
178 color: box.current ? Theme.fill : Theme.fg
179 }
180 Lb {
181 anchors.verticalCenter: parent.verticalCenter
182 text: win.windowsOn(box.wsId).length ? win.windowsOn(box.wsId).length + " open" : "empty"
183 color: box.current ? Theme.fill : Theme.grey
184 }
185 }
186
187 Rectangle {
188 id: frame
189 y: 34
190 width: grid.boxW
191 height: grid.boxH
192 radius: 20
193 clip: true
194 color: Theme.s1
195 border.width: box.current ? 2 : 1
196 border.color: box.current ? Theme.fill : (wm.containsMouse ? Theme.grey : Theme.s2)
197
198 MouseArea {
199 id: wm
200 anchors.fill: parent
201 hoverEnabled: true
202 onClicked: { Hypr.workspace(box.wsId); win.closeRequested() }
203 }
204
205 Repeater {
206 model: win.windowsOn(box.wsId)
207 delegate: Rectangle {
208 id: tile
209 required property var modelData
210 readonly property var ipc: modelData.lastIpcObject || {}
211 readonly property var at: ipc.at || [0, 0]
212 readonly property var size: ipc.size || [win.monW / 2, win.monH / 2]
213 readonly property bool hit: win.matches(modelData)
214 readonly property real homeX: (at[0] - (win.mon ? win.mon.x : 0)) * grid.sc
215 readonly property real homeY: (at[1] - (win.mon ? win.mon.y : 0)) * grid.sc
216 x: homeX
217 y: homeY
218 width: Math.max(12, size[0] * grid.sc)
219 height: Math.max(12, size[1] * grid.sc)
220 z: dm.drag.active ? 10 : (ipc.floating ? 2 : 1)
221 color: Theme.s3
222 radius: 6
223 border.width: dm.containsMouse || dm.drag.active ? 2 : 1
224 border.color: dm.containsMouse || dm.drag.active ? Theme.fill : Theme.s2
225 opacity: hit ? 1 : 0.25
226
227 ScreencopyView {
228 id: shot
229 anchors.fill: parent
230 anchors.margins: 1
231 captureSource: win.opened && tile.modelData.wayland ? tile.modelData.wayland : null
232 live: true
233 }
234 Image { // big centred icon until the preview arrives, then a corner badge
235 x: shot.hasContent ? 4 : (parent.width - width) / 2
236 y: shot.hasContent ? parent.height - height - 4 : (parent.height - height) / 2
237 width: shot.hasContent ? 18 : Math.min(32, parent.width / 2)
238 height: width
239 sourceSize.width: 64
240 sourceSize.height: 64
241 source: win.iconFor(tile.modelData)
242 asynchronous: true
243 }
244 MouseArea {
245 id: dm
246 anchors.fill: parent
247 hoverEnabled: true
248 cursorShape: drag.active ? Qt.ClosedHandCursor : Qt.PointingHandCursor
249 drag.target: tile
250 drag.threshold: 6
251 drag.onActiveChanged: if (drag.active) win.dragWs = box.wsId
252 onReleased: {
253 win.dragWs = -1
254 if (!drag.active) return
255 var c = tile.mapToItem(grid, tile.width / 2, tile.height / 2)
256 var ws = win.wsAt(c.x, c.y)
257 if (ws > 0 && ws !== box.wsId) { Hypr.moveWindow(tile.modelData.address, ws); resync.restart() }
258 tile.x = Qt.binding(() => tile.homeX)
259 tile.y = Qt.binding(() => tile.homeY)
260 }
261 onClicked: win.focus(tile.modelData)
262 }
263 }
264 }
265 }
266 }
267 }
268 }
269 }
270}