refactor: unified BarModule base component, click-to-open panels, remove pinning
This commit is contained in:
parent
034f0b6d85
commit
26476dc930
33 changed files with 273 additions and 517 deletions
|
|
@ -4,33 +4,19 @@ import Quickshell.Wayland
|
|||
import "." as M
|
||||
import "../services" as S
|
||||
|
||||
// Unified bar panel — fullscreen transparent window so content can resize
|
||||
// freely without triggering Wayland surface resizes.
|
||||
// Bar panel - fullscreen transparent window so content can resize freely
|
||||
// without triggering Wayland surface resizes.
|
||||
//
|
||||
// Hover mode (popupMode: false, default):
|
||||
// Parent drives visibility via showPanel. Panel auto-closes when showPanel
|
||||
// drops, with a 50ms debounce. Reports panelHovered back to parent.
|
||||
// Pass anchorItem for lazy position computation on each show.
|
||||
//
|
||||
// Popup mode (popupMode: true):
|
||||
// Shows immediately on creation. Stays open until click-outside or
|
||||
// dismiss() call. Emits dismissed() when closed — caller's LazyLoader
|
||||
// sets active: false. Pass anchorX (screen-relative centre x).
|
||||
// Parent drives visibility via showPanel. Click-outside or Esc dismisses
|
||||
// and emits dismissed(). Pass anchorItem for lazy position computation.
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property bool popupMode: false
|
||||
|
||||
// Hover mode
|
||||
property bool showPanel: true
|
||||
property bool showPanel: false
|
||||
property Item anchorItem: null
|
||||
property bool panelHovered: false
|
||||
|
||||
// Popup mode
|
||||
property real anchorX: -1
|
||||
signal dismissed
|
||||
|
||||
// Shared
|
||||
required property color accentColor
|
||||
property string panelTitle: ""
|
||||
property Component titleActionsComponent: null
|
||||
|
|
@ -43,22 +29,6 @@ PanelWindow {
|
|||
color: "transparent"
|
||||
|
||||
property bool _winVisible: false
|
||||
property bool _pinned: false
|
||||
property real _dragStartX: 0
|
||||
property real _dragStartY: 0
|
||||
property bool _dragging: false
|
||||
|
||||
// When pinned: mask = full panel so content is interactive, desktop accessible outside.
|
||||
// When dragging: mask = null (full screen) so Niri keeps delivering events when cursor
|
||||
// leaves the panel bounds mid-drag.
|
||||
mask: (_pinned && !_dragging) ? _pinMask : null
|
||||
|
||||
property Region _pinMask: Region {
|
||||
x: panelContainer.x
|
||||
y: panelContainer.y
|
||||
width: panelContainer.width
|
||||
height: panelContainer.height
|
||||
}
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.exclusiveZone: 0
|
||||
|
|
@ -80,38 +50,29 @@ PanelWindow {
|
|||
if (root.anchorItem) {
|
||||
const pt = root.anchorItem.mapToGlobal(root.anchorItem.width / 2, 0);
|
||||
cx = pt.x - (scr?.x ?? 0);
|
||||
} else {
|
||||
} else if (root.anchorX >= 0) {
|
||||
cx = root.anchorX;
|
||||
} else {
|
||||
cx = sw / 2;
|
||||
}
|
||||
panelContainer.x = Math.max(0, Math.min(Math.round(cx - root.contentWidth / 2), sw - root.contentWidth));
|
||||
}
|
||||
|
||||
// Grace period: after _show(), suppress auto-close briefly so Niri has time
|
||||
// to route wl_pointer.enter to the new overlay surface (cursor may be stationary).
|
||||
// Popup mode gets a longer window (1500ms) so the user can move the cursor to the
|
||||
// panel content after clicking the bar without accidentally dismissing it.
|
||||
// Grace period: after _show(), suppress click-outside dismiss briefly so
|
||||
// Niri has time to route wl_pointer.enter to the new overlay surface.
|
||||
property bool _grace: false
|
||||
Timer {
|
||||
id: _graceTimer
|
||||
interval: root.popupMode ? 1500 : 400
|
||||
onTriggered: {
|
||||
root._grace = false;
|
||||
if (!root.showPanel && !root._pinned)
|
||||
root.dismiss();
|
||||
}
|
||||
interval: 400
|
||||
onTriggered: root._grace = false
|
||||
}
|
||||
|
||||
// Content-change grace: call keepOpen(ms) when panel content is about to
|
||||
// resize/rebuild (session switch, device list change, etc.) to prevent the
|
||||
// hover-drop-on-resize from closing the panel.
|
||||
// resize/rebuild (session switch, device list change, etc.).
|
||||
property bool _contentBusy: false
|
||||
Timer {
|
||||
id: _contentBusyTimer
|
||||
onTriggered: {
|
||||
root._contentBusy = false;
|
||||
if (!root.showPanel && !root._grace && !root._pinned)
|
||||
_hideTimer.restart();
|
||||
}
|
||||
onTriggered: root._contentBusy = false
|
||||
}
|
||||
function keepOpen(ms) {
|
||||
_contentBusy = true;
|
||||
|
|
@ -121,13 +82,7 @@ PanelWindow {
|
|||
|
||||
function _show() {
|
||||
_updatePosition();
|
||||
// Only snap to closed position when genuinely opening from scratch.
|
||||
// If we are interrupting a hide animation, animate back from the current
|
||||
// y/opacity so there's no visible jump — the showAnim NumberAnimations
|
||||
// always run from the *current* value to their target.
|
||||
if (!hideAnim.running) {
|
||||
// Explicitly set y before animating — avoids the y:-height binding (live, depends on
|
||||
// _panelColumn.height) surviving a 0→0 no-op animation when layout isn't done yet.
|
||||
panelContainer.y = -(panelContainer.height > 0 ? panelContainer.height : 400);
|
||||
panelContainer.opacity = 0;
|
||||
}
|
||||
|
|
@ -144,12 +99,12 @@ PanelWindow {
|
|||
}
|
||||
|
||||
function dismiss() {
|
||||
_pinned = false;
|
||||
if (!_winVisible)
|
||||
return;
|
||||
showAnim.stop();
|
||||
if (S.Theme.reducedMotion) {
|
||||
_winVisible = false;
|
||||
if (popupMode)
|
||||
dismissed();
|
||||
dismissed();
|
||||
} else {
|
||||
hideAnim.start();
|
||||
}
|
||||
|
|
@ -157,30 +112,12 @@ PanelWindow {
|
|||
_graceTimer.stop();
|
||||
}
|
||||
|
||||
Component.onCompleted: if (popupMode)
|
||||
_show()
|
||||
|
||||
Timer {
|
||||
id: _hideTimer
|
||||
interval: 150
|
||||
onTriggered: if (!root.showPanel && !root._grace && !root._pinned && !root._contentBusy)
|
||||
root.dismiss()
|
||||
}
|
||||
|
||||
onShowPanelChanged: {
|
||||
if (root.popupMode)
|
||||
return;
|
||||
if (showPanel) {
|
||||
_hideTimer.stop();
|
||||
// Only replay the open animation if the panel is actually closed or
|
||||
// currently animating away. If it is already visible, stopping the
|
||||
// hide timer is sufficient — calling _show() would reset y/opacity to
|
||||
// 0 and cause a visible flash when the cursor crosses the gap between
|
||||
// the bar module and the panel.
|
||||
if (!_winVisible || hideAnim.running)
|
||||
_show();
|
||||
} else {
|
||||
_hideTimer.restart();
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -220,17 +157,15 @@ PanelWindow {
|
|||
}
|
||||
onFinished: {
|
||||
root._winVisible = false;
|
||||
if (root.popupMode)
|
||||
root.dismissed();
|
||||
root.dismissed();
|
||||
}
|
||||
}
|
||||
|
||||
// Popup mode: click-outside dismiss.
|
||||
// Click-outside dismiss.
|
||||
// TapHandler fires for all taps; position check skips taps inside panelContainer.
|
||||
// Gated on !_grace so spurious events during the 400ms opening window don't dismiss.
|
||||
// Gated on !_grace so spurious events during the opening window don't dismiss.
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: root.popupMode
|
||||
|
||||
TapHandler {
|
||||
enabled: !root._grace
|
||||
|
|
@ -243,6 +178,13 @@ PanelWindow {
|
|||
}
|
||||
}
|
||||
|
||||
// Esc dismiss
|
||||
Shortcut {
|
||||
sequence: "Escape"
|
||||
enabled: root._winVisible
|
||||
onActivated: root.dismiss()
|
||||
}
|
||||
|
||||
M.PopupBackground {
|
||||
x: panelContainer.x
|
||||
y: panelContainer.y
|
||||
|
|
@ -260,52 +202,17 @@ PanelWindow {
|
|||
height: _panelColumn.height
|
||||
opacity: 0
|
||||
|
||||
HoverHandler {
|
||||
enabled: !root.popupMode && !root._pinned
|
||||
onHoveredChanged: if (!root.popupMode && !root._pinned)
|
||||
root.panelHovered = hovered
|
||||
}
|
||||
|
||||
Column {
|
||||
id: _panelColumn
|
||||
width: root.contentWidth
|
||||
|
||||
// Header row: title + action buttons + pin — shown in hover mode always,
|
||||
// and in popup mode when a title or actions are provided.
|
||||
// Header row: title + action buttons
|
||||
Item {
|
||||
id: _headerItem
|
||||
visible: !root.popupMode || root.panelTitle !== "" || root.titleActionsComponent !== null
|
||||
visible: root.panelTitle !== "" || root.titleActionsComponent !== null
|
||||
width: parent.width
|
||||
height: 24
|
||||
|
||||
// Drag header to freely reposition panel while pinned (hover mode only).
|
||||
// _dragging clears the input mask so Niri keeps delivering events when the
|
||||
// cursor leaves the panel bounds during a fast drag.
|
||||
DragHandler {
|
||||
enabled: root._pinned && !root.popupMode
|
||||
onActiveChanged: {
|
||||
root._dragging = active;
|
||||
if (active) {
|
||||
root._dragStartX = panelContainer.x;
|
||||
root._dragStartY = panelContainer.y;
|
||||
}
|
||||
}
|
||||
onActiveTranslationChanged: {
|
||||
if (active) {
|
||||
const sw = root.screen?.width ?? 1920;
|
||||
const sh = root.screen?.height ?? 1080;
|
||||
panelContainer.x = Math.max(0, Math.min(root._dragStartX + activeTranslation.x, sw - root.contentWidth));
|
||||
panelContainer.y = Math.max(0, Math.min(root._dragStartY + activeTranslation.y, sh - panelContainer.height));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show move cursor on header when pinned
|
||||
HoverHandler {
|
||||
enabled: root._pinned && !root.popupMode
|
||||
cursorShape: Qt.SizeAllCursor
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.panelTitle !== ""
|
||||
anchors.left: parent.left
|
||||
|
|
@ -318,52 +225,14 @@ PanelWindow {
|
|||
font.family: S.Theme.fontFamily
|
||||
}
|
||||
|
||||
// Action buttons — anchored left of pin button slot
|
||||
Loader {
|
||||
id: _titleActionsLoader
|
||||
anchors.right: _pinBtn.left
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
sourceComponent: root.titleActionsComponent
|
||||
}
|
||||
|
||||
// Pin button — zero-width in popup mode so actions anchor flush to right
|
||||
Item {
|
||||
id: _pinBtn
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: root.popupMode ? 0 : 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: root.popupMode ? 0 : 20
|
||||
height: 20
|
||||
visible: !root.popupMode
|
||||
|
||||
HoverHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
root._pinned = !root._pinned;
|
||||
if (!root._pinned && !root.showPanel)
|
||||
root.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root._pinned ? "\uDB81\uDC03" : "\uDB82\uDD31"
|
||||
color: root._pinned ? root.accentColor : S.Theme.base04
|
||||
font.pixelSize: S.Theme.fontSize - 1
|
||||
font.family: S.Theme.iconFontFamily
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Divider at bottom of header
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
|
@ -391,7 +260,7 @@ PanelWindow {
|
|||
}
|
||||
}
|
||||
|
||||
// Border overlay — on top of content so full-bleed items don't cover it
|
||||
// Border overlay - on top of content so full-bleed items don't cover it
|
||||
Rectangle {
|
||||
x: panelContainer.x
|
||||
y: panelContainer.y
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue