Krolyxon
Commit 4b1d495

Branches

Refs →

aurora.configquickshellauroraClipWindow.qml

360 lines · 14.3 KB · QML

1import QtQuick
2import QtQuick.Layouts
3import QtQuick.Effects
4import Quickshell
5import Quickshell.Wayland
6
7// Clipboard history (cliphist): a card over the dimmed screen. Search pill with the live caret, filter chips
8// (Tab cycles: all / text / links / images), the history as rows (type in a circle: text, link, code, the colour
9// itself, the image as dots; a Doto number) and the selected entry in full on the right.
10// Enter copies + closes, Delete removes, Esc or a click outside closes.
11// After glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), clip/Clip.qml.
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; 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-clipboard"
26 WlrLayershell.keyboardFocus: opened ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
27
28 onOpenedChanged: {
29 if (!opened) return
30 filter.text = ""
31 kind = 0
32 armed = false
33 Clip.refresh()
34 list.currentIndex = 0
35 filter.forceActiveFocus()
36 }
37
38 property bool armed: false
39 Timer { id: disarm; interval: 3000; onTriggered: win.armed = false }
40
41 readonly property var kinds: ["all", "text", "links", "images"]
42 property int kind: 0
43 function typeOf(e) {
44 if (e.image) return "image"
45 var t = e.text.trim()
46 if (/^(https?|ftp|file):\/\/\S+$/i.test(t) || /^www\.\S+$/i.test(t)) return "link"
47 if (/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(t) || /^rgba?\(/i.test(t)) return "colour"
48 if (/[{};]|=>|^\s*(sudo|git|cd|ls|cat|echo|hyprctl|qs|pacman|paru|systemctl|python3?)\s/.test(t)) return "code"
49 return "text"
50 }
51 readonly property var shown: {
52 var q = filter.text.toLowerCase(), out = [], l = Clip.entries, k = kinds[kind]
53 for (var i = 0; i < l.length; i++) {
54 var ty = typeOf(l[i])
55 if (k === "text" && (ty === "image" || ty === "link")) continue
56 if (k === "links" && ty !== "link") continue
57 if (k === "images" && ty !== "image") continue
58 if (q === "" || l[i].text.toLowerCase().indexOf(q) >= 0) out.push(l[i])
59 }
60 return out
61 }
62 readonly property var cur: list.currentIndex >= 0 && list.currentIndex < shown.length ? shown[list.currentIndex] : null
63 function pick(e) {
64 if (!e) return
65 Clip.copy(e.id)
66 closeRequested()
67 }
68 function icon(ty) { return ty === "link" ? Theme.iLink : ty === "code" ? Theme.iCode : ty === "image" ? Theme.iImage : Theme.iText }
69
70 Rectangle {
71 anchors.fill: parent
72 color: Theme.scrim
73 opacity: win.t
74 MouseArea { anchors.fill: parent; onClicked: win.closeRequested() }
75 }
76
77 Rectangle {
78 id: card
79 anchors.horizontalCenter: parent.horizontalCenter
80 y: Math.round((parent.height - height) / 2) - 30 + 24 * (1 - win.t)
81 width: Math.min(parent.width - 80, 1160)
82 height: Math.min(parent.height - 160, 780)
83 opacity: win.t
84 radius: 44
85 color: Theme.fade(Theme.s1, Theme.panelAlpha)
86 border.width: 1
87 border.color: Theme.s2
88 MouseArea { anchors.fill: parent }
89
90 // ---- search ----
91 Rectangle {
92 id: search
93 x: 34
94 y: 30
95 width: parent.width - 68
96 height: 66
97 radius: 33
98 color: Theme.s2
99 Icon { id: lens; x: 24; anchors.verticalCenter: parent.verticalCenter; text: Theme.iSearch; color: Theme.grey; font.pixelSize: 22 }
100 TextInput {
101 id: filter
102 anchors.left: lens.right
103 anchors.leftMargin: 16
104 anchors.right: chips.left
105 anchors.rightMargin: 12
106 anchors.verticalCenter: parent.verticalCenter
107 font.family: Theme.ui
108 font.pixelSize: 22
109 color: Theme.fg
110 selectionColor: Theme.fill
111 selectedTextColor: Theme.fillFg
112 clip: true
113 cursorDelegate: Rectangle {
114 width: 2
115 color: Theme.live
116 visible: filter.activeFocus
117 Timer { running: filter.activeFocus; interval: 500; repeat: true; onTriggered: parent.opacity = 1 - parent.opacity }
118 }
119 Text {
120 visible: !filter.text
121 text: "Search clipboard"
122 font: filter.font
123 color: Theme.mute
124 }
125 onTextChanged: list.currentIndex = 0
126 Keys.onEscapePressed: win.closeRequested()
127 Keys.onReturnPressed: win.pick(win.cur)
128 Keys.onEnterPressed: win.pick(win.cur)
129 Keys.onUpPressed: list.decrementCurrentIndex()
130 Keys.onDownPressed: list.incrementCurrentIndex()
131 Keys.onTabPressed: { win.kind = (win.kind + 1) % win.kinds.length; list.currentIndex = 0 }
132 Keys.onBacktabPressed: { win.kind = (win.kind + win.kinds.length - 1) % win.kinds.length; list.currentIndex = 0 }
133 Keys.onPressed: event => {
134 if (event.key === Qt.Key_Delete && win.cur) {
135 Clip.remove(win.cur.id)
136 event.accepted = true
137 } else if (event.key === Qt.Key_PageDown) {
138 list.currentIndex = Math.min(win.shown.length - 1, list.currentIndex + 10); event.accepted = true
139 } else if (event.key === Qt.Key_PageUp) {
140 list.currentIndex = Math.max(0, list.currentIndex - 10); event.accepted = true
141 }
142 }
143 }
144 Row {
145 id: chips
146 anchors.right: parent.right
147 anchors.rightMargin: 12
148 anchors.verticalCenter: parent.verticalCenter
149 spacing: 6
150 Repeater {
151 model: win.kinds
152 Chip {
153 required property string modelData
154 required property int index
155 text: modelData
156 selected: win.kind === index
157 onClicked: { win.kind = index; list.currentIndex = 0; filter.forceActiveFocus() }
158 }
159 }
160 }
161 }
162
163 // ---- the history ----
164 Row {
165 id: head
166 x: 40
167 anchors.top: search.bottom
168 anchors.topMargin: 22
169 spacing: 14
170 Num { anchors.baseline: hl.baseline; text: win.shown.length; font.pixelSize: 34 }
171 Lb { id: hl; text: win.shown.length === 1 ? "entry" : "entries" }
172 }
173 Row {
174 anchors.right: list.right
175 anchors.verticalCenter: head.verticalCenter
176 spacing: 8
177 IconBtn {
178 visible: Clip.entries.length > 0
179 icon: Theme.iTrash
180 label: win.armed ? "sure?" : "wipe all"
181 active: win.armed
182 onClicked: {
183 if (!win.armed) { win.armed = true; disarm.restart(); return }
184 win.armed = false
185 Clip.wipe()
186 }
187 }
188 }
189
190 ListView {
191 id: list
192 x: 34
193 anchors.top: head.bottom
194 anchors.topMargin: 18
195 anchors.bottom: parent.bottom
196 anchors.bottomMargin: 34
197 width: Math.round((parent.width - 68) * 0.48)
198 clip: true
199 model: win.shown
200 spacing: 4
201 boundsBehavior: Flickable.StopAtBounds
202 highlightMoveDuration: 0
203
204 delegate: Rectangle {
205 id: row
206 required property var modelData
207 required property int index
208 readonly property bool selected: list.currentIndex === index
209 readonly property string ty: win.typeOf(modelData)
210 readonly property bool hasImg: modelData.image && Clip.ready[modelData.id] === true
211 width: list.width
212 height: 66
213 radius: 33
214 color: selected ? Theme.fill : rm.containsMouse ? Theme.s2 : "transparent"
215
216 MouseArea {
217 id: rm
218 anchors.fill: parent
219 hoverEnabled: true
220 cursorShape: Qt.PointingHandCursor
221 onEntered: list.currentIndex = row.index
222 onClicked: win.pick(row.modelData)
223 }
224 Rectangle {
225 id: ic
226 x: 8
227 anchors.verticalCenter: parent.verticalCenter
228 width: 50
229 height: 50
230 radius: 25
231 color: row.ty === "colour" ? row.modelData.text.trim() : row.selected ? Theme.fillFg : Theme.s2
232 Icon {
233 anchors.centerIn: parent
234 visible: row.ty !== "colour" && !thumb.visible
235 text: win.icon(row.ty)
236 font.pixelSize: 20
237 color: row.selected ? Theme.fill : Theme.fg
238 }
239 Image {
240 id: thumb
241 anchors.fill: parent
242 visible: row.hasImg && status === Image.Ready
243 source: row.hasImg ? "file://" + Clip.imageOf(row.modelData) : ""
244 fillMode: Image.PreserveAspectCrop
245 sourceSize: Qt.size(100, 100)
246 asynchronous: true
247 layer.enabled: true
248 layer.effect: DotMask { step: 3; circle: true }
249 }
250 }
251 Column {
252 anchors.left: ic.right
253 anchors.leftMargin: 14
254 anchors.right: num.left
255 anchors.rightMargin: 12
256 anchors.verticalCenter: parent.verticalCenter
257 Text {
258 width: parent.width
259 elide: Text.ElideRight
260 maximumLineCount: 1
261 text: row.modelData.text.replace(/\s+/g, " ")
262 font.family: Theme.ui
263 font.pixelSize: 15
264 color: row.selected ? Theme.fillFg : Theme.fg
265 }
266 Lb { text: row.ty; px: 10; color: row.selected ? Theme.fillFg : Theme.grey }
267 }
268 Num {
269 id: num
270 anchors.right: parent.right
271 anchors.rightMargin: 22
272 anchors.verticalCenter: parent.verticalCenter
273 text: ("0" + (row.index + 1)).slice(-2)
274 font.pixelSize: 28
275 color: row.selected ? Theme.fillFg : Theme.mute
276 }
277 }
278 Lb {
279 visible: win.shown.length === 0
280 anchors.centerIn: parent
281 color: Theme.mute
282 text: Clip.available ? (Clip.entries.length ? "no match" : "clipboard is empty") : "cliphist is not installed"
283 }
284 }
285
286 // ---- the selected entry in full ----
287 Rectangle {
288 id: preview
289 anchors.left: list.right
290 anchors.leftMargin: 24
291 anchors.right: parent.right
292 anchors.rightMargin: 34
293 anchors.top: search.bottom
294 anchors.topMargin: 30
295 anchors.bottom: parent.bottom
296 anchors.bottomMargin: 34
297 radius: 30
298 color: Theme.s2
299 readonly property var e: win.cur
300 readonly property string ty: e ? win.typeOf(e) : ""
301
302 Lb {
303 id: pl
304 x: 30
305 y: 28
306 text: preview.e ? preview.ty + " · " + (list.currentIndex + 1) + " of " + win.shown.length : ""
307 }
308 Text {
309 visible: preview.e !== null && preview.ty !== "image"
310 anchors.left: parent.left
311 anchors.right: parent.right
312 anchors.top: pl.bottom
313 anchors.bottom: parent.bottom
314 anchors.margins: 30
315 wrapMode: Text.WrapAnywhere
316 elide: Text.ElideRight
317 textFormat: Text.PlainText
318 text: preview.e ? preview.e.text : ""
319 font.family: preview.ty === "code" ? Theme.mono : Theme.ui
320 font.pixelSize: preview.ty === "code" ? 14 : 17
321 color: preview.ty === "colour" ? preview.e.text.trim() : Theme.fg
322 }
323 Image {
324 id: big
325 visible: preview.ty === "image"
326 anchors.left: parent.left
327 anchors.right: parent.right
328 anchors.top: pl.bottom
329 anchors.bottom: dims.top
330 anchors.margins: 30
331 fillMode: Image.PreserveAspectFit
332 asynchronous: true
333 sourceSize.width: 900
334 source: preview.ty === "image" && Clip.ready[preview.e.id] ? "file://" + Clip.imageOf(preview.e) : ""
335 }
336 Row {
337 id: dims
338 visible: preview.ty === "image"
339 x: 30
340 anchors.bottom: parent.bottom
341 anchors.bottomMargin: 28
342 spacing: 12
343 Num { anchors.baseline: df.baseline; text: preview.e && preview.ty === "image" ? preview.e.text.split(" ")[1] || "" : ""; font.pixelSize: 40 }
344 Lb { id: df; text: preview.e && preview.ty === "image" ? preview.e.fmt : "" }
345 }
346 }
347 }
348
349 Row {
350 anchors.horizontalCenter: parent.horizontalCenter
351 anchors.top: card.bottom
352 anchors.topMargin: 28
353 opacity: win.t
354 spacing: 36
355 Repeater {
356 model: ["↑↓ select", "↵ copy", "tab filter", "del delete", "esc close"]
357 Lb { required property string modelData; text: modelData }
358 }
359 }
360}