Krolyxon
Commit fb9b856

Branches

Refs →

aurora.configquickshellauroraOverview.qml

245 lines · 11.0 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.bgA
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: 420
91 implicitHeight: 32
92 color: Theme.surface
93 border.width: 1
94 border.color: Theme.accent
95 Text {
96 anchors.left: parent.left
97 anchors.leftMargin: 10
98 anchors.verticalCenter: parent.verticalCenter
99 text: Theme.iSearch
100 color: Theme.dim
101 font.family: Theme.font
102 font.pixelSize: Theme.fontSize
103 }
104 TextInput {
105 id: filter
106 anchors.fill: parent
107 anchors.leftMargin: 32
108 anchors.rightMargin: 10
109 verticalAlignment: TextInput.AlignVCenter
110 color: Theme.fg
111 selectionColor: Theme.accent
112 selectedTextColor: Theme.bg
113 font.family: Theme.font
114 font.pixelSize: Theme.fontSize
115 clip: true
116 Keys.onEscapePressed: win.closeRequested()
117 Keys.onReturnPressed: { var t = win.firstMatch(); if (t) win.focus(t) }
118 Keys.onEnterPressed: { var t = win.firstMatch(); if (t) win.focus(t) }
119 }
120 }
121 Text {
122 text: "click focus drag move enter first match esc close"
123 color: Theme.dim
124 font.family: Theme.font
125 font.pixelSize: Theme.fontSize - 2
126 }
127 }
128
129 Item {
130 id: grid
131 Layout.fillWidth: true
132 Layout.fillHeight: true
133
134 readonly property int cols: 5
135 readonly property int gap: 16
136 // largest box with the monitor's aspect ratio that fits 5 x 2
137 readonly property real boxW: Math.min((width - gap * (cols - 1)) / cols,
138 ((height - gap - 2 * 22) / 2) * win.monW / win.monH)
139 readonly property real boxH: boxW * win.monH / win.monW
140 readonly property real sc: boxW / win.monW
141
142 Repeater {
143 id: boxes
144 model: 10
145 delegate: Item {
146 id: box
147 required property int index
148 readonly property int wsId: win.offset + index + 1
149 readonly property bool current: win.activeWs !== null && win.activeWs.id === wsId
150 readonly property real originX: (grid.width - (grid.boxW * grid.cols + grid.gap * (grid.cols - 1))) / 2
151 x: originX + (index % grid.cols) * (grid.boxW + grid.gap)
152 y: Math.floor(index / grid.cols) * (grid.boxH + grid.gap + 22)
153 width: grid.boxW
154 height: grid.boxH + 22
155 z: win.dragWs === wsId ? 10 : 0
156
157 Text {
158 text: Theme.wsIcon(box.wsId) + " " + box.wsId
159 color: box.current ? Theme.accent : Theme.dim
160 font.family: Theme.font
161 font.pixelSize: Theme.fontSize
162 font.bold: box.current
163 }
164
165 Rectangle {
166 id: frame
167 y: 20
168 width: grid.boxW
169 height: grid.boxH
170 color: Theme.surface
171 border.width: box.current ? 2 : 1
172 border.color: box.current ? Theme.accent : (wm.containsMouse ? Theme.accentDim : Theme.line)
173
174 MouseArea {
175 id: wm
176 anchors.fill: parent
177 hoverEnabled: true
178 onClicked: { Hypr.workspace(box.wsId); win.closeRequested() }
179 }
180
181 Repeater {
182 model: win.windowsOn(box.wsId)
183 delegate: Rectangle {
184 id: tile
185 required property var modelData
186 readonly property var ipc: modelData.lastIpcObject || {}
187 readonly property var at: ipc.at || [0, 0]
188 readonly property var size: ipc.size || [win.monW / 2, win.monH / 2]
189 readonly property bool hit: win.matches(modelData)
190 readonly property real homeX: (at[0] - (win.mon ? win.mon.x : 0)) * grid.sc
191 readonly property real homeY: (at[1] - (win.mon ? win.mon.y : 0)) * grid.sc
192 x: homeX
193 y: homeY
194 width: Math.max(12, size[0] * grid.sc)
195 height: Math.max(12, size[1] * grid.sc)
196 z: dm.drag.active ? 10 : (ipc.floating ? 2 : 1)
197 color: Theme.surfaceHi
198 border.width: dm.containsMouse || dm.drag.active ? 2 : 1
199 border.color: dm.containsMouse || dm.drag.active ? Theme.accent : Theme.line
200 opacity: hit ? 1 : 0.25
201
202 ScreencopyView {
203 id: shot
204 anchors.fill: parent
205 anchors.margins: 1
206 captureSource: win.opened && tile.modelData.wayland ? tile.modelData.wayland : null
207 live: true
208 }
209 Image { // big centred icon until the preview arrives, then a corner badge
210 x: shot.hasContent ? 4 : (parent.width - width) / 2
211 y: shot.hasContent ? parent.height - height - 4 : (parent.height - height) / 2
212 width: shot.hasContent ? 18 : Math.min(32, parent.width / 2)
213 height: width
214 sourceSize.width: 64
215 sourceSize.height: 64
216 source: win.iconFor(tile.modelData)
217 asynchronous: true
218 }
219 MouseArea {
220 id: dm
221 anchors.fill: parent
222 hoverEnabled: true
223 cursorShape: drag.active ? Qt.ClosedHandCursor : Qt.PointingHandCursor
224 drag.target: tile
225 drag.threshold: 6
226 drag.onActiveChanged: if (drag.active) win.dragWs = box.wsId
227 onReleased: {
228 win.dragWs = -1
229 if (!drag.active) return
230 var c = tile.mapToItem(grid, tile.width / 2, tile.height / 2)
231 var ws = win.wsAt(c.x, c.y)
232 if (ws > 0 && ws !== box.wsId) { Hypr.moveWindow(tile.modelData.address, ws); resync.restart() }
233 tile.x = Qt.binding(() => tile.homeX)
234 tile.y = Qt.binding(() => tile.homeY)
235 }
236 onClicked: win.focus(tile.modelData)
237 }
238 }
239 }
240 }
241 }
242 }
243 }
244 }
245}