From 29537f64ab08188bfd8d8e08b402ef98611522bf Mon Sep 17 00:00:00 2001 From: DJ Date: Sun, 5 Jul 2026 11:14:21 -0400 Subject: [PATCH] fix(regionSelector): monitor buttons follow left-to-right layout order Replace the manual monitorOrder config array (a debugging artifact from a temporarily rearranged setup) with automatic sorting by Hyprland layout position (ascending x, ties by y). The list is rebuilt on every overlay open, screen hotplug, and shortcut re-trigger, with refreshMonitors() and an unpopulated-monitor (lastIpcObject) fallback to ShellScreen position. Removes the regex JSONC parse path and the monitorOrder key from the config schema and defaults. --- CHANGELOG.md | 5 ++ defaults/config.json | 1 - quickshell/modules/common/Config.qml | 5 -- .../modules/regionSelector/RegionSelector.qml | 52 +++++++++++----- .../modules/regionSelector/RegionUtils.js | 60 +++++-------------- 5 files changed, 58 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99fa080..e813c90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,11 @@ and this project adheres to [Conventional Commits](https://www.conventionalcommi - **Region Selector Click/Drag Dead-Zone** - Press-to-release movements within `dragThreshold` (default `6` px) are now treated as clicks that select the whole hovered window/region, instead of snipping a tiny drag rectangle. This is a user-visible behavior change: if you relied on tiny deliberate drags to grab very small rectangles, lower or zero the threshold - **Region Selector Empty-Space Clicks** - A click (or sub-threshold micro-drag) over empty space with no targetable region now dismisses the overlay cleanly instead of capturing a sliver +- **Monitor Button Order** - The toolbar's monitor capture buttons now follow left-to-right layout order automatically (ascending x, ties broken top-to-bottom, from Hyprland logical coordinates), re-read every time the selector opens — no configuration needed + +### Removed + +- **`monitorOrder` Config Key** - The manual monitor button ordering array is gone; ordering is now automatic from the layout position. A leftover key in an existing config is ignored and dropped the next time the config is rewritten ### Fixed diff --git a/defaults/config.json b/defaults/config.json index 491c547..f68d46f 100644 --- a/defaults/config.json +++ b/defaults/config.json @@ -24,7 +24,6 @@ "useCircleSelection": false } }, - "monitorOrder": [], "regionSelector": { "dragThreshold": 6, "targetRegions": { diff --git a/quickshell/modules/common/Config.qml b/quickshell/modules/common/Config.qml index 0013830..38414b6 100644 --- a/quickshell/modules/common/Config.qml +++ b/quickshell/modules/common/Config.qml @@ -77,11 +77,6 @@ Singleton { } } - // ─── Monitor Order ─────────────────────────────────────────── - // Monitor button order for full-screen capture (empty = auto-detect) - // Example: ["DP-2", "DP-1"] to show DP-2 first - property var monitorOrder: [] - // ─── Region Selector ────────────────────────────────────────── property JsonObject regionSelector: JsonObject { property int dragThreshold: 6 // Click/drag dead-zone in px (0 = disabled) diff --git a/quickshell/modules/regionSelector/RegionSelector.qml b/quickshell/modules/regionSelector/RegionSelector.qml index 20878a2..07ad9da 100644 --- a/quickshell/modules/regionSelector/RegionSelector.qml +++ b/quickshell/modules/regionSelector/RegionSelector.qml @@ -36,31 +36,52 @@ Scope { // ─── Monitor List Management ───────────────────────────────────────────────── property var monitorList: [] - FileView { - id: monitorOrderFileView - path: Directories.shellConfigPath - onLoaded: root.rebuildMonitorList() - } - Component.onCompleted: rebuildMonitorList() - // Builds the monitor list from Quickshell.screens and applies custom ordering. - // Uses regex to parse monitorOrder from JSONC config (handles comments/trailing commas). + // Re-snapshot the monitor list every time the overlay opens: Hyprland's monitor + // data may not be populated yet at Component.onCompleted (startup race), and + // monitors can be hotplugged or rearranged between opens. The overlay Loaders + // below react to the same signal with no guaranteed handler order — safe only + // because allMonitors is a live binding, not a one-shot read. + Connections { + target: GlobalStates + function onRegionSelectorOpenChanged() { + if (GlobalStates.regionSelectorOpen) root.rebuildMonitorList(); + } + } + + // Hotplug while the overlay is open: the Variants below tracks Quickshell.screens + // live, so the button row must follow too or it desyncs from the overlay windows + Connections { + target: Quickshell + function onScreensChanged() { + if (GlobalStates.regionSelectorOpen) root.rebuildMonitorList(); + } + } + + // Builds the monitor list from Quickshell.screens, sorted by layout position + // (Hyprland logical coordinates): left-to-right, ties top-to-bottom. function rebuildMonitorList() { - // Parse custom order from config - const rawText = monitorOrderFileView.text(); - const customOrder = RegionUtils.parseMonitorOrder(rawText); + // Ask Hyprland to re-fetch monitor state; some rearrangements emit no event. + // Async — this rebuild reads current values, the refresh benefits the next one + Hyprland.refreshMonitors(); // Build monitor info list let list = []; for (let i = 0; i < Quickshell.screens.length; i++) { const screen = Quickshell.screens[i]; const monitor = Hyprland.monitorFor(screen); - list.push(RegionUtils.buildMonitorInfo(screen, monitor)); + // monitorFor creates its object eagerly; x/y stay 0 until IPC data + // arrives, so treat unpopulated the same as missing. Only warn when the + // overlay is open — the startup rebuild is replaced at first open anyway + const populated = monitor && monitor.lastIpcObject; + if (!populated && GlobalStates.regionSelectorOpen) + console.warn("RegionSelector: Hyprland monitor data unavailable for " + screen.name + "; using ShellScreen position"); + list.push(RegionUtils.buildMonitorInfo(screen, populated ? monitor : null)); } - // Apply custom ordering - root.monitorList = RegionUtils.sortMonitorsByOrder(list, customOrder); + // Sort by layout position + root.monitorList = RegionUtils.sortMonitorsByPosition(list); } // ─── Cross-Monitor Capture Coordination ────────────────────────────────────── @@ -130,6 +151,9 @@ Scope { // Opens the region selector with the specified action. // Selection mode is determined by config or defaults to RectCorners. function openWithAction(action) { + // Re-triggering while already open emits no regionSelectorOpenChanged, so + // rebuild explicitly to pick up mid-session monitor rearrangement + root.rebuildMonitorList(); root.action = action; root.selectionMode = getSelectionModeForAction(action); root.captureMode = RegionSelection.CaptureMode.Instant; // Never persists across opens diff --git a/quickshell/modules/regionSelector/RegionUtils.js b/quickshell/modules/regionSelector/RegionUtils.js index 8a78865..c57800d 100644 --- a/quickshell/modules/regionSelector/RegionUtils.js +++ b/quickshell/modules/regionSelector/RegionUtils.js @@ -2,26 +2,6 @@ // This is a Qt/QML JavaScript library (.pragma library makes it stateless) .pragma library -// Parse monitorOrder from raw JSONC config text using regex. -// This approach bypasses JSON parsing issues with JSONC (comments + trailing commas). -// Returns an array of monitor names, or empty array if parsing fails. -function parseMonitorOrder(rawConfigText) { - try { - // Match "monitorOrder": ["name1", "name2", ...] - const match = rawConfigText.match(/"monitorOrder"\s*:\s*\[([^\]]*)\]/); - if (match && match[1]) { - // Extract quoted strings from array contents - const items = match[1].match(/"([^"]+)"/g); - if (items) { - return items.map(s => s.replace(/"/g, '')); - } - } - } catch (e) { - // Silent failure - return empty array - } - return []; -} - // Clamp a region to fit within screen bounds. // Returns a new object with clamped coordinates; does not modify input. function clampRegionToScreen(region, screenWidth, screenHeight) { @@ -41,39 +21,29 @@ function clampRegionToScreen(region, screenWidth, screenHeight) { }; } -// Sort monitors by custom order from config. -// Monitors in customOrder appear first in that order. -// Monitors not in customOrder appear after, in their original order. +// Sort monitors by layout position (Hyprland logical coordinates): +// ascending x, ties broken by ascending y (left-to-right, then top-to-bottom). // Returns a new sorted array; does not modify input. -function sortMonitorsByOrder(monitors, customOrder) { - if (!customOrder || customOrder.length === 0) { - return monitors; - } - - return [...monitors].sort((a, b) => { - const aIdx = customOrder.indexOf(a.name); - const bIdx = customOrder.indexOf(b.name); - - // Both not in custom order - preserve original order - if (aIdx === -1 && bIdx === -1) return 0; - // Only a not in custom order - a goes after b - if (aIdx === -1) return 1; - // Only b not in custom order - b goes after a - if (bIdx === -1) return -1; - // Both in custom order - sort by index - return aIdx - bIdx; - }); +function sortMonitorsByPosition(monitors) { + return [...monitors].sort((a, b) => a.x - b.x || a.y - b.y); } // Build a monitor info object from Quickshell screen and Hyprland monitor data. // Used to create the monitor list for the capture buttons. +// x/y are the layout position (Hyprland logical coordinates). A null +// hyprlandMonitor falls back to ShellScreen's virtual desktop position, which +// tracks the same layout in practice; the caller logs the fallback. function buildMonitorInfo(screen, hyprlandMonitor) { + if (!hyprlandMonitor) { + return { + name: screen.name, + x: screen.x, + y: screen.y + }; + } return { name: hyprlandMonitor.name, x: hyprlandMonitor.x, - y: hyprlandMonitor.y, - width: screen.width, - height: screen.height, - scale: hyprlandMonitor.scale + y: hyprlandMonitor.y }; }