feat(quickshell): add application launcher + hover affordance
Update PKGBUILD version / update-pkgver (push) Successful in 6s
Update PKGBUILD version / update-pkgver (push) Successful in 6s
New Launcher bar widget (first in the left cluster) opening a LauncherPopout over DesktopEntries — search field + keyboard-navigable app list, launches via entry.execute(). Icon-click only for now; Mod+Space stays on walker (drun-only scope, walker keeps dmenu/run/calc). Placing a popout in the left cluster forced the shared host anchor to become side-aware: left-cluster triggers left-align (body grows rightward), right cluster keeps the original right-align — otherwise a left-anchored popout renders off-screen. The anchor rect now carries the trigger's left edge + width (PopoutState.openByName, Tray). Clickable bar widgets gain a hover affordance via opacity (0.8), not color: the icon color already encodes status (muted/off/active/critical), so a color-based hover would clobber that signal. Launcher keeps an accent hover as it carries no status color.
This commit is contained in:
@@ -70,13 +70,17 @@ quickshell is the **default bar and notification daemon**, spawned by niri as `q
|
||||
cd defaults/xdg/quickshell/moonarch && for f in *.qml; do [ "$f" = shell.qml ] && continue; n=${f%.qml}; grep -q 'pragma Singleton' "$f" && echo "singleton $n $f" || echo "$n $f"; done | sort > qmldir
|
||||
```
|
||||
|
||||
- **Own popouts:** a composed widget carries its own popout as a `Component` and renders it in the shared morphing host. Pattern (see any indicator, e.g. `AudioIndicator.qml`): an inline `Component { AudioPopout { popouts: root.popouts } }`; the widget declares a `popoutName` and registers on completion (`popouts.register(popoutName, popoutComponent, root, payloadFn)`), then its click handler calls `popouts.toggleByName(popoutName)`. The widget file needs `pragma ComponentBehavior: Bound` so the popout resolves `root` when the shared host instantiates it. The low-level `popouts.open(component, rect, payload)` still exists for non-registered popouts (Tray).
|
||||
- **Own popouts:** a composed widget carries its own popout as a `Component` and renders it in the shared morphing host. Pattern (see any indicator, e.g. `AudioIndicator.qml`): an inline `Component { AudioPopout { popouts: root.popouts } }`; the widget declares a `popoutName` and registers on completion (`popouts.register(popoutName, popoutComponent, root, payloadFn)`), then its click handler calls `popouts.toggleByName(popoutName)`. The widget file needs `pragma ComponentBehavior: Bound` so the popout resolves `root` when the shared host instantiates it. The low-level `popouts.open(component, rect, payload)` still exists for non-registered popouts (Tray). The host anchors the popout body to the trigger's on-screen box and picks the side from it: **left-aligned** when the trigger sits in the left half of the screen (so left-cluster popouts grow rightward and stay on-screen), **right-aligned** otherwise (`PopoutHost.qml` `frame.leftAnchored`). The anchor rect carries the trigger's left edge + width; both `openByName` and Tray's direct `open()` pass that box.
|
||||
|
||||
- **Popouts by keybind:** registered popouts are addressable by name through the `PopoutRouter` singleton, which exposes an `IpcHandler { target: "popout" }` with `toggle`/`open`/`close`. A niri keybind fires `qs -c moonarch ipc call popout toggle <name>` (e.g. `Mod+A` → `audio`, in `defaults/xdg/niri/config.kdl`). The router routes to the focused monitor's `PopoutState` via `Niri.activeOutput` (workspace `is_focused` → `output`, matched against `screen.name`); an open popout grabs the keyboard (Escape closes). Names: `audio backlight network bluetooth nightlight cpugov battery notifications media`. Tray is not keybind-addressable.
|
||||
- **Popouts by keybind:** registered popouts are addressable by name through the `PopoutRouter` singleton, which exposes an `IpcHandler { target: "popout" }` with `toggle`/`open`/`close`. A niri keybind fires `qs -c moonarch ipc call popout toggle <name>` (e.g. `Mod+A` → `audio`, in `defaults/xdg/niri/config.kdl`). The router routes to the focused monitor's `PopoutState` via `Niri.activeOutput` (workspace `is_focused` → `output`, matched against `screen.name`); an open popout grabs the keyboard (Escape closes). Names: `audio backlight network bluetooth nightlight cpugov battery notifications media launcher`. Tray is not keybind-addressable.
|
||||
|
||||
- **Launcher:** `Launcher.qml` is the first widget in the left cluster (an apps glyph) and owns `LauncherPopout.qml` — an application launcher over `DesktopEntries.applications` (search field + keyboard-navigable icon+name list; ↑/↓ select, Enter/click launch via `entry.execute()`, Esc close; app icons via `Quickshell.iconPath`). It registers under `launcher` (so it is IPC-addressable) but is **icon-click only** for now — `Mod+Space` stays on `walker`, which remains the full dmenu/run/calc launcher. Binding a key later is one line in `config.kdl`. Scope is app-launch (drun) only; walker keeps the other modes.
|
||||
|
||||
- **Hover affordance:** clickable bar widgets dim to `opacity: 0.8` on hover (110ms `animDurationShort` fade) so they read as interactive. Deliberately **opacity, not color** — the icon color already encodes status (muted/off → `subtext0`, active → `accent`, battery critical → red), so a color-based hover would clobber the state signal. The Launcher is the exception: no status color, so it uses an `accent` hover instead. Applied per widget (no shared parent to hoist it to); `Workspaces`/`Tray` keep their own per-element hover, non-clickable `PrivacyIndicator`/`WindowTitle` get none.
|
||||
|
||||
- **Keyboard navigation:** an open popout grabs the keyboard, so it is fully operable without the mouse. Controls (`Toggle`, `ListRow`, `VolumeSlider`, `IconButton`) set `activeFocusOnTab` and show a focus ring; `PopoutPanel` focuses the first control on open and maps ↑/↓ onto Qt's native Tab chain (`nextItemInFocusChain` + an `activeFocusOnTab` filter). Keys: Tab/↑/↓ move focus, ←/→ nudge a slider ±5%, Space/Enter activate, Esc closes. Icon actions use the shared `IconButton` (destructive ones set `activeColor: Theme.red`); Rectangle-shaped buttons (clear-all, notification actions) are made focusable inline.
|
||||
|
||||
Rationale + spike results: `DECISIONS.md` (2026-07-05 pilot + user-extension, 2026-07-06 composition + composable popouts, 2026-07-08 keybind registry + IPC router, keyboard-navigable controls).
|
||||
Rationale + spike results: `DECISIONS.md` (2026-07-05 pilot + user-extension, 2026-07-06 composition + composable popouts, 2026-07-08 keybind registry + IPC router, keyboard-navigable controls; launcher widget + side-aware popout anchoring).
|
||||
|
||||
## mpv + ModernZ OSC
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Decisions
|
||||
|
||||
## 2026-07-08 – Quickshell launcher popout + side-aware popout anchoring
|
||||
- **Who**: Dominik, ClaudeCode
|
||||
- **Why**: Wanted an application launcher living in the quickshell bar rather than as a separate overlay. Explored the options together: a bar popout (like the existing audio/network popouts) vs. a keybind-only centered overlay. Chose a bar-popout launcher with a click icon as the **first** widget in the left cluster.
|
||||
- **Tradeoffs**: (1) Placement — keybind-only + centered was deferred as the "complicated" path (centering needs a host change because the body width isn't known until content loads); icon + bar popout reuses the existing register/host/keyboard machinery for free. (2) Keybind — `Mod+Space` stays on `walker`; the quickshell launcher is icon-click only for now. walker remains the full dmenu/run/calc launcher; the quickshell one is app-launch (drun) only. It still registers under `launcher` so wiring a key later is one line. (3) **Side-aware anchoring (forced correctness fix)** — every existing popout sits in the right cluster and the host anchored the body *right-aligned* to the trigger (`x = anchorRect.x - width + flareR`). A left-cluster trigger anchored that way grows leftward off-screen regardless of width. So the far-left placement required generalizing the host anchor: it now derives the side from the trigger's on-screen box (left-align when the trigger is in the left screen half, right-align otherwise). Right-cluster popouts are pixel-identical (recomputed); only the anchor-rect semantics changed (now left-edge + width instead of right-edge). Considered avoiding the host change — impossible, a right-aligned left-edge trigger is always off-screen.
|
||||
- **How**: New `Launcher.qml` (apps glyph, owns `LauncherPopout` as a `Component`, registers `launcher`, click → `toggleByName`) added first in `shell.qml` `leftContent`. New `LauncherPopout.qml` — fixed-size (so the frame doesn't morph per keystroke) `Item` with `focusFirst()`; a `TextInput` search field over `DesktopEntries.applications.values` (filter on name/genericName/comment/keywords, drop `noDisplay`, sort by name), a `ListView` of icon+name rows (`Quickshell.iconPath` for icons); ↑/↓ move selection, Enter/click `entry.execute()` + close, Esc close. Side-aware anchor: `PopoutHost.qml` `frame.leftAnchored` (`anchorMid < root.width/2`) picks left/right x; `PopoutState.openByName` and `Tray.qml`'s direct `open()` now pass the trigger's left-edge+width box. `qmldir` regenerated. Verified via a repo test instance and a throwaway harness that instantiates `LauncherPopout` directly (`quickshell -p`): config + popout load clean, result list + delegates + icon lookup evaluate without QML errors (a few apps lack a themed icon → blank icon, cosmetic). Follow-on UX tweak in the same commit: clickable bar widgets now give a hover affordance via `opacity: 0.8` (not a color change — icon color is the status signal, so a color hover would clobber muted/active/critical state; the Launcher keeps an `accent` hover as it carries no status color). Applied per widget as there is no shared icon parent; `Workspaces`/`Tray` keep their existing per-element hover.
|
||||
|
||||
## 2026-07-08 – Quickshell popouts: keyboard-navigable controls
|
||||
- **Who**: Dominik, ClaudeCode
|
||||
- **Why**: Popouts open by keybind (Mod+A) and grab the keyboard, but nothing inside was operable without a mouse — the shared controls (`Toggle`/`ListRow`/`VolumeSlider`) and ad-hoc glyph buttons were pure `MouseArea`, took no focus, reacted to no key. Opening by key is pointless if you then need the mouse. Goal: full mouse-free operation.
|
||||
|
||||
@@ -6,6 +6,13 @@ import Quickshell.Services.Pipewire
|
||||
|
||||
Item {
|
||||
id: root
|
||||
// Hover affordance — dim slightly on hover so the clickable icon feels interactive (state color untouched).
|
||||
opacity: hover.hovered ? 0.8 : 1.0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.animDurationShort
|
||||
}
|
||||
}
|
||||
property var popouts: null
|
||||
property string popoutName: "audio"
|
||||
|
||||
|
||||
@@ -5,6 +5,13 @@ import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
// Hover affordance — dim slightly on hover so the clickable icon feels interactive (state color untouched).
|
||||
opacity: hover.hovered ? 0.8 : 1.0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.animDurationShort
|
||||
}
|
||||
}
|
||||
property var popouts: null
|
||||
property string popoutName: "backlight"
|
||||
|
||||
|
||||
@@ -6,6 +6,13 @@ import Quickshell.Services.UPower
|
||||
|
||||
Row {
|
||||
id: root
|
||||
// Hover affordance — dim slightly on hover so the clickable icon feels interactive (state color untouched).
|
||||
opacity: hover.hovered ? 0.8 : 1.0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.animDurationShort
|
||||
}
|
||||
}
|
||||
property var popouts: null
|
||||
property string popoutName: "battery"
|
||||
|
||||
|
||||
@@ -6,6 +6,13 @@ import Quickshell.Bluetooth
|
||||
|
||||
Item {
|
||||
id: root
|
||||
// Hover affordance — dim slightly on hover so the clickable icon feels interactive (state color untouched).
|
||||
opacity: hover.hovered ? 0.8 : 1.0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.animDurationShort
|
||||
}
|
||||
}
|
||||
property var popouts: null
|
||||
property string popoutName: "bluetooth"
|
||||
|
||||
|
||||
@@ -5,6 +5,13 @@ import Quickshell
|
||||
|
||||
Text {
|
||||
id: root
|
||||
// Hover affordance — dim slightly on hover so the clickable icon feels interactive (state color untouched).
|
||||
opacity: hover.hovered ? 0.8 : 1.0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.animDurationShort
|
||||
}
|
||||
}
|
||||
property bool showYear: false
|
||||
|
||||
SystemClock {
|
||||
|
||||
@@ -5,6 +5,13 @@ import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
// Hover affordance — dim slightly on hover so the clickable icon feels interactive (state color untouched).
|
||||
opacity: hover.hovered ? 0.8 : 1.0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.animDurationShort
|
||||
}
|
||||
}
|
||||
property var popouts: null
|
||||
property string popoutName: "cpugov"
|
||||
|
||||
|
||||
@@ -4,6 +4,13 @@ import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
// Hover affordance — dim slightly on hover so the clickable icon feels interactive (state color untouched).
|
||||
opacity: hover.hovered ? 0.8 : 1.0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.animDurationShort
|
||||
}
|
||||
}
|
||||
|
||||
implicitWidth: iconText.implicitWidth
|
||||
implicitHeight: Theme.barHeight
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// ABOUTME: Launcher bar widget — an apps glyph that opens the application-launcher popout on click.
|
||||
// ABOUTME: Registers under "launcher" so it is also addressable by keybind via the niri IPC router.
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property var popouts: null
|
||||
property string popoutName: "launcher"
|
||||
|
||||
// Register with the shared popout state so this popout is addressable by keybind (niri IPC).
|
||||
Component.onCompleted: if (root.popouts)
|
||||
root.popouts.register(popoutName, popoutComponent, root, null)
|
||||
|
||||
// This widget's popout — handed to the shared animated host on click.
|
||||
Component {
|
||||
id: popoutComponent
|
||||
LauncherPopout {
|
||||
popouts: root.popouts
|
||||
}
|
||||
}
|
||||
|
||||
implicitWidth: Theme.barHeight
|
||||
implicitHeight: Theme.barHeight
|
||||
|
||||
readonly property bool open: root.popouts && root.popouts.currentName === root.popoutName
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize + 5
|
||||
color: (hover.hovered || root.open) ? Theme.accent : Theme.text
|
||||
text: String.fromCodePoint(0xf14de) // md-rocket-launch
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: hover
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: if (root.popouts)
|
||||
root.popouts.toggleByName(root.popoutName)
|
||||
}
|
||||
Tooltip {
|
||||
anchorItem: root
|
||||
shown: hover.hovered
|
||||
text: qsTr("Applications")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// ABOUTME: Application launcher popout — a search field over DesktopEntries with a keyboard-navigable
|
||||
// ABOUTME: result list (icon + name); Enter/click launches the selected app and closes the popout.
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property var popouts: null
|
||||
|
||||
// Fixed size so the frame does not morph on every keystroke (the host sizes from implicit size).
|
||||
readonly property int panelWidth: 460
|
||||
readonly property int rowHeight: 40
|
||||
readonly property int visibleRows: 8
|
||||
|
||||
implicitWidth: col.width
|
||||
implicitHeight: col.height
|
||||
|
||||
property string query: ""
|
||||
|
||||
// Case-insensitive substring match over name/genericName/comment/keywords.
|
||||
function matches(app, q) {
|
||||
if (!q)
|
||||
return true;
|
||||
const hay = [app.name, app.genericName, app.comment, (app.keywords || []).join(" ")].filter(Boolean).join(" ").toLowerCase();
|
||||
return hay.includes(q);
|
||||
}
|
||||
|
||||
readonly property var results: {
|
||||
const all = DesktopEntries.applications ? DesktopEntries.applications.values : [];
|
||||
const q = root.query.trim().toLowerCase();
|
||||
const out = all.filter(a => a && !a.noDisplay && root.matches(a, q));
|
||||
out.sort((a, b) => (a.name || "").localeCompare(b.name || ""));
|
||||
return out;
|
||||
}
|
||||
|
||||
function launch(app) {
|
||||
if (!app)
|
||||
return;
|
||||
app.execute();
|
||||
if (root.popouts)
|
||||
root.popouts.close();
|
||||
}
|
||||
|
||||
// The host focuses this on open (PopoutHost's focusFirst path) — hand focus to the search field.
|
||||
function focusFirst() {
|
||||
input.forceActiveFocus();
|
||||
}
|
||||
|
||||
// Reset the selection to the top whenever the filtered set changes.
|
||||
onQueryChanged: list.currentIndex = 0
|
||||
|
||||
Column {
|
||||
id: col
|
||||
width: root.panelWidth
|
||||
leftPadding: 12
|
||||
rightPadding: 12
|
||||
topPadding: 12
|
||||
bottomPadding: 12
|
||||
spacing: 8
|
||||
readonly property int innerWidth: width - leftPadding - rightPadding
|
||||
|
||||
// --- Search field ---
|
||||
Rectangle {
|
||||
width: col.innerWidth
|
||||
height: 34
|
||||
radius: Theme.radius
|
||||
color: Theme.surface0
|
||||
border.width: input.activeFocus ? 1 : 0
|
||||
border.color: Theme.accent
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: String.fromCodePoint(0xf0349) // md-magnify
|
||||
color: Theme.subtext0
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize + 2
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: input
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - parent.spacing - 20
|
||||
color: Theme.text
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
clip: true
|
||||
selectByMouse: true
|
||||
activeFocusOnTab: true
|
||||
onTextChanged: root.query = text
|
||||
|
||||
// Placeholder — shown only while empty.
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: input.text.length === 0
|
||||
text: qsTr("Search applications…")
|
||||
color: Theme.subtext0
|
||||
font: input.font
|
||||
}
|
||||
|
||||
Keys.onDownPressed: list.incrementCurrentIndex()
|
||||
Keys.onUpPressed: list.decrementCurrentIndex()
|
||||
Keys.onReturnPressed: root.launch(root.results[list.currentIndex])
|
||||
Keys.onEnterPressed: root.launch(root.results[list.currentIndex])
|
||||
Keys.onEscapePressed: if (root.popouts)
|
||||
root.popouts.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Results ---
|
||||
Item {
|
||||
width: col.innerWidth
|
||||
height: root.rowHeight * root.visibleRows
|
||||
|
||||
ListView {
|
||||
id: list
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
model: root.results
|
||||
currentIndex: 0
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
delegate: Rectangle {
|
||||
id: appRow
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: ListView.view.width
|
||||
height: root.rowHeight
|
||||
radius: Theme.radius
|
||||
color: (appRow.ListView.isCurrentItem || rowHover.hovered) ? Theme.surface0 : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
spacing: 10
|
||||
|
||||
Image {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 24
|
||||
height: 24
|
||||
sourceSize.width: 24
|
||||
sourceSize.height: 24
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: Quickshell.iconPath(appRow.modelData.icon || "", "application-x-executable")
|
||||
}
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 24 - parent.spacing
|
||||
text: appRow.modelData.name || appRow.modelData.id
|
||||
elide: Text.ElideRight
|
||||
color: Theme.text
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: rowHover
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
list.currentIndex = appRow.index;
|
||||
root.launch(appRow.modelData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state — overlays the (empty) list area.
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: root.results.length === 0
|
||||
text: qsTr("No matches")
|
||||
color: Theme.subtext0
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,13 @@ import Quickshell.Services.Mpris
|
||||
|
||||
Item {
|
||||
id: root
|
||||
// Hover affordance — dim slightly on hover so the clickable icon feels interactive (state color untouched).
|
||||
opacity: hover.hovered ? 0.8 : 1.0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.animDurationShort
|
||||
}
|
||||
}
|
||||
property var popouts: null
|
||||
property int maxWidth: 220
|
||||
property string popoutName: "media"
|
||||
|
||||
@@ -6,6 +6,13 @@ import Quickshell.Networking
|
||||
|
||||
Item {
|
||||
id: root
|
||||
// Hover affordance — dim slightly on hover so the clickable icon feels interactive (state color untouched).
|
||||
opacity: hover.hovered ? 0.8 : 1.0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.animDurationShort
|
||||
}
|
||||
}
|
||||
property var popouts: null
|
||||
property string popoutName: "network"
|
||||
|
||||
|
||||
@@ -5,6 +5,13 @@ import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
// Hover affordance — dim slightly on hover so the clickable icon feels interactive (state color untouched).
|
||||
opacity: hover.hovered ? 0.8 : 1.0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.animDurationShort
|
||||
}
|
||||
}
|
||||
property var popouts: null
|
||||
property string popoutName: "nightlight"
|
||||
|
||||
|
||||
@@ -5,6 +5,13 @@ import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
// Hover affordance — dim slightly on hover so the clickable icon feels interactive (state color untouched).
|
||||
opacity: hover.hovered ? 0.8 : 1.0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.animDurationShort
|
||||
}
|
||||
}
|
||||
property var popouts: null
|
||||
property string popoutName: "notifications"
|
||||
|
||||
|
||||
@@ -75,9 +75,15 @@ PanelWindow {
|
||||
readonly property real bodyH: content.implicitHeight + pad * 2
|
||||
property real offset: root.wantOpen ? 0 : 1
|
||||
|
||||
// Anchor side: left-align when the trigger sits in the left half of the screen (left-cluster
|
||||
// popouts grow rightward and stay on-screen), right-align otherwise (the original behavior for
|
||||
// right-cluster widgets). anchorRect is the trigger's on-screen box in this surface's coords.
|
||||
readonly property real anchorMid: root.popouts.anchorRect.x + root.popouts.anchorRect.width / 2
|
||||
readonly property bool leftAnchored: anchorMid < root.width / 2
|
||||
|
||||
clip: true
|
||||
// Body right edge aligns to the trigger icon; the panel is wider by the flare radius each side.
|
||||
x: root.popouts.anchorRect.x - width + flareR
|
||||
// The panel is wider than the body by the flare radius each side; align the body edge to the trigger.
|
||||
x: frame.leftAnchored ? root.popouts.anchorRect.x - flareR : root.popouts.anchorRect.x + root.popouts.anchorRect.width - width + flareR
|
||||
y: 0
|
||||
implicitWidth: bodyW + flareR * 2
|
||||
implicitHeight: bodyH * (1 - offset)
|
||||
|
||||
@@ -58,8 +58,10 @@ QtObject {
|
||||
if (!e || !e.anchorItem)
|
||||
return;
|
||||
const it = e.anchorItem;
|
||||
const pr = it.mapToItem(null, it.width, it.height + 4);
|
||||
root.open(e.component, Qt.rect(pr.x, pr.y, 0, 0), e.payloadFn ? e.payloadFn() : null);
|
||||
// Anchor rect = the widget's on-screen box (top-left below it + its width). The host picks the
|
||||
// alignment side from this, so a left-cluster widget's popout grows rightward and stays on-screen.
|
||||
const tl = it.mapToItem(null, 0, it.height + 4);
|
||||
root.open(e.component, Qt.rect(tl.x, tl.y, it.width, 0), e.payloadFn ? e.payloadFn() : null);
|
||||
root.currentName = name;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,9 +45,10 @@ Row {
|
||||
else if (mouse.button === Qt.MiddleButton)
|
||||
iconItem.modelData.secondaryActivate();
|
||||
else if (mouse.button === Qt.RightButton && iconItem.modelData.hasMenu && root.popouts) {
|
||||
// Icon bottom-right in screen coords (the bar window origin equals the screen origin).
|
||||
const p = iconItem.mapToItem(null, iconItem.width, iconItem.height + 4);
|
||||
root.popouts.open(popoutComponent, Qt.rect(p.x, p.y, 0, 0), iconItem.modelData.menu);
|
||||
// Icon bottom-left + width in screen coords (the bar window origin equals the screen
|
||||
// origin); the host derives the anchor side from this box.
|
||||
const p = iconItem.mapToItem(null, 0, iconItem.height + 4);
|
||||
root.popouts.open(popoutComponent, Qt.rect(p.x, p.y, iconItem.width, 0), iconItem.modelData.menu);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,13 @@ import Quickshell.Io
|
||||
|
||||
Item {
|
||||
id: root
|
||||
// Hover affordance — dim slightly on hover so the clickable icon feels interactive (state color untouched).
|
||||
opacity: hover.hovered ? 0.8 : 1.0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.animDurationShort
|
||||
}
|
||||
}
|
||||
|
||||
property int count: 0
|
||||
property string tip: ""
|
||||
|
||||
@@ -13,6 +13,8 @@ CpuGovPopout CpuGovPopout.qml
|
||||
Divider Divider.qml
|
||||
IconButton IconButton.qml
|
||||
IdleInhibitorIndicator IdleInhibitorIndicator.qml
|
||||
Launcher Launcher.qml
|
||||
LauncherPopout LauncherPopout.qml
|
||||
ListRow ListRow.qml
|
||||
ModuleBox ModuleBox.qml
|
||||
MprisPopout MprisPopout.qml
|
||||
|
||||
@@ -14,6 +14,10 @@ ShellRoot {
|
||||
|
||||
// Left cluster.
|
||||
leftContent: [
|
||||
Launcher {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
popouts: bar.popouts
|
||||
},
|
||||
ModuleBox {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Clock {}
|
||||
|
||||
Reference in New Issue
Block a user