Krolyxon
Branch master

Branches

Refs →

aurora.configquickshellauroraOverview.qml

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