auroraf0edd5d
aurora: add autohide dock (pinned + per-monitor running apps)
+183 −0
2 files changed
- A .config/quickshell/aurora/Dock.qml +178 −0
- M .config/quickshell/aurora/shell.qml +5 −0
| @@ -0,0 +1,178 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | import Quickshell | |
| 3 | + | import Quickshell.Wayland | |
| 4 | + | import Quickshell.Hyprland | |
| 5 | + | ||
| 6 | + | // Autohide dock (per monitor): touch the bottom edge to slide it up. | |
| 7 | + | // Pinned apps (Settings.dockPinned) + apps with windows on this monitor's workspaces. | |
| 8 | + | // Left click: focus (cycles through that app's windows) or launch. Middle: new instance. Right: pin/unpin. | |
| 9 | + | PanelWindow { | |
| 10 | + | id: win | |
| 11 | + | required property var modelData | |
| 12 | + | screen: modelData | |
| 13 | + | visible: Settings.dock | |
| 14 | + | ||
| 15 | + | readonly property var hmon: Hyprland.monitorFor(modelData) | |
| 16 | + | readonly property int size: 40 | |
| 17 | + | property bool shown: false | |
| 18 | + | ||
| 19 | + | anchors.bottom: true | |
| 20 | + | implicitWidth: Math.max(160, row.implicitWidth + 16) | |
| 21 | + | implicitHeight: size + 16 | |
| 22 | + | exclusiveZone: 0 | |
| 23 | + | color: "transparent" | |
| 24 | + | // hidden: only a 2px strip at the very bottom catches the mouse; shown: the whole dock | |
| 25 | + | mask: Region { item: win.shown ? panel : strip } | |
| 26 | + | WlrLayershell.layer: WlrLayer.Top | |
| 27 | + | WlrLayershell.namespace: "aurora-dock" | |
| 28 | + | WlrLayershell.keyboardFocus: WlrKeyboardFocus.None | |
| 29 | + | ||
| 30 | + | Timer { id: hide; interval: 600; onTriggered: win.shown = false } | |
| 31 | + | ||
| 32 | + | function appId(t) { | |
| 33 | + | if (t.wayland && t.wayland.appId) return t.wayland.appId | |
| 34 | + | return t.lastIpcObject && t.lastIpcObject.class ? t.lastIpcObject.class : "" | |
| 35 | + | } | |
| 36 | + | function entryFor(id) { | |
| 37 | + | return DesktopEntries.byId(id) || DesktopEntries.heuristicLookup(id) | |
| 38 | + | } | |
| 39 | + | // [{id, entry, wins: [HyprlandToplevel]}], pinned first (in pin order), then running by first seen | |
| 40 | + | readonly property var apps: { | |
| 41 | + | var out = [], byId = {}, pins = Settings.dockPinned | |
| 42 | + | for (var i = 0; i < pins.length; i++) { | |
| 43 | + | var p = { id: pins[i], entry: entryFor(pins[i]), wins: [], pinned: true } | |
| 44 | + | byId[pins[i].toLowerCase()] = p | |
| 45 | + | out.push(p) | |
| 46 | + | } | |
| 47 | + | var l = Hyprland.toplevels.values | |
| 48 | + | for (var j = 0; j < l.length; j++) { | |
| 49 | + | var t = l[j] | |
| 50 | + | if (!t.workspace || !t.workspace.monitor || t.workspace.monitor !== hmon) continue | |
| 51 | + | var id = appId(t) | |
| 52 | + | if (id === "") continue | |
| 53 | + | var e = entryFor(id) | |
| 54 | + | // match pins by id, or by the desktop entry they resolve to (e.g. "footclient" -> foot) | |
| 55 | + | var key = id.toLowerCase() | |
| 56 | + | var a = byId[key] | |
| 57 | + | if (!a && e) for (var k = 0; k < out.length; k++) if (out[k].entry === e) { a = out[k]; break } | |
| 58 | + | if (!a) { | |
| 59 | + | a = { id: id, entry: e, wins: [], pinned: false } | |
| 60 | + | byId[key] = a | |
| 61 | + | out.push(a) | |
| 62 | + | } | |
| 63 | + | a.wins.push(t) | |
| 64 | + | } | |
| 65 | + | return out | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | function activate(a) { | |
| 69 | + | if (a.wins.length === 0) { if (a.entry) a.entry.execute(); return } | |
| 70 | + | // focus the next window of that app after the active one (cycles) | |
| 71 | + | var act = Hyprland.activeToplevel, i = -1 | |
| 72 | + | for (var k = 0; k < a.wins.length; k++) if (act && a.wins[k].address === act.address) i = k | |
| 73 | + | Hypr.focusWindow(a.wins[(i + 1) % a.wins.length].address) | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | Item { | |
| 77 | + | id: strip | |
| 78 | + | anchors.bottom: parent.bottom | |
| 79 | + | width: parent.width | |
| 80 | + | height: 2 | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | MouseArea { | |
| 84 | + | anchors.fill: parent | |
| 85 | + | hoverEnabled: true | |
| 86 | + | onEntered: { hide.stop(); win.shown = true } | |
| 87 | + | onExited: hide.restart() | |
| 88 | + | ||
| 89 | + | Rectangle { | |
| 90 | + | id: panel | |
| 91 | + | width: parent.width | |
| 92 | + | height: parent.height - 6 | |
| 93 | + | y: win.shown ? 0 : parent.height | |
| 94 | + | Behavior on y { NumberAnimation { duration: 140; easing.type: Easing.OutCubic } } | |
| 95 | + | color: Theme.bgA | |
| 96 | + | border.width: 1 | |
| 97 | + | border.color: Theme.accent | |
| 98 | + | ||
| 99 | + | Row { | |
| 100 | + | id: row | |
| 101 | + | anchors.centerIn: parent | |
| 102 | + | spacing: 4 | |
| 103 | + | ||
| 104 | + | Repeater { | |
| 105 | + | model: win.apps | |
| 106 | + | delegate: Item { | |
| 107 | + | id: cell | |
| 108 | + | required property var modelData | |
| 109 | + | readonly property bool running: modelData.wins.length > 0 | |
| 110 | + | readonly property bool focused: { | |
| 111 | + | var act = Hyprland.activeToplevel | |
| 112 | + | if (!act) return false | |
| 113 | + | for (var k = 0; k < modelData.wins.length; k++) | |
| 114 | + | if (modelData.wins[k].address === act.address) return true | |
| 115 | + | return false | |
| 116 | + | } | |
| 117 | + | width: win.size | |
| 118 | + | height: win.size | |
| 119 | + | ||
| 120 | + | Rectangle { | |
| 121 | + | anchors.fill: parent | |
| 122 | + | color: cell.focused ? Theme.accentDim : (cm.containsMouse ? Theme.surfaceHi : "transparent") | |
| 123 | + | } | |
| 124 | + | Image { | |
| 125 | + | id: ico | |
| 126 | + | anchors.centerIn: parent | |
| 127 | + | width: win.size - 12 | |
| 128 | + | height: width | |
| 129 | + | sourceSize.width: 64 | |
| 130 | + | sourceSize.height: 64 | |
| 131 | + | asynchronous: true | |
| 132 | + | source: Quickshell.iconPath(cell.modelData.entry ? cell.modelData.entry.icon : cell.modelData.id.toLowerCase(), true) | |
| 133 | + | } | |
| 134 | + | Text { // icon not in the theme: first letter of the app name | |
| 135 | + | anchors.centerIn: parent | |
| 136 | + | visible: ico.status !== Image.Ready | |
| 137 | + | text: (cell.modelData.entry ? cell.modelData.entry.name : cell.modelData.id).charAt(0).toUpperCase() | |
| 138 | + | color: Theme.accent | |
| 139 | + | font.family: Theme.font | |
| 140 | + | font.pixelSize: 18 | |
| 141 | + | font.bold: true | |
| 142 | + | } | |
| 143 | + | Rectangle { // running marker: accent underline, wider with more windows | |
| 144 | + | visible: cell.running | |
| 145 | + | anchors.bottom: parent.bottom | |
| 146 | + | anchors.horizontalCenter: parent.horizontalCenter | |
| 147 | + | width: Math.min(parent.width - 8, 8 + (cell.modelData.wins.length - 1) * 8) | |
| 148 | + | height: 2 | |
| 149 | + | color: Theme.accent | |
| 150 | + | } | |
| 151 | + | Rectangle { // pinned marker: small dot top-right | |
| 152 | + | visible: cell.modelData.pinned | |
| 153 | + | anchors.top: parent.top | |
| 154 | + | anchors.right: parent.right | |
| 155 | + | anchors.margins: 3 | |
| 156 | + | width: 3 | |
| 157 | + | height: 3 | |
| 158 | + | color: Theme.dim | |
| 159 | + | } | |
| 160 | + | MouseArea { | |
| 161 | + | id: cm | |
| 162 | + | anchors.fill: parent | |
| 163 | + | hoverEnabled: true | |
| 164 | + | acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton | |
| 165 | + | cursorShape: Qt.PointingHandCursor | |
| 166 | + | onEntered: { hide.stop(); win.shown = true } | |
| 167 | + | onClicked: m => { | |
| 168 | + | if (m.button === Qt.RightButton) Settings.togglePin(cell.modelData.id) | |
| 169 | + | else if (m.button === Qt.MiddleButton) { if (cell.modelData.entry) cell.modelData.entry.execute() } | |
| 170 | + | else win.activate(cell.modelData) | |
| 171 | + | } | |
| 172 | + | } | |
| 173 | + | } | |
| 174 | + | } | |
| 175 | + | } | |
| 176 | + | } | |
| 177 | + | } | |
| 178 | + | } | |
| @@ -77,6 +77,11 @@ ShellRoot { | |||
| 77 | 77 | Desktop {} | |
| 78 | 78 | } | |
| 79 | 79 | ||
| 80 | + | Variants { | |
| 81 | + | model: Quickshell.screens | |
| 82 | + | Dock {} | |
| 83 | + | } | |
| 84 | + | ||
| 80 | 85 | Variants { | |
| 81 | 86 | model: Quickshell.screens | |
| 82 | 87 | Bar { | |