quickshell: click-outside dropdown dismissal, drop auto-close timers

Extend the launcher/session HyprlandFocusGrab to the bar dropdowns and
remove the per-dropdown inactivity timers. Shift+Super+S brackets hyprshot
with a screenshot pin so open menus survive slurp's input grab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
rope 2026-06-17 14:19:10 +01:00
parent 215239e7aa
commit 83b4c5ef09
2 changed files with 36 additions and 40 deletions

View file

@ -281,7 +281,9 @@ in
end
-- Screenshots Shift+Super+S matches GNOME binding
hl.bind(mod .. " + SHIFT + S", hl.dsp.exec_cmd("hyprshot -m region --clipboard-only"))
-- Pin/unpin quickshell's focus grab around the region select so an
-- open menu survives slurp's input grab (no-ops if qs isn't up).
hl.bind(mod .. " + SHIFT + S", hl.dsp.exec_cmd("sh -c 'qs ipc call screenshot pin; hyprshot -m region --clipboard-only; qs ipc call screenshot unpin'"))
hl.bind("Print", hl.dsp.exec_cmd("hyprshot -m output --clipboard-only"))
-- Settings shortcut Super+I matches GNOME binding

View file

@ -238,6 +238,16 @@ in
function reload(): void { Quickshell.reload(false); }
}
// Screenshot pin: the Shift+Super+S keybind brackets hyprshot
// with pin/unpin so the focus grab is suspended while slurp
// grabs input otherwise the open menu would close like any
// click-outside. Self-heals via a watchdog if unpin is missed.
IpcHandler {
target: "screenshot"
function pin(): void { if (root.mainBar) root.mainBar.setScreenshotPin(true); }
function unpin(): void { if (root.mainBar) root.mainBar.setScreenshotPin(false); }
}
NotificationServer {
id: _notifServer
bodySupported: true
@ -291,7 +301,7 @@ in
// (caelestia's): the grab redirects focus to this window and
// OnDemand lets the layer surface accept it. Exclusive fights
// the grab it self-clears and instantly closes the panel.
WlrLayershell.keyboardFocus: sessionMenu.open || launcherPanel.open ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
WlrLayershell.keyboardFocus: (sessionMenu.open || launcherPanel.open || bar.activeDropdown !== null) && !bar.screenshotPinned ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
anchors {
top: true
@ -910,18 +920,38 @@ in
}
}
// Click-outside dismissal for the keyboard-grabbing panels
// Click-outside dismissal for every panel the launcher,
// session menu AND the bar dropdowns. Clicking into another
// window (or anywhere outside the bar) clears the grab and
// closes whatever is open. Suspended while screenshotPinned so
// slurp can grab input without dismissing the menu.
HyprlandFocusGrab {
active: sessionMenu.open || launcherPanel.open
active: (sessionMenu.open || launcherPanel.open || bar.activeDropdown !== null) && !bar.screenshotPinned
windows: [bar]
onCleared: {
if (bar.screenshotPinned) return;
sessionMenu.open = false;
launcherPanel.open = false;
bar.closeAllDropdowns();
}
}
property var activeDropdown: null
// Set by the screenshot keybind (via IPC) to hold menus open
// while a region screenshot runs. Watchdog unpins if the
// bracketing unpin call is ever missed.
property bool screenshotPinned: false
Timer {
id: _pinWatchdog
interval: 30000
onTriggered: bar.screenshotPinned = false
}
function setScreenshotPin(v) {
screenshotPinned = v;
if (v) _pinWatchdog.restart(); else _pinWatchdog.stop();
}
function closeAllDropdowns() {
if (activeDropdown && activeDropdown.visible) {
activeDropdown.animateClose();
@ -1024,7 +1054,6 @@ in
onEntered: {
if (bar.activeDropdown) {
if (bar.activeDropdown !== calPopup) bar.toggleDropdown(calPopup, function() { calPopup.resetView(); });
else bar.activeDropdown.resetAutoClose();
}
}
}
@ -1104,7 +1133,6 @@ in
onEntered: {
if (bar.activeDropdown) {
if (bar.activeDropdown !== volDropdown) volWidget.openVolDropdown();
else bar.activeDropdown.resetAutoClose();
}
}
}
@ -1271,7 +1299,6 @@ in
onEntered: {
if (bar.activeDropdown) {
if (bar.activeDropdown !== netDropdown) netWidget.openNetDropdown();
else bar.activeDropdown.resetAutoClose();
}
}
}
@ -1352,7 +1379,6 @@ in
onEntered: {
if (bar.activeDropdown) {
if (bar.activeDropdown !== batteryDropdown) batteryWidget.openBatteryDropdown();
else bar.activeDropdown.resetAutoClose();
}
}
}
@ -1366,12 +1392,6 @@ in
height: Theme.barHeight
anchors.verticalCenter: parent.verticalCenter
HoverHandler {
onHoveredChanged: {
if (hovered && bar.activeDropdown) bar.activeDropdown.resetAutoClose();
}
}
Repeater {
model: SystemTray.items
@ -1405,7 +1425,6 @@ in
acceptedButtons: Qt.NoButton
onEntered: {
if (bar.activeDropdown) {
bar.activeDropdown.resetAutoClose();
if (modelData.hasMenu && !(bar.activeDropdown === contextMenu && contextMenu.trayItem === modelData)) {
if (bar.activeDropdown === contextMenu) {
// Same dropdown, just switch content
@ -1413,7 +1432,6 @@ in
contextMenu.dropdownX = pos.x;
contextMenu.trayItem = modelData;
menuOpener.menu = modelData.menu;
contextMenu.resetAutoClose();
} else {
bar.toggleDropdown(contextMenu, function() {
let pos = parent.mapToItem(bar.contentItem, parent.width / 2, 0);
@ -1457,7 +1475,6 @@ in
property real dropdownX: 0
property real fullWidth: 200
property real fullHeight: 200
property int autoCloseMs: 1500
// Flush-right dropdowns merge into the screen frame's
// right column instead of centering on their widget.
property bool alignRight: false
@ -1477,14 +1494,9 @@ in
bar.activeDropdown = null;
chrome.shrinkToButton(dropdown);
}
_autoClose.stop();
_closeDelay.start();
}
function resetAutoClose() {
if (visible && !closing) _autoClose.restart();
}
// Reopen a dropdown that's mid-close: the pending hide
// timer must be cancelled, otherwise it fires later and
// closes the revived dropdown (and the whole chrome).
@ -1492,7 +1504,6 @@ in
_closeDelay.stop();
closing = false;
open = true;
_autoClose.restart();
}
x: alignRight
@ -1507,33 +1518,18 @@ in
if (visible) {
closing = false;
open = true;
_autoClose.restart();
} else {
open = false;
closing = false;
_autoClose.stop();
}
}
Timer {
id: _autoClose
interval: dropdown.autoCloseMs
onTriggered: bar.closeAllDropdowns()
}
Timer {
id: _closeDelay
interval: 300
onTriggered: { dropdown.visible = false; dropdown.closing = false; if (bar.activeDropdown === dropdown) bar.activeDropdown = null; }
}
HoverHandler {
onHoveredChanged: {
if (hovered) _autoClose.stop();
else _autoClose.restart();
}
}
// Content is clipped to the chrome's ANIMATED geometry
// revealed as the panel slides/grows over it and wiped as
// the panel leaves, instead of popping in place. The inner
@ -1731,7 +1727,6 @@ in
alignRight: true
fullWidth: volDropdownCol.width + 28
fullHeight: volDropdownCol.height + 20
autoCloseMs: 3000
Column {
id: volDropdownCol
@ -2165,7 +2160,6 @@ in
// edges render as soft 2px lines
fullWidth: Math.ceil(calRow.width) + 24
fullHeight: Math.ceil(calRow.height) + 24
autoCloseMs: 3000
// Month being viewed; reset to today when the popup opens
// (via the setup function passed to bar.toggleDropdown).