| 1 | import QtQuick |
| 2 | import QtQuick.Layouts |
| 3 | import Quickshell |
| 4 | import Quickshell.Wayland |
| 5 | |
| 6 | // Glyph picker (SUPER+grave, `chars` IPC): emoji, nerd font glyphs, unicode symbols, kaomoji, fancy text. |
| 7 | // Type to fuzzy search; arrows / Ctrl+HJKL move, Tab section, Ctrl+Left/Right group, Enter copies, Shift+Enter types it |
| 8 | // into the focused window, Ctrl+Enter collects into a buffer, Alt+Enter copies the codepoint. |
| 9 | // Looks like glyph's charms (github.com/Nebelung-cloud/glyph-rice, GPL-3.0, charms/Charms.qml): a card over the dimmed |
| 10 | // screen, search pill, section chips, tiles (the selected one white... here primary), the selection large at the bottom. |
| 11 | PanelWindow { |
| 12 | id: win |
| 13 | property bool opened: false |
| 14 | signal closeRequested() |
| 15 | |
| 16 | property real t: opened ? 1 : 0 |
| 17 | Behavior on t { NumberAnimation { duration: Theme.t; easing.type: Easing.BezierSpline; easing.bezierCurve: Theme.glide } } |
| 18 | |
| 19 | visible: t > 0 |
| 20 | anchors { top: true; bottom: true; left: true; right: true } |
| 21 | exclusionMode: ExclusionMode.Ignore |
| 22 | color: "transparent" |
| 23 | WlrLayershell.layer: WlrLayer.Overlay |
| 24 | WlrLayershell.namespace: "aurora-chars" |
| 25 | WlrLayershell.keyboardFocus: opened ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None |
| 26 | |
| 27 | readonly property var sections: [ |
| 28 | { id: "all", label: "all", icon: Theme.iGrid, sets: ["emoji", "nerd", "sym"] }, |
| 29 | { id: "recent", label: "recent", icon: Theme.iHistory, sets: ["emoji", "nerd", "sym"] }, |
| 30 | { id: "emoji", label: "emoji", icon: Theme.iEmoji, sets: ["emoji"] }, |
| 31 | { id: "nerd", label: "nerd", icon: Theme.iFontGlyph, sets: ["nerd"] }, |
| 32 | { id: "sym", label: "symbols", icon: Theme.iSigma, sets: ["sym"] }, |
| 33 | { id: "kao", label: "kaomoji", icon: Theme.iKaomoji, sets: ["kao"] }, |
| 34 | { id: "fancy", label: "fancy", icon: Theme.iFancy, sets: [] } |
| 35 | ] |
| 36 | property int sec: 0 |
| 37 | property int grp: -1 |
| 38 | property string buffer: "" |
| 39 | property var results: [] |
| 40 | property var hovered: null |
| 41 | readonly property var section: sections[sec] |
| 42 | readonly property bool listMode: section.id === "kao" || section.id === "fancy" |
| 43 | readonly property int tone: Settings.charTone |
| 44 | // groups of the shown set (single-set sections only) |
| 45 | readonly property var groups: { |
| 46 | if (!Chars.loaded || section.sets.length !== 1) return [] |
| 47 | var i = Chars.setIndex(section.sets[0]) |
| 48 | return i >= 0 ? Chars.sets[i].groups : [] |
| 49 | } |
| 50 | readonly property var view: listMode ? list : grid |
| 51 | readonly property var current: hovered ? hovered : (results.length && view.currentIndex >= 0 ? results[view.currentIndex] : null) |
| 52 | |
| 53 | onOpenedChanged: { |
| 54 | if (!opened) return |
| 55 | hovered = null |
| 56 | buffer = "" |
| 57 | filter.text = "" |
| 58 | grp = -1 |
| 59 | Chars.load() |
| 60 | sec = Chars.loaded && Chars.recent(1).length ? 1 : 0 |
| 61 | recompute() |
| 62 | filter.forceActiveFocus() |
| 63 | } |
| 64 | onSecChanged: { grp = -1; recompute() } |
| 65 | onGrpChanged: recompute() |
| 66 | Connections { |
| 67 | target: Chars |
| 68 | function onLoadedChanged() { |
| 69 | if (win.opened && Chars.recent(1).length && filter.text === "") win.sec = 1 |
| 70 | win.recompute() |
| 71 | } |
| 72 | } |
| 73 | Timer { id: debounce; interval: 40; onTriggered: win.recompute() } |
| 74 | |
| 75 | function setList(id) { |
| 76 | var out = [] |
| 77 | for (var i = 0; i < section.sets.length; i++) out.push(Chars.setIndex(section.sets[i])) |
| 78 | return out |
| 79 | } |
| 80 | function recompute() { |
| 81 | hovered = null |
| 82 | var q = filter.text |
| 83 | if (section.id === "fancy") results = Chars.fancy(q.trim() === "" ? "aurora" : q) |
| 84 | else if (!Chars.loaded) results = [] |
| 85 | else if (section.id === "recent") { |
| 86 | if (q.trim() === "") results = Chars.recent(300) |
| 87 | else { |
| 88 | var l = Chars.search(q, setList(), -1), out = [] |
| 89 | for (var i = 0; i < l.length; i++) if (Chars.counts[l[i].c]) out.push(l[i]) |
| 90 | results = out |
| 91 | } |
| 92 | } else if (section.id === "kao") { |
| 93 | var k = Chars.search(q, setList(), grp) |
| 94 | results = q.trim() === "" ? Chars.byUse(k) : k |
| 95 | } else results = Chars.search(q, setList(), grp) |
| 96 | grid.currentIndex = 0 |
| 97 | list.currentIndex = 0 |
| 98 | grid.positionViewAtBeginning() |
| 99 | list.positionViewAtBeginning() |
| 100 | } |
| 101 | function cycleSection(d) { sec = (sec + d + sections.length) % sections.length } |
| 102 | function cycleGroup(d) { |
| 103 | if (!groups.length) return |
| 104 | var n = groups.length + 1 // -1 = all |
| 105 | grp = ((grp + 1 + d + n) % n) - 1 |
| 106 | } |
| 107 | function outOf(it) { return section.id === "fancy" ? it.c : Chars.glyph(it, tone) } |
| 108 | function codeOf(s) { |
| 109 | var cps = Array.from(s) |
| 110 | if (cps.length === 1) return "0x" + cps[0].codePointAt(0).toString(16).toUpperCase() |
| 111 | return Chars.hex(s) |
| 112 | } |
| 113 | // how: copy | type | buffer | code |
| 114 | function pick(it, how) { |
| 115 | if (!it && buffer === "") return |
| 116 | var s = it ? outOf(it) : "" |
| 117 | if (it && section.id !== "fancy" && (how === "buffer" || buffer === "")) Chars.bump(it.c) |
| 118 | if (how === "buffer") { buffer += s; return } |
| 119 | if (how === "code") { Chars.copy(codeOf(s)); closeRequested(); return } |
| 120 | if (buffer !== "") s = buffer |
| 121 | closeRequested() |
| 122 | if (how === "type" && Chars.hasWtype) Chars.type(s) |
| 123 | else Chars.copy(s) |
| 124 | } |
| 125 | function move(key) { |
| 126 | var v = view |
| 127 | if (listMode) { |
| 128 | if (key === Qt.Key_Up) v.decrementCurrentIndex() |
| 129 | else if (key === Qt.Key_Down) v.incrementCurrentIndex() |
| 130 | return |
| 131 | } |
| 132 | if (key === Qt.Key_Up) v.moveCurrentIndexUp() |
| 133 | else if (key === Qt.Key_Down) v.moveCurrentIndexDown() |
| 134 | else if (key === Qt.Key_Left) v.moveCurrentIndexLeft() |
| 135 | else if (key === Qt.Key_Right) v.moveCurrentIndexRight() |
| 136 | } |
| 137 | function page(d) { |
| 138 | var step = listMode ? 10 : grid.cols * Math.max(1, Math.floor(grid.height / grid.cellHeight)) |
| 139 | view.currentIndex = Math.max(0, Math.min(results.length - 1, view.currentIndex + d * step)) |
| 140 | } |
| 141 | function fontOf(it) { |
| 142 | return it && it.s !== undefined && Chars.sets[it.s] && Chars.sets[it.s].id === "emoji" ? "Noto Color Emoji" : Theme.nf |
| 143 | } |
| 144 | |
| 145 | Rectangle { |
| 146 | anchors.fill: parent |
| 147 | color: Theme.scrim |
| 148 | opacity: win.t |
| 149 | MouseArea { anchors.fill: parent; onClicked: win.closeRequested() } |
| 150 | } |
| 151 | |
| 152 | Rectangle { |
| 153 | id: card |
| 154 | anchors.horizontalCenter: parent.horizontalCenter |
| 155 | y: Math.round((parent.height - height) / 2) - 30 + 24 * (1 - win.t) |
| 156 | width: Math.min(parent.width - 80, 960) |
| 157 | height: Math.min(parent.height - 160, 740) |
| 158 | opacity: win.t |
| 159 | radius: 44 |
| 160 | color: Theme.fade(Theme.s1, Theme.panelAlpha) |
| 161 | border.width: 1 |
| 162 | border.color: Theme.s2 |
| 163 | MouseArea { anchors.fill: parent } |
| 164 | |
| 165 | ColumnLayout { |
| 166 | anchors.fill: parent |
| 167 | anchors.margins: 30 |
| 168 | spacing: 14 |
| 169 | |
| 170 | // ---- search + tone + close ---- |
| 171 | RowLayout { |
| 172 | Layout.fillWidth: true |
| 173 | spacing: 10 |
| 174 | Rectangle { |
| 175 | Layout.fillWidth: true |
| 176 | implicitHeight: 60 |
| 177 | radius: 30 |
| 178 | color: Theme.s2 |
| 179 | Icon { |
| 180 | id: lens |
| 181 | x: 22 |
| 182 | anchors.verticalCenter: parent.verticalCenter |
| 183 | text: Theme.iSearch |
| 184 | color: Theme.grey |
| 185 | font.pixelSize: 20 |
| 186 | } |
| 187 | Text { |
| 188 | anchors.left: filter.left |
| 189 | anchors.right: count.left |
| 190 | anchors.verticalCenter: parent.verticalCenter |
| 191 | visible: filter.text === "" |
| 192 | elide: Text.ElideRight |
| 193 | text: win.section.id === "fancy" ? "Type some text" : "Search " + win.section.label + " (name, keyword, u+f0001, or paste a glyph)" |
| 194 | color: Theme.mute |
| 195 | font.family: Theme.ui |
| 196 | font.pixelSize: 18 |
| 197 | } |
| 198 | TextInput { |
| 199 | id: filter |
| 200 | anchors.left: lens.right |
| 201 | anchors.leftMargin: 14 |
| 202 | anchors.right: count.left |
| 203 | anchors.rightMargin: 12 |
| 204 | anchors.verticalCenter: parent.verticalCenter |
| 205 | color: Theme.fg |
| 206 | selectionColor: Theme.fill |
| 207 | selectedTextColor: Theme.fillFg |
| 208 | font.family: Theme.ui |
| 209 | font.pixelSize: 18 |
| 210 | clip: true |
| 211 | cursorDelegate: Rectangle { |
| 212 | width: 2 |
| 213 | color: Theme.live |
| 214 | visible: filter.activeFocus |
| 215 | Timer { running: filter.activeFocus; interval: 500; repeat: true; onTriggered: parent.opacity = 1 - parent.opacity } |
| 216 | } |
| 217 | onTextChanged: { |
| 218 | // recent is the landing page: typing there searches everything (used ones rank first anyway) |
| 219 | if (text !== "" && win.section.id === "recent") win.sec = 0 |
| 220 | if (text === "") win.recompute(); else debounce.restart() |
| 221 | } |
| 222 | Keys.onEscapePressed: win.closeRequested() |
| 223 | Keys.onPressed: event => { |
| 224 | var k = event.key, m = event.modifiers |
| 225 | // ctrl+h/j/k/l move through the results like the arrows |
| 226 | var vim = { [Qt.Key_H]: Qt.Key_Left, [Qt.Key_J]: Qt.Key_Down, [Qt.Key_K]: Qt.Key_Up, [Qt.Key_L]: Qt.Key_Right } |
| 227 | if ((m & Qt.ControlModifier) && vim[k] !== undefined) { |
| 228 | win.hovered = null |
| 229 | win.move(vim[k]) |
| 230 | event.accepted = true |
| 231 | return |
| 232 | } |
| 233 | if (k === Qt.Key_Return || k === Qt.Key_Enter) { |
| 234 | if (debounce.running) { debounce.stop(); win.recompute() } |
| 235 | var it = win.results[win.view.currentIndex] |
| 236 | if (m & Qt.ControlModifier) win.pick(it, "buffer") |
| 237 | else if (m & Qt.ShiftModifier) win.pick(it, "type") |
| 238 | else if (m & Qt.AltModifier) win.pick(it, "code") |
| 239 | else win.pick(it, "copy") |
| 240 | } else if (k === Qt.Key_Tab) win.cycleSection(1) |
| 241 | else if (k === Qt.Key_Backtab) win.cycleSection(-1) |
| 242 | else if ((m & Qt.ControlModifier) && (k === Qt.Key_Left || k === Qt.Key_Right)) win.cycleGroup(k === Qt.Key_Left ? -1 : 1) |
| 243 | else if ((m & Qt.AltModifier) && k >= Qt.Key_1 && k < Qt.Key_1 + win.sections.length) win.sec = k - Qt.Key_1 |
| 244 | else if (k === Qt.Key_Up || k === Qt.Key_Down || (!win.listMode && (k === Qt.Key_Left || k === Qt.Key_Right))) { |
| 245 | win.hovered = null |
| 246 | win.move(k) |
| 247 | } else if (k === Qt.Key_PageDown) win.page(1) |
| 248 | else if (k === Qt.Key_PageUp) win.page(-1) |
| 249 | else if (k === Qt.Key_Backspace && filter.text === "" && win.buffer !== "") |
| 250 | win.buffer = Array.from(win.buffer).slice(0, -1).join("") |
| 251 | else return |
| 252 | event.accepted = true |
| 253 | } |
| 254 | } |
| 255 | Lb { |
| 256 | id: count |
| 257 | anchors.right: parent.right |
| 258 | anchors.rightMargin: 22 |
| 259 | anchors.verticalCenter: parent.verticalCenter |
| 260 | text: win.results.length |
| 261 | } |
| 262 | } |
| 263 | // skin tone: the raised hand in each tone, click to choose (persisted) |
| 264 | Row { |
| 265 | visible: Chars.loaded && (win.section.id === "emoji" || win.section.id === "all" || win.section.id === "recent") |
| 266 | spacing: 2 |
| 267 | Repeater { |
| 268 | model: 6 |
| 269 | delegate: Rectangle { |
| 270 | required property int index |
| 271 | readonly property var hand: Chars.byChar["✋"] |
| 272 | width: 40 |
| 273 | height: 40 |
| 274 | radius: 20 |
| 275 | color: Settings.charTone === index ? Theme.fill : (tm.containsMouse ? Theme.s3 : "transparent") |
| 276 | Text { |
| 277 | anchors.centerIn: parent |
| 278 | text: parent.hand ? Chars.glyph(parent.hand, parent.index) : "" |
| 279 | font.family: "Noto Color Emoji" |
| 280 | font.pixelSize: 17 |
| 281 | } |
| 282 | MouseArea { |
| 283 | id: tm |
| 284 | anchors.fill: parent |
| 285 | hoverEnabled: true |
| 286 | cursorShape: Qt.PointingHandCursor |
| 287 | onClicked: { Settings.charTone = parent.index; filter.forceActiveFocus() } |
| 288 | } |
| 289 | } |
| 290 | } |
| 291 | } |
| 292 | IconBtn { implicitHeight: 48; icon: Theme.iClose; onClicked: win.closeRequested() } |
| 293 | } |
| 294 | |
| 295 | // ---- sections ---- |
| 296 | Row { |
| 297 | Layout.fillWidth: true |
| 298 | spacing: 2 |
| 299 | Repeater { |
| 300 | model: win.sections |
| 301 | delegate: Chip { |
| 302 | required property var modelData |
| 303 | required property int index |
| 304 | text: modelData.label |
| 305 | selected: win.sec === index |
| 306 | onClicked: { win.sec = index; filter.forceActiveFocus() } |
| 307 | } |
| 308 | } |
| 309 | } |
| 310 | |
| 311 | // ---- groups of the section ---- |
| 312 | ListView { |
| 313 | id: chips |
| 314 | Layout.fillWidth: true |
| 315 | implicitHeight: 32 |
| 316 | visible: win.groups.length > 0 |
| 317 | orientation: ListView.Horizontal |
| 318 | spacing: 4 |
| 319 | clip: true |
| 320 | boundsBehavior: Flickable.StopAtBounds |
| 321 | model: win.groups.length ? ["all"].concat(win.groups) : [] |
| 322 | currentIndex: win.grp + 1 |
| 323 | onCurrentIndexChanged: positionViewAtIndex(currentIndex, ListView.Contain) |
| 324 | delegate: Chip { |
| 325 | required property var modelData |
| 326 | required property int index |
| 327 | height: 32 |
| 328 | px: 10 |
| 329 | filled: true |
| 330 | text: modelData |
| 331 | selected: win.grp === index - 1 |
| 332 | onClicked: { win.grp = index - 1; filter.forceActiveFocus() } |
| 333 | } |
| 334 | } |
| 335 | |
| 336 | // ---- results ---- |
| 337 | Item { |
| 338 | Layout.fillWidth: true |
| 339 | Layout.fillHeight: true |
| 340 | |
| 341 | GridView { |
| 342 | id: grid |
| 343 | anchors.fill: parent |
| 344 | visible: !win.listMode |
| 345 | readonly property int cols: Math.max(1, Math.floor(width / 58)) |
| 346 | cellWidth: Math.floor(width / cols) |
| 347 | cellHeight: 58 |
| 348 | clip: true |
| 349 | model: win.listMode ? 0 : win.results.length |
| 350 | boundsBehavior: Flickable.StopAtBounds |
| 351 | highlightMoveDuration: 0 |
| 352 | keyNavigationWraps: false |
| 353 | cacheBuffer: 400 |
| 354 | delegate: Rectangle { |
| 355 | id: cell |
| 356 | required property int index |
| 357 | readonly property var it: win.results[index] |
| 358 | readonly property bool selected: grid.currentIndex === index |
| 359 | width: grid.cellWidth - 4 |
| 360 | height: grid.cellHeight - 4 |
| 361 | radius: 18 |
| 362 | color: selected ? Theme.fill : (gm.containsMouse ? Theme.s2 : "transparent") |
| 363 | Text { |
| 364 | anchors.centerIn: parent |
| 365 | text: cell.it ? Chars.glyph(cell.it, win.tone) : "" |
| 366 | color: cell.selected ? Theme.fillFg : Theme.fg |
| 367 | font.family: win.fontOf(cell.it) |
| 368 | font.pixelSize: 26 |
| 369 | } |
| 370 | MouseArea { |
| 371 | id: gm |
| 372 | anchors.fill: parent |
| 373 | hoverEnabled: true |
| 374 | acceptedButtons: Qt.LeftButton | Qt.RightButton |
| 375 | cursorShape: Qt.PointingHandCursor |
| 376 | onContainsMouseChanged: win.hovered = containsMouse ? cell.it : null |
| 377 | onClicked: mouse => { |
| 378 | grid.currentIndex = cell.index |
| 379 | win.pick(cell.it, mouse.button === Qt.RightButton ? "buffer" : "copy") |
| 380 | } |
| 381 | } |
| 382 | } |
| 383 | } |
| 384 | |
| 385 | ListView { |
| 386 | id: list |
| 387 | anchors.fill: parent |
| 388 | visible: win.listMode |
| 389 | clip: true |
| 390 | model: win.listMode ? win.results.length : 0 |
| 391 | spacing: 2 |
| 392 | boundsBehavior: Flickable.StopAtBounds |
| 393 | highlightMoveDuration: 0 |
| 394 | delegate: Rectangle { |
| 395 | id: row |
| 396 | required property int index |
| 397 | readonly property var it: win.results[index] |
| 398 | readonly property bool selected: list.currentIndex === index |
| 399 | width: list.width |
| 400 | height: 52 |
| 401 | radius: 26 |
| 402 | color: selected ? Theme.fill : (lm.containsMouse ? Theme.s2 : "transparent") |
| 403 | Text { |
| 404 | anchors.left: parent.left |
| 405 | anchors.leftMargin: 22 |
| 406 | anchors.right: tag.left |
| 407 | anchors.rightMargin: 12 |
| 408 | anchors.verticalCenter: parent.verticalCenter |
| 409 | text: row.it ? row.it.c : "" |
| 410 | elide: Text.ElideRight |
| 411 | maximumLineCount: 1 |
| 412 | color: row.selected ? Theme.fillFg : Theme.fg |
| 413 | font.family: Theme.ui |
| 414 | font.pixelSize: 18 |
| 415 | } |
| 416 | Lb { |
| 417 | id: tag |
| 418 | anchors.right: parent.right |
| 419 | anchors.rightMargin: 22 |
| 420 | anchors.verticalCenter: parent.verticalCenter |
| 421 | px: 10 |
| 422 | text: row.it ? (win.section.id === "fancy" ? row.it.n : row.it.k) : "" |
| 423 | color: row.selected ? Theme.fillFg : Theme.grey |
| 424 | } |
| 425 | MouseArea { |
| 426 | id: lm |
| 427 | anchors.fill: parent |
| 428 | hoverEnabled: true |
| 429 | acceptedButtons: Qt.LeftButton | Qt.RightButton |
| 430 | cursorShape: Qt.PointingHandCursor |
| 431 | onContainsMouseChanged: win.hovered = containsMouse ? row.it : null |
| 432 | onClicked: mouse => { |
| 433 | list.currentIndex = row.index |
| 434 | win.pick(row.it, mouse.button === Qt.RightButton ? "buffer" : "copy") |
| 435 | } |
| 436 | } |
| 437 | } |
| 438 | } |
| 439 | |
| 440 | Lb { |
| 441 | anchors.centerIn: parent |
| 442 | visible: win.results.length === 0 |
| 443 | color: Theme.mute |
| 444 | text: !Chars.loaded && win.section.id !== "fancy" ? "loading glyphs…" |
| 445 | : (win.section.id === "recent" && filter.text === "" ? "nothing used yet" : "no matches") |
| 446 | } |
| 447 | } |
| 448 | |
| 449 | // ---- the selected / hovered glyph, large ---- |
| 450 | Rectangle { |
| 451 | Layout.fillWidth: true |
| 452 | implicitHeight: 92 |
| 453 | radius: 30 |
| 454 | color: Theme.s2 |
| 455 | |
| 456 | Rectangle { |
| 457 | id: big |
| 458 | visible: !win.listMode |
| 459 | x: 12 |
| 460 | anchors.verticalCenter: parent.verticalCenter |
| 461 | width: visible ? 68 : 0 |
| 462 | height: 68 |
| 463 | radius: 34 |
| 464 | color: Theme.s3 |
| 465 | Text { |
| 466 | anchors.centerIn: parent |
| 467 | text: win.current ? Chars.glyph(win.current, win.tone) : "" |
| 468 | color: Theme.fill |
| 469 | font.family: win.fontOf(win.current) |
| 470 | font.pixelSize: 36 |
| 471 | } |
| 472 | } |
| 473 | Column { |
| 474 | anchors.left: big.right |
| 475 | anchors.leftMargin: 16 |
| 476 | anchors.right: buf.left |
| 477 | anchors.rightMargin: 12 |
| 478 | anchors.verticalCenter: parent.verticalCenter |
| 479 | spacing: 4 |
| 480 | Text { |
| 481 | width: parent.width |
| 482 | text: win.current ? win.current.n : "" |
| 483 | elide: Text.ElideRight |
| 484 | color: Theme.fg |
| 485 | font.family: Theme.ui |
| 486 | font.pixelSize: 16 |
| 487 | font.weight: Font.DemiBold |
| 488 | } |
| 489 | Lb { |
| 490 | width: parent.width |
| 491 | visible: win.current !== null && win.section.id !== "fancy" |
| 492 | px: 10 |
| 493 | color: Theme.fill |
| 494 | text: { |
| 495 | var c = win.current |
| 496 | if (!c || c.s === undefined || !Chars.sets[c.s]) return "" |
| 497 | var st = Chars.sets[c.s] |
| 498 | return (st.id === "kao" ? "" : Chars.hex(Chars.glyph(c, win.tone)) + " · ") |
| 499 | + st.label + " · " + st.groups[c.g] |
| 500 | } |
| 501 | elide: Text.ElideRight |
| 502 | } |
| 503 | Lb { |
| 504 | width: parent.width |
| 505 | visible: text !== "" |
| 506 | px: 10 |
| 507 | text: win.current && win.current.k && win.section.id !== "kao" ? win.current.k : "" |
| 508 | elide: Text.ElideRight |
| 509 | } |
| 510 | } |
| 511 | Row { // collected with ctrl+enter / right click |
| 512 | id: buf |
| 513 | anchors.right: parent.right |
| 514 | anchors.rightMargin: 22 |
| 515 | anchors.verticalCenter: parent.verticalCenter |
| 516 | spacing: 10 |
| 517 | visible: win.buffer !== "" |
| 518 | width: visible ? implicitWidth : 0 |
| 519 | Icon { |
| 520 | anchors.verticalCenter: parent.verticalCenter |
| 521 | text: Theme.iBuffer |
| 522 | color: Theme.live |
| 523 | font.pixelSize: 18 |
| 524 | } |
| 525 | Text { |
| 526 | anchors.verticalCenter: parent.verticalCenter |
| 527 | width: Math.min(implicitWidth, 220) |
| 528 | text: win.buffer |
| 529 | elide: Text.ElideLeft |
| 530 | color: Theme.fg |
| 531 | font.family: Theme.nf |
| 532 | font.pixelSize: 20 |
| 533 | } |
| 534 | } |
| 535 | } |
| 536 | } |
| 537 | } |
| 538 | |
| 539 | Row { |
| 540 | anchors.horizontalCenter: parent.horizontalCenter |
| 541 | anchors.top: card.bottom |
| 542 | anchors.topMargin: 26 |
| 543 | opacity: win.t |
| 544 | spacing: 30 |
| 545 | Repeater { |
| 546 | model: ["↵ copy"].concat(Chars.hasWtype ? ["shift ↵ type"] : []).concat(["ctrl ↵ collect", "alt ↵ code", "ctrl hjkl move", "tab section", "ctrl ←→ group", "esc close"]) |
| 547 | Lb { required property string modelData; text: modelData } |
| 548 | } |
| 549 | } |
| 550 | } |