diff --git a/settings/hyprland.nix b/settings/hyprland.nix index 873c32f..abbf620 100644 --- a/settings/hyprland.nix +++ b/settings/hyprland.nix @@ -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 diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 54e924f..4824fdc 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -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).