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.
This commit is contained in:
DJ committed 2026-07-05 11:14:21 -04:00
1 parent 5344908036
commit 29537f64ab
5 files changed
+58 -65

No files matched your search

+5
View File
@@ -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
-1
View File
@@ -24,7 +24,6 @@
"useCircleSelection": false
}
},
"monitorOrder": [],
"regionSelector": {
"dragThreshold": 6,
"targetRegions": {
-5
View File
@@ -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)
@@ -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
@@ -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
};
}