feat(quickshell): Alt+Tab overview of every monitor and workspace
Each monitor is a row of workspace cards at its own aspect ratio, with live captures of the windows where they sit, including windows on hidden workspaces. Alt+Tab opens it with the previous window picked, further Tabs walk the focus history, and releasing Alt switches. Super+W opens it without the hold. Hover, click, Enter and the workspace digits work too. Group cycling moves from Alt+Tab to Super+Tab and Super+Shift+Tab.
This commit is contained in:
1 parent
e9735d1129
commit
b69f4bcf0d
7 files changed
+637
-2
No files matched your search
@@ -66,6 +66,7 @@ bar/ the two surfaces and their modules
|
||||
popouts/ hover panels hanging off rail and bar modules
|
||||
overlays/ power menu, notification toasts, volume/brightness OSD
|
||||
lock/ session lock
|
||||
overview/ every monitor, workspace and window at once
|
||||
backdrop/ the wallpaper, drawn by the shell
|
||||
assets/ halftone and stripe tiles, generated with ImageMagick
|
||||
```
|
||||
@@ -144,6 +145,9 @@ Set in `~/.config/hypr/lua/binds/shell.lua`:
|
||||
- `Super+Ctrl+L` — lock
|
||||
- `Super+P` — power menu
|
||||
- `Super+Shift+N` — toggle do not disturb
|
||||
- `Alt+Tab` / `Alt+Shift+Tab` — overview, walking focus history; let go of Alt to switch
|
||||
- `Super+W` — overview, without the hold
|
||||
- `Super+Tab` / `Super+Shift+Tab` — next / previous window in a group
|
||||
- `Super+Shift+I` — stay awake (no dim, lock or screen-off until toggled back)
|
||||
- `Super+Ctrl+Shift+N` — clear notifications
|
||||
- `Super+A` — ask panel
|
||||
@@ -215,6 +219,27 @@ NetworkManager for Wi-Fi, UPower for battery, MPRIS for media, and Quickshell's
|
||||
own notification server. These are event-driven, so the bar and OSD react the
|
||||
instant a volume key is pressed instead of on the next poll.
|
||||
|
||||
## Overview
|
||||
|
||||
`overview/Overview.qml`. Each monitor is a row of small portholes, one per
|
||||
workspace at that monitor's aspect ratio, with live captures of the windows
|
||||
where they sit (`ScreencopyView`, so windows on hidden workspaces show too).
|
||||
The monitor's name heads its row once there is more than one. A group shows
|
||||
the member in front with a `1/3` tag; picking a hidden member puts it in
|
||||
front.
|
||||
|
||||
`Alt+Tab` opens it with the previous window picked, more Tabs walk back
|
||||
through focus history, and letting go of Alt switches. Hyprland keeps the
|
||||
Alt+Tab bind while the overview has the keyboard, so those presses arrive
|
||||
over IPC (`overviewNext` / `overviewPrev`); the Alt release comes to the
|
||||
overview itself, with a release bind on `ALT_L` as a backstop. Hover picks
|
||||
only once the pointer moves, so a resting pointer cannot override the
|
||||
keyboard. Enter, a click, or `1`–`9` for a workspace also go; Escape or a
|
||||
click on the dark cancels.
|
||||
|
||||
Focus is dispatched a moment after the overview lets go of the keyboard:
|
||||
Hyprland ignores focus changes while a layer holds it exclusively.
|
||||
|
||||
## Wallpaper
|
||||
|
||||
`backdrop/Backdrop.qml` draws it on the background layer; there is no
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
import "root:/config"
|
||||
import "root:/components"
|
||||
import "root:/services"
|
||||
|
||||
// Every monitor, every workspace, every window, at once. Alt+Tab opens it on
|
||||
// the focused screen with the previous window picked; more Tabs walk back
|
||||
// through focus history, and letting go of Alt goes there. Hover and click
|
||||
// work too, as do Enter, Escape and the workspace digits.
|
||||
//
|
||||
// It is a scene, like the power menu: no panel, the desktop dims, and each
|
||||
// monitor is laid out as a row of small portholes — its workspaces, at its
|
||||
// own aspect ratio, with live captures of the windows where they sit. The
|
||||
// picked window is ringed in the light and named underneath.
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
|
||||
readonly property bool open: Actions.overviewOpen
|
||||
&& Actions.overviewScreen === modelData.name
|
||||
visible: root.open || fade.running
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.namespace: "takemi-overview"
|
||||
WlrLayershell.keyboardFocus: root.open
|
||||
? WlrKeyboardFocus.Exclusive
|
||||
: WlrKeyboardFocus.None
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "transparent"
|
||||
|
||||
// ----------------------------------------------------------------- model
|
||||
// Rebuilt from Hyprland's IPC snapshots on open and whenever windows move
|
||||
// while open. Plain arrays rather than live models: everything here is a
|
||||
// picture of one moment.
|
||||
property var monitors: [] // [{ mon, ipc, workspaces: [{ ws, windows: [...] }] }]
|
||||
property var mru: [] // window addresses, most recent first
|
||||
property string picked: "" // address of the picked window
|
||||
|
||||
readonly property var pickedWindow: {
|
||||
for (const m of root.monitors)
|
||||
for (const w of m.workspaces)
|
||||
for (const win of w.windows)
|
||||
if (win.address === root.picked) return win;
|
||||
return null;
|
||||
}
|
||||
|
||||
function rebuild() {
|
||||
const mons = Hyprland.monitors.values.slice()
|
||||
.filter(m => m.lastIpcObject && m.lastIpcObject.width)
|
||||
.sort((a, b) => a.lastIpcObject.x - b.lastIpcObject.x);
|
||||
const all = [];
|
||||
const out = [];
|
||||
for (const mon of mons) {
|
||||
const mi = mon.lastIpcObject;
|
||||
const wss = Hyprland.workspaces.values
|
||||
.filter(ws => ws.monitor === mon)
|
||||
.filter(ws => ws.id > 0 || ws.toplevels.values.length > 0)
|
||||
.sort((a, b) => (a.id < 0) - (b.id < 0) || a.id - b.id);
|
||||
const rows = [];
|
||||
for (const ws of wss) {
|
||||
const wins = [];
|
||||
for (const tl of ws.toplevels.values) {
|
||||
const ipc = tl.lastIpcObject;
|
||||
if (!ipc || !ipc.at || !ipc.mapped) continue;
|
||||
const win = {
|
||||
toplevel: tl,
|
||||
address: "0x" + String(tl.address).replace(/^0x/, ""),
|
||||
x: ipc.at[0] - mi.x, y: ipc.at[1] - mi.y,
|
||||
w: ipc.size[0], h: ipc.size[1],
|
||||
title: ipc.title || "",
|
||||
appClass: ipc.class || "",
|
||||
hidden: ipc.hidden === true,
|
||||
floating: ipc.floating === true,
|
||||
group: (ipc.grouped || []).map(a => "0x" + String(a).replace(/^0x/, "")),
|
||||
history: ipc.focusHistoryID ?? 999,
|
||||
};
|
||||
wins.push(win);
|
||||
all.push(win);
|
||||
}
|
||||
// Tiled first, floating on top, as Hyprland draws them.
|
||||
wins.sort((a, b) => a.floating - b.floating);
|
||||
rows.push({ ws: ws, windows: wins });
|
||||
}
|
||||
out.push({ mon: mon, ipc: mi, workspaces: rows });
|
||||
}
|
||||
root.monitors = out;
|
||||
root.mru = all.sort((a, b) => a.history - b.history).map(w => w.address);
|
||||
if (!root.touched || root.mru.indexOf(root.picked) < 0)
|
||||
root.picked = root.mru.length > 1 ? root.mru[1] : (root.mru[0] ?? "");
|
||||
}
|
||||
|
||||
function step(delta: int) {
|
||||
root.touched = true;
|
||||
const n = root.mru.length;
|
||||
if (n === 0) return;
|
||||
const at = Math.max(0, root.mru.indexOf(root.picked));
|
||||
root.picked = root.mru[(at + delta + n) % n];
|
||||
}
|
||||
|
||||
// Hyprland will not move focus while a layer holds the keyboard
|
||||
// exclusively, so let go of it first and dispatch a moment later.
|
||||
property string pendingDispatch: ""
|
||||
|
||||
function commit() {
|
||||
root.pendingDispatch = root.picked !== ""
|
||||
? "hl.dsp.focus({ window = \"address:" + root.picked + "\" })"
|
||||
: "";
|
||||
Actions.closeOverview();
|
||||
if (root.pendingDispatch !== "") release.restart();
|
||||
}
|
||||
|
||||
function goToWorkspace(id: int) {
|
||||
root.pendingDispatch = "hl.dsp.focus({ workspace = " + id + " })";
|
||||
Actions.closeOverview();
|
||||
release.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: release
|
||||
interval: 40
|
||||
onTriggered: Hyprland.dispatch(root.pendingDispatch)
|
||||
}
|
||||
|
||||
// Whether the pointer has moved since the overview opened. A pointer that
|
||||
// happens to rest over a card would otherwise pick that window the moment
|
||||
// the cards appear under it, overriding Alt+Tab's choice.
|
||||
property bool pointerLive: false
|
||||
property point pointerStart: Qt.point(-1, -1)
|
||||
|
||||
// Whether the pick came from a person rather than from the first snapshot.
|
||||
property bool touched: false
|
||||
|
||||
onOpenChanged: {
|
||||
if (root.open) {
|
||||
PopupCoordinator.dismissAll();
|
||||
Hyprland.refreshMonitors();
|
||||
Hyprland.refreshWorkspaces();
|
||||
Hyprland.refreshToplevels();
|
||||
root.picked = "";
|
||||
root.touched = false;
|
||||
root.pointerLive = false;
|
||||
root.pointerStart = Qt.point(-1, -1);
|
||||
root.rebuild();
|
||||
// The IPC snapshots land a moment after the refresh request.
|
||||
settle.restart();
|
||||
keys.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: settle
|
||||
interval: 60
|
||||
onTriggered: root.rebuild()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Actions
|
||||
function onOverviewStepped(delta) { if (root.open) root.step(delta); }
|
||||
function onOverviewCommitted() { if (root.open) root.commit(); }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Hyprland
|
||||
enabled: root.open
|
||||
function onRawEvent(event) {
|
||||
const n = event.name;
|
||||
if (n === "openwindow" || n === "closewindow" || n === "movewindowv2"
|
||||
|| n === "changefloatingmode" || n === "fullscreen") {
|
||||
Hyprland.refreshToplevels();
|
||||
settle.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- backdrop
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.ink
|
||||
opacity: root.open ? 0.92 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { id: fade; duration: Theme.durBase; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
TapHandler { onTapped: Actions.closeOverview() }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- scene
|
||||
Item {
|
||||
id: keys
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
HoverHandler {
|
||||
onPointChanged: {
|
||||
const p = point.position;
|
||||
if (root.pointerStart.x < 0) { root.pointerStart = p; return; }
|
||||
if (Math.abs(p.x - root.pointerStart.x) + Math.abs(p.y - root.pointerStart.y) > 6)
|
||||
root.pointerLive = true;
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
const k = event.key;
|
||||
if (k === Qt.Key_Escape) Actions.closeOverview();
|
||||
else if (k === Qt.Key_Return || k === Qt.Key_Enter || k === Qt.Key_Space) root.commit();
|
||||
else if (k === Qt.Key_Tab || k === Qt.Key_Right || k === Qt.Key_Down
|
||||
|| k === Qt.Key_L || k === Qt.Key_J) root.step(1);
|
||||
else if (k === Qt.Key_Backtab || k === Qt.Key_Left || k === Qt.Key_Up
|
||||
|| k === Qt.Key_H || k === Qt.Key_K) root.step(-1);
|
||||
else if (k >= Qt.Key_1 && k <= Qt.Key_9) root.goToWorkspace(k - Qt.Key_0);
|
||||
else return;
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
// Alt+Tab is a hold: letting go of Alt is the choice.
|
||||
Keys.onReleased: event => {
|
||||
if (event.key === Qt.Key_Alt && Actions.overviewHold) {
|
||||
event.accepted = true;
|
||||
root.commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: scene
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.padXL
|
||||
opacity: root.open ? 1 : 0
|
||||
|
||||
transform: Translate {
|
||||
y: root.open ? 0 : 10
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
// The widest a row of cards may be, and how tall all rows together.
|
||||
readonly property real maxRowWidth: root.width * 0.86
|
||||
readonly property real maxRowsHeight: root.height * 0.62
|
||||
|
||||
// One scale for every card on every monitor, so a window is the same
|
||||
// size wherever it sits: the largest that fits both the widest row and
|
||||
// the summed heights.
|
||||
readonly property real k: {
|
||||
let k = 0.25;
|
||||
let totalH = 0;
|
||||
for (const m of root.monitors) {
|
||||
const w = m.ipc.width / m.ipc.scale;
|
||||
const h = m.ipc.height / m.ipc.scale;
|
||||
const n = Math.max(1, m.workspaces.length);
|
||||
k = Math.min(k, (scene.maxRowWidth - (n - 1) * Theme.padL) / (n * w));
|
||||
totalH += h;
|
||||
}
|
||||
if (totalH > 0) k = Math.min(k, scene.maxRowsHeight / totalH);
|
||||
return Math.max(0.05, k);
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.monitors
|
||||
|
||||
Column {
|
||||
id: monRow
|
||||
required property var modelData
|
||||
readonly property real logicalW: modelData.ipc.width / modelData.ipc.scale
|
||||
readonly property real logicalH: modelData.ipc.height / modelData.ipc.scale
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.padS
|
||||
|
||||
// The monitor's name only matters once there is more than one.
|
||||
Text {
|
||||
visible: root.monitors.length > 1
|
||||
text: (monRow.modelData.mon.name + " · "
|
||||
+ Math.round(monRow.logicalW) + "×" + Math.round(monRow.logicalH)).toUpperCase()
|
||||
color: Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
font.letterSpacing: 2
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.padL
|
||||
|
||||
Repeater {
|
||||
model: monRow.modelData.workspaces
|
||||
|
||||
WorkspaceCard {
|
||||
required property var modelData
|
||||
ws: modelData.ws
|
||||
windows: modelData.windows
|
||||
k: scene.k
|
||||
logicalW: monRow.logicalW
|
||||
logicalH: monRow.logicalH
|
||||
current: monRow.modelData.mon.activeWorkspace === modelData.ws
|
||||
|| (modelData.ws.id < 0 && monRow.modelData.ipc.specialWorkspace?.id === modelData.ws.id)
|
||||
picked: root.picked
|
||||
onPick: address => {
|
||||
if (!root.pointerLive) return;
|
||||
root.touched = true;
|
||||
root.picked = address;
|
||||
}
|
||||
onActivate: address => { root.picked = address; root.commit(); }
|
||||
onActivateWorkspace: id => root.goToWorkspace(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ caption
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 4
|
||||
width: scene.maxRowWidth * 0.7
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.pickedWindow ? root.pickedWindow.title : "No windows"
|
||||
color: Theme.accent
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.fontDisplay
|
||||
font.pixelSize: Theme.fsTitle
|
||||
font.weight: Font.Light
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: root.pickedWindow !== null
|
||||
text: root.pickedWindow
|
||||
? (root.pickedWindow.appClass + " · workspace "
|
||||
+ (root.pickedWindow.toplevel.workspace?.name ?? "?")).toUpperCase()
|
||||
: ""
|
||||
color: Theme.muted
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
font.letterSpacing: 2
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "TAB NEXT ENTER GO 1–9 WORKSPACE ESC CANCEL"
|
||||
color: Theme.overlay
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
font.letterSpacing: 2
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
import "root:/config"
|
||||
import "root:/services"
|
||||
|
||||
// One workspace in the overview: a small porthole at its monitor's aspect
|
||||
// ratio, the wallpaper faint behind it, and its windows as live captures where
|
||||
// they sit. Under it, the workspace's number in the bar's pill when it is the
|
||||
// one on screen.
|
||||
Item {
|
||||
id: card
|
||||
|
||||
required property HyprlandWorkspace ws
|
||||
required property var windows
|
||||
required property real k // logical px -> card px
|
||||
required property real logicalW
|
||||
required property real logicalH
|
||||
property bool current: false
|
||||
property string picked: ""
|
||||
|
||||
signal pick(string address)
|
||||
signal activate(string address)
|
||||
signal activateWorkspace(int id)
|
||||
|
||||
readonly property bool special: card.ws.id < 0
|
||||
// The picked window, if it is one of the hidden members of a group here:
|
||||
// the group's slot shows it instead of the member that is in front.
|
||||
readonly property var pickedGroup: {
|
||||
for (const w of card.windows)
|
||||
if (w.address === card.picked && w.group.length > 1) return w.group;
|
||||
return [];
|
||||
}
|
||||
|
||||
implicitWidth: Math.round(card.logicalW * card.k)
|
||||
implicitHeight: porthole.height + label.anchors.topMargin + label.height
|
||||
|
||||
Rectangle {
|
||||
id: porthole
|
||||
width: card.implicitWidth
|
||||
height: Math.round(card.logicalH * card.k)
|
||||
radius: Theme.radiusM
|
||||
color: Theme.mantle
|
||||
border.width: 1
|
||||
border.color: card.current ? Theme.outline : Theme.rim
|
||||
clip: true
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
source: Wallpaper.source
|
||||
sourceSize: Qt.size(width, height)
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
opacity: 0.35
|
||||
}
|
||||
|
||||
// Click on bare wallpaper: go to the workspace itself.
|
||||
TapHandler {
|
||||
onTapped: if (!card.special) card.activateWorkspace(card.ws.id)
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: card.windows
|
||||
|
||||
Rectangle {
|
||||
id: tile
|
||||
required property var modelData
|
||||
readonly property bool isPicked: tile.modelData.address === card.picked
|
||||
// A group draws one member: the picked one if it is in this
|
||||
// group, otherwise whichever Hyprland has in front.
|
||||
visible: card.pickedGroup.length > 0 && card.pickedGroup.indexOf(tile.modelData.address) >= 0
|
||||
? tile.isPicked
|
||||
: !tile.modelData.hidden
|
||||
|
||||
x: Math.round(tile.modelData.x * card.k)
|
||||
y: Math.round(tile.modelData.y * card.k)
|
||||
width: Math.max(8, Math.round(tile.modelData.w * card.k))
|
||||
height: Math.max(8, Math.round(tile.modelData.h * card.k))
|
||||
z: tile.isPicked ? 2 : (tile.modelData.floating ? 1 : 0)
|
||||
radius: Theme.radiusS
|
||||
color: Theme.surface
|
||||
clip: true
|
||||
|
||||
// The app's icon, under the capture, for the moment before the
|
||||
// first frame lands or for a window that cannot be captured.
|
||||
Image {
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(32, parent.width * 0.4)
|
||||
height: width
|
||||
sourceSize: Qt.size(width, height)
|
||||
source: {
|
||||
const e = DesktopEntries.heuristicLookup(tile.modelData.appClass);
|
||||
return e && e.icon ? Quickshell.iconPath(e.icon, true) : "";
|
||||
}
|
||||
visible: !capture.hasContent
|
||||
}
|
||||
|
||||
ScreencopyView {
|
||||
id: capture
|
||||
anchors.fill: parent
|
||||
captureSource: tile.modelData.toplevel.wayland
|
||||
live: true
|
||||
constraintSize: Qt.size(tile.width, tile.height)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: "transparent"
|
||||
border.width: tile.isPicked ? 2 : 1
|
||||
border.color: tile.isPicked ? Theme.accent : Theme.alpha(Theme.ink, 0.6)
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation { duration: Theme.durFast }
|
||||
}
|
||||
}
|
||||
|
||||
// Everything else sinks back a little.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Theme.ink
|
||||
opacity: tile.isPicked ? 0 : 0.28
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Theme.durFast }
|
||||
}
|
||||
}
|
||||
|
||||
// How many windows share this slot.
|
||||
Rectangle {
|
||||
visible: tile.modelData.group.length > 1
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 4
|
||||
width: groupText.implicitWidth + 8
|
||||
height: groupText.implicitHeight + 2
|
||||
radius: height / 2
|
||||
color: Theme.alpha(Theme.ink, 0.75)
|
||||
|
||||
Text {
|
||||
id: groupText
|
||||
anchors.centerIn: parent
|
||||
text: (tile.modelData.group.indexOf(tile.modelData.address) + 1)
|
||||
+ "/" + tile.modelData.group.length
|
||||
color: Theme.subtext
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fsMicro
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: if (hovered) card.pick(tile.modelData.address)
|
||||
// Hovered from the start, then the pointer moves: still a pick.
|
||||
onPointChanged: if (hovered) card.pick(tile.modelData.address)
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: card.activate(tile.modelData.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The workspace's name: the focused one wears it in the bar's light pill,
|
||||
// the rest in grey.
|
||||
Rectangle {
|
||||
id: label
|
||||
anchors.top: porthole.bottom
|
||||
anchors.topMargin: Theme.padS
|
||||
anchors.horizontalCenter: porthole.horizontalCenter
|
||||
width: Math.max(height * 1.6, labelText.implicitWidth + 14)
|
||||
height: labelText.implicitHeight + 4
|
||||
radius: height / 2
|
||||
color: card.current ? Theme.accent : "transparent"
|
||||
|
||||
Text {
|
||||
id: labelText
|
||||
anchors.centerIn: parent
|
||||
text: card.special ? card.ws.name.replace(/^special:/, "") : String(card.ws.id)
|
||||
color: card.current ? Theme.ink : Theme.muted
|
||||
font.family: card.special ? Theme.fontMono : Theme.fontDisplay
|
||||
font.pixelSize: Theme.fsSmall
|
||||
font.weight: Theme.weightDisplay
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,9 @@ Singleton {
|
||||
property bool launcherOpen: false
|
||||
property bool askOpen: false
|
||||
property bool dashboardOpen: false
|
||||
property bool overviewOpen: false
|
||||
// Opened by Alt+Tab, so letting go of Alt picks the window.
|
||||
property bool overviewHold: false
|
||||
|
||||
// Seed text for the next launcher open. Lets a keybind or a script hand the
|
||||
// launcher a query it did not have to type — `qs ipc call shell prompt
|
||||
@@ -40,6 +43,9 @@ Singleton {
|
||||
// …and for the dashboard drawer.
|
||||
property string dashboardScreen: ""
|
||||
|
||||
// …and for the overview.
|
||||
property string overviewScreen: ""
|
||||
|
||||
// When the dashboard was last closed from outside. A click on the bar's
|
||||
// clock first clears the drawer's focus grab, which closes it, and then
|
||||
// lands on the clock, which would toggle it straight back open. A toggle
|
||||
@@ -51,6 +57,8 @@ Singleton {
|
||||
property string askSeed: ""
|
||||
|
||||
signal osdRequested(string kind)
|
||||
signal overviewStepped(int delta)
|
||||
signal overviewCommitted()
|
||||
|
||||
function togglePowerMenu() {
|
||||
if (!root.powerMenuOpen)
|
||||
@@ -88,6 +96,29 @@ Singleton {
|
||||
root.dashboardClosedAt = Date.now();
|
||||
}
|
||||
|
||||
function toggleOverview() {
|
||||
if (!root.overviewOpen) {
|
||||
root.overviewHold = false;
|
||||
root.overviewScreen = root.focusedScreenName();
|
||||
}
|
||||
root.overviewOpen = !root.overviewOpen;
|
||||
}
|
||||
function closeOverview() { root.overviewOpen = false; }
|
||||
|
||||
// Alt+Tab and Alt+Shift+Tab. The first press opens the overview with the
|
||||
// previous window picked; Hyprland keeps the bind while it is open, so
|
||||
// later presses arrive here too and walk the focus history.
|
||||
function overviewStep(delta: int) {
|
||||
if (!root.overviewOpen) {
|
||||
root.overviewHold = true;
|
||||
root.overviewScreen = root.focusedScreenName();
|
||||
root.overviewOpen = true;
|
||||
if (delta < 0) root.overviewStepped(-2);
|
||||
return;
|
||||
}
|
||||
root.overviewStepped(delta);
|
||||
}
|
||||
|
||||
function toggleAsk() {
|
||||
if (!root.askOpen) {
|
||||
root.askSeed = "";
|
||||
@@ -164,6 +195,10 @@ Singleton {
|
||||
function toggleDnd(): void { Notifs.toggleDnd(); }
|
||||
function clearNotifications(): void { Notifs.clearAll(); }
|
||||
function toggleStayAwake(): void { Idle.toggleStayAwake(); }
|
||||
function overview(): void { root.toggleOverview(); }
|
||||
function overviewNext(): void { root.overviewStep(1); }
|
||||
function overviewPrev(): void { root.overviewStep(-1); }
|
||||
function overviewCommit(): void { if (root.overviewHold) root.overviewCommitted(); }
|
||||
function wallpaper(path: string): void { Wallpaper.set(path); }
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import "root:/bar"
|
||||
import "root:/launcher"
|
||||
import "root:/dashboard"
|
||||
import "root:/ask"
|
||||
import "root:/overview"
|
||||
import "root:/overlays"
|
||||
import "root:/lock"
|
||||
|
||||
@@ -18,6 +19,7 @@ import "root:/lock"
|
||||
// popouts/ hover panels hanging off bar modules
|
||||
// launcher/ the application launcher
|
||||
// dashboard/ the drawer that drops from the bar
|
||||
// overview/ every monitor, workspace and window at once (Alt+Tab)
|
||||
// ask/ the question panel, dropped from the top edge
|
||||
// overlays/ power menu, notification toasts, OSD
|
||||
// lock/ session lock
|
||||
@@ -84,6 +86,12 @@ ShellRoot {
|
||||
AskPanel {}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
Overview {}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
|
||||
Reference in new issue
Block a user