feat(region-selector): add one-click monitor capture buttons

Add monitor buttons (DP-1, DP-2, etc.) to the region selector toolbar
for instant full-screen capture without drag selection.

- Add monitor buttons alongside Rect/Circle selection modes
- Support screenshot, record, and recordWithSound modes (not OCR/Search)
- Add configurable button order via `monitorOrder` in config
- Support cross-monitor capture from any toolbar
- Use regex extraction to read config from JSONC files
This commit is contained in:
DJ committed 2025-12-08 21:27:30 -05:00
1 parent 11e73ec0d8
commit 2dc90c4e18
5 files changed
+146 -1

No files matched your search

+5
View File
@@ -42,6 +42,11 @@
}
},
// ─── Monitor Order ──────────────────────────────────────────
// Monitor button order for full-screen capture (empty = auto-detect)
// Example: ["DP-2", "DP-1"] to show DP-2 button first
"monitorOrder": [],
// ─── Region Selector ────────────────────────────────────────
// Controls the selection overlay behavior
"regionSelector": {
+5
View File
@@ -76,6 +76,11 @@ 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 {
// Auto-detection of clickable regions
@@ -20,8 +20,24 @@ Toolbar {
// Use a synchronizer on these
property var action
property var selectionMode
// Monitor list for full-screen capture buttons
property var monitors: []
// Signals
signal dismiss()
signal captureFullMonitor(string monitorName)
// Show monitor buttons only for screenshot/record modes (not OCR or Search)
readonly property bool showMonitorButtons: {
switch (action) {
case RegionSelection.SnipAction.Copy:
case RegionSelection.SnipAction.Edit:
case RegionSelection.SnipAction.Record:
case RegionSelection.SnipAction.RecordWithSound:
return true;
default:
return false;
}
}
MaterialShape {
Layout.fillHeight: true
@@ -79,4 +95,20 @@ Toolbar {
}
}
// Monitor capture buttons - one-click full-screen capture for each monitor
Repeater {
model: root.showMonitorButtons ? root.monitors : []
delegate: ToolbarTabButton {
required property var modelData
required property int index
current: false
text: modelData.name
materialSymbol: "desktop_windows"
onClicked: root.captureFullMonitor(modelData.name)
StyledToolTip {
text: Translation.tr("Capture full screen: %1").arg(modelData.name)
}
}
}
}
@@ -26,11 +26,43 @@ PanelWindow {
bottom: true
}
enum SnipAction { Copy, Edit, Search, CharRecognition, Record, RecordWithSound }
enum SnipAction { Copy, Edit, Search, CharRecognition, Record, RecordWithSound }
enum SelectionMode { RectCorners, Circle }
property var action: RegionSelection.SnipAction.Copy
property var selectionMode: RegionSelection.SelectionMode.RectCorners
signal dismiss()
// Monitor capture support
property var allMonitors: []
property string targetMonitorCapture: ""
signal captureMonitorRequested(string monitorName)
// Watch for cross-monitor capture requests (only when UI is visible and ready)
onTargetMonitorCaptureChanged: {
if (!root.visible || targetMonitorCapture === "" || targetMonitorCapture !== root.hyprlandMonitor.name) return;
captureFullMonitorLocal();
}
// Capture the full region of this monitor
function captureFullMonitorLocal() {
dragState.regionX = 0;
dragState.regionY = 0;
dragState.regionWidth = root.screen.width;
dragState.regionHeight = root.screen.height;
dragState.endDrag();
root.snip();
}
// Handle monitor button click - capture full screen of the specified monitor
function captureFullMonitor(monitorName: string) {
if (monitorName === root.hyprlandMonitor.name) {
// This is our monitor, capture locally
captureFullMonitorLocal();
} else {
// Request parent to coordinate capture on the target monitor
root.captureMonitorRequested(monitorName);
}
}
property string saveScreenshotDir: Config.options.screenSnip.savePath !== ""
? Config.options.screenSnip.savePath
@@ -370,6 +402,7 @@ PanelWindow {
spacing: 6
OptionsToolbar {
monitors: root.allMonitors
Synchronizer on action {
property alias source: root.action
}
@@ -377,6 +410,7 @@ PanelWindow {
property alias source: root.selectionMode
}
onDismiss: root.dismiss();
onCaptureFullMonitor: (monitorName) => root.captureFullMonitor(monitorName)
}
Item {
anchors {
@@ -19,10 +19,76 @@ Scope {
function dismiss() {
GlobalStates.regionSelectorOpen = false
root.targetMonitorCapture = "" // Reset cross-monitor capture state
}
property var action: RegionSelection.SnipAction.Copy
property var selectionMode: RegionSelection.SelectionMode.RectCorners
// Monitor list - updated imperatively when config loads
property var monitorList: []
// Read monitor order from config and rebuild monitor list
FileView {
id: monitorOrderFileView
path: Directories.shellConfigPath
onLoaded: root.rebuildMonitorList()
}
Component.onCompleted: rebuildMonitorList()
function rebuildMonitorList() {
let customOrder = [];
try {
const rawText = monitorOrderFileView.text();
// Extract monitorOrder array directly with regex
const match = rawText.match(/"monitorOrder"\s*:\s*\[([^\]]*)\]/);
if (match && match[1]) {
// Parse the array contents: "DP-1", "DP-3", "DP-2"
const items = match[1].match(/"([^"]+)"/g);
if (items) {
customOrder = items.map(s => s.replace(/"/g, ''));
}
}
} catch (e) {
customOrder = [];
}
let list = [];
for (let i = 0; i < Quickshell.screens.length; i++) {
const screen = Quickshell.screens[i];
const monitor = Hyprland.monitorFor(screen);
list.push({
name: monitor.name,
x: monitor.x,
y: monitor.y,
width: screen.width,
height: screen.height,
scale: monitor.scale
});
}
// Apply custom ordering from config if specified
if (customOrder && customOrder.length > 0) {
list.sort((a, b) => {
const aIndex = customOrder.indexOf(a.name);
const bIndex = customOrder.indexOf(b.name);
if (aIndex === -1 && bIndex === -1) return 0;
if (aIndex === -1) return 1;
if (bIndex === -1) return -1;
return aIndex - bIndex;
});
}
root.monitorList = list;
}
// Cross-monitor capture coordination: set this to trigger capture on that monitor
property string targetMonitorCapture: ""
function captureMonitor(monitorName: string) {
root.targetMonitorCapture = monitorName;
}
Variants {
model: Quickshell.screens
@@ -33,7 +99,10 @@ Scope {
sourceComponent: RegionSelection {
screen: regionSelectorLoader.modelData
allMonitors: root.monitorList
targetMonitorCapture: root.targetMonitorCapture
onDismiss: root.dismiss()
onCaptureMonitorRequested: (monitorName) => root.captureMonitor(monitorName)
action: root.action
selectionMode: root.selectionMode
}