mirror of
https://github.com/thesleepingsage/hypr-lens.git
synced 2026-11-01 06:56:42 +01:00
feat(regionSelector): freehand true-shape capture, R/Space keys, single-owner mode state
- fix: capture-mode state desync (6-press C cycle, inverted tab display). selectionMode/captureMode now have a single owner (RegionSelection); the toolbar receives them as read-only bindings and emits *Selected request signals on tab clicks - no more bidirectional Synchronizer + currentIndex write-back storms. captureMode is int (same-value writes no longer emit change signals). - feat: freehand (ex-Circle) true-shape capture - a polygon mask makes pixels outside the drawn loop transparent (OCR flattens onto white; recording stays rectangular). Mask survives the crop editor: confirm captures loop intersected with the adjusted rect. - feat: circle/freehand selection works in Crop mode (loop bounding box seeds the editor); Rect/Freehand tabs inert only while editing. - feat: R toggles Rect/Freehand; Space confirms the crop editor; 'Circle' tab relabeled Freehand (config keys keep circle names). - docs: MANUAL, wiki, CONFIG_README, CHANGELOG updated.
This commit is contained in:
1 parent
285a7b4e5f
commit
510446e499
8 files changed
+168
-70
No files matched your search
+3
-1
@@ -9,7 +9,9 @@ and this project adheres to [Conventional Commits](https://www.conventionalcommi
|
||||
|
||||
### Added
|
||||
|
||||
- **Crop Mode** - Press `C` (or the new Instant/Crop toolbar tabs) to switch the region selector into Crop mode: releasing a selection opens an adjustable editor (8 resize handles + interior move, min-size and screen clamping, live size label) instead of capturing immediately; **Enter** or the confirm button captures, **Escape** returns to drawing. Works for screenshots and recording alike; configurable via `regionSelector.crop.minSize`/`handleSize`
|
||||
- **Crop Mode** - Press `C` (or the new Instant/Crop toolbar tabs) to switch the region selector into Crop mode: releasing a selection opens an adjustable editor (8 resize handles + interior move, min-size and screen clamping, live size label) instead of capturing immediately; **Enter**/**Space** or the confirm button captures, **Escape** returns to drawing. Works with both selection modes (a circle loop seeds the editor with its bounding box). Works for screenshots and recording alike; configurable via `regionSelector.crop.minSize`/`handleSize`
|
||||
- **`R` Hotkey** - Toggles Rect/Freehand selection mode inside the overlay (the "Circle" tab is now labeled **Freehand**; config keys keep their `circle` names)
|
||||
- **Freehand True-Shape Capture** - Freehand selections now capture only the pixels inside the drawn loop (transparent outside, via an ImageMagick polygon mask). OCR flattens onto white; recording remains rectangular. In Crop mode the mask survives the editor - confirm captures the loop intersected with the adjusted rect
|
||||
- **`regionSelector.dragThreshold` Config Key** - Pixel dead-zone (default `6`) for the region selector's click/drag discriminator; `0` disables it, restoring the previous per-pixel sensitivity (circle mode always uses `0` — every circle gesture is the drawn shape)
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -25,12 +25,14 @@ Click and drag to select a rectangular region. Features:
|
||||
- **Dimension display** - Shows width x height as you drag
|
||||
- **Window snapping** - Detected windows highlight for quick selection
|
||||
|
||||
### Circle Selection
|
||||
### Freehand Selection
|
||||
|
||||
Freehand captures the true drawn shape: the output is cropped to the loop's bounding box and everything outside the loop is transparent. OCR flattens the mask onto white (tesseract dislikes alpha); recording always captures the bounding rectangle (wf-recorder is rectangle-only). In Crop mode the mask survives the editor: confirm captures the drawn loop intersected with the adjusted rect.
|
||||
|
||||
Used for image search by default. Draw a freeform circle around content:
|
||||
|
||||
- Hold and drag to draw
|
||||
- Release to capture the bounded area
|
||||
- Release to capture the drawn shape (transparent outside the loop)
|
||||
- Useful for irregular shapes or specific content
|
||||
|
||||
Toggle between modes in config:
|
||||
@@ -50,10 +52,10 @@ By default captures happen instantly when you release the mouse. Press `C` (or u
|
||||
|
||||
- Releasing a selection (or clicking a detected window / monitor button) opens an adjustable editor instead of capturing
|
||||
- Drag the 8 handles to resize, or drag inside the rect to move it
|
||||
- `Enter` or the confirm button captures the adjusted region; `Escape` goes back to drawing
|
||||
- `Enter` or `Space` (or the confirm button) captures the adjusted region; `Escape` goes back to drawing
|
||||
- Works for all actions, including recording; right-click selections still open the editor and route to Edit on confirm
|
||||
|
||||
Crop mode is per monitor, always starts off (the overlay opens in Instant mode), and temporarily forces rectangle selection — your previous selection mode is restored when you leave it. Minimum size and handle size are configurable via `regionSelector.crop` in the config.
|
||||
Crop mode is per monitor and always starts off (the overlay opens in Instant mode). Freehand selection works in Crop too — the drawn loop's bounding box seeds the editor. Toggle Rect/Freehand with `R`. Minimum size and handle size are configurable via `regionSelector.crop` in the config.
|
||||
|
||||
## Screenshot Actions
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ Rectangle selection tool settings.
|
||||
|
||||
### regionSelector.circle
|
||||
|
||||
Circle selection tool settings.
|
||||
Freehand ("circle") selection tool settings — the config key keeps its historical name.
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
@@ -164,7 +164,7 @@ With satty, **Enter** (or the copy/save toolbar buttons) keeps the edit — copy
|
||||
|
||||
### regionSelector.crop
|
||||
|
||||
Crop mode: an adjustable crop editor as an alternative to the default instant capture. Toggle it with the **C** key (hardcoded, per monitor) or the Instant/Crop toolbar tabs. In Crop mode, releasing a selection opens an editor with 8 resize handles and interior move-drag instead of capturing immediately; **Enter** (or the confirm button) captures the adjusted region, **Escape** goes back to drawing. The overlay always opens in Instant mode.
|
||||
Crop mode: an adjustable crop editor as an alternative to the default instant capture. Toggle it with the **C** key (hardcoded, per monitor) or the Instant/Crop toolbar tabs. In Crop mode, releasing a selection opens an editor with 8 resize handles and interior move-drag instead of capturing immediately; **Enter** or **Space** (or the confirm button) captures the adjusted region, **Escape** goes back to drawing. The overlay always opens in Instant mode.
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
|
||||
@@ -18,10 +18,18 @@ import Quickshell.Hyprland
|
||||
Toolbar {
|
||||
id: root
|
||||
|
||||
// Use a synchronizer on these
|
||||
// action uses a Synchronizer (display-only here, never written back).
|
||||
// selectionMode/captureMode are plain read-only bindings from RegionSelection — the
|
||||
// single owner. Tab clicks *request* a change via the *Selected signals instead of
|
||||
// writing state, and the tab indices are derived display. (The previous bidirectional
|
||||
// Synchronizer + currentIndex write-back + the C-key external writer left three copies
|
||||
// of the mode that desynced under re-entrant sync.)
|
||||
property var action
|
||||
property var selectionMode
|
||||
property var captureMode
|
||||
property int captureMode
|
||||
|
||||
signal selectionModeSelected(var mode)
|
||||
signal captureModeSelected(int mode)
|
||||
// True while the adjustable crop editor is open (plain binding from RegionSelection)
|
||||
property bool cropEditing: false
|
||||
// Monitor list for full-screen capture buttons
|
||||
@@ -36,10 +44,12 @@ Toolbar {
|
||||
readonly property bool showMonitorButtons: actionConfig.allowsMonitorButtons
|
||||
readonly property bool cropActive: root.captureMode === RegionSelection.CaptureMode.Crop
|
||||
|
||||
// Keep tab indices in sync with externally driven state (C-key toggle swaps
|
||||
// captureMode; entering Crop forces selectionMode back to RectCorners)
|
||||
// Follow the owner: whenever the bound-in mode changes (C key, forced RectCorners,
|
||||
// sessionReset, or an accepted tab request), reflect it in the tab index. Reads the
|
||||
// just-changed property directly — never a derived property that may not have
|
||||
// re-evaluated yet.
|
||||
onCaptureModeChanged: {
|
||||
const idx = root.cropActive ? 1 : 0;
|
||||
const idx = root.captureMode === RegionSelection.CaptureMode.Crop ? 1 : 0;
|
||||
if (captureModeTabBar.currentIndex !== idx) captureModeTabBar.setCurrentIndex(idx);
|
||||
}
|
||||
onSelectionModeChanged: {
|
||||
@@ -65,20 +75,25 @@ Toolbar {
|
||||
}
|
||||
}
|
||||
|
||||
// Selection mode tabs (Rect/Circle) - inert while Crop is active (Crop implies rectangles)
|
||||
// Selection mode tabs (Rect/Circle) - usable in Crop too (a circle loop seeds the
|
||||
// editor via its bounding box); inert only while the crop editor is open, same as
|
||||
// the capture tabs
|
||||
ToolbarTabBar {
|
||||
id: tabBar
|
||||
enabled: !root.cropActive
|
||||
enabled: !root.cropEditing
|
||||
opacity: enabled ? 1 : 0.4
|
||||
tabButtonList: [
|
||||
{"icon": "activity_zone", "name": Translation.tr("Rect")},
|
||||
{"icon": "gesture", "name": Translation.tr("Circle")}
|
||||
{"icon": "gesture", "name": Translation.tr("Freehand")}
|
||||
]
|
||||
Component.onCompleted: {
|
||||
currentIndex = root.selectionMode === RegionSelection.SelectionMode.RectCorners ? 0 : 1
|
||||
}
|
||||
onCurrentIndexChanged: {
|
||||
root.selectionMode = currentIndex === 0 ? RegionSelection.SelectionMode.RectCorners : RegionSelection.SelectionMode.Circle;
|
||||
// Emit only for genuine user input; index changes that merely follow the
|
||||
// bound-in state compute the same mode and stay silent (no echo loop).
|
||||
const mode = currentIndex === 0 ? RegionSelection.SelectionMode.RectCorners : RegionSelection.SelectionMode.Circle;
|
||||
if (mode !== root.selectionMode) root.selectionModeSelected(mode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +112,8 @@ Toolbar {
|
||||
currentIndex = root.cropActive ? 1 : 0
|
||||
}
|
||||
onCurrentIndexChanged: {
|
||||
root.captureMode = currentIndex === 0 ? RegionSelection.CaptureMode.Instant : RegionSelection.CaptureMode.Crop;
|
||||
const mode = currentIndex === 0 ? RegionSelection.CaptureMode.Instant : RegionSelection.CaptureMode.Crop;
|
||||
if (mode !== root.captureMode) root.captureModeSelected(mode);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,18 +103,31 @@ QtObject {
|
||||
clearTargetedRegion();
|
||||
}
|
||||
|
||||
// Compute bounding box from circle selection points
|
||||
// Compute the padded bounding box of the circle selection points without touching
|
||||
// any state — safe for crop-mode seeding, where region* must stay binding-driven
|
||||
// padding: extra space around the bounding box
|
||||
// fallbackX, fallbackY: coordinates to use if no points recorded
|
||||
function setRegionFromCirclePoints(padding: real, fallbackX: real, fallbackY: real) {
|
||||
function circleBoundingBox(padding: real, fallbackX: real, fallbackY: real): var {
|
||||
const dragPoints = (points.length > 0) ? points : [{ x: fallbackX, y: fallbackY }];
|
||||
const maxX = Math.max(...dragPoints.map(p => p.x));
|
||||
const minX = Math.min(...dragPoints.map(p => p.x));
|
||||
const maxY = Math.max(...dragPoints.map(p => p.y));
|
||||
const minY = Math.min(...dragPoints.map(p => p.y));
|
||||
regionX = minX - padding;
|
||||
regionY = minY - padding;
|
||||
regionWidth = maxX - minX + padding * 2;
|
||||
regionHeight = maxY - minY + padding * 2;
|
||||
return {
|
||||
x: minX - padding,
|
||||
y: minY - padding,
|
||||
width: maxX - minX + padding * 2,
|
||||
height: maxY - minY + padding * 2
|
||||
};
|
||||
}
|
||||
|
||||
// Set the selection region to the circle points' bounding box (breaks the region*
|
||||
// bindings — only safe on paths that end in dispatch/dismissal)
|
||||
function setRegionFromCirclePoints(padding: real, fallbackX: real, fallbackY: real) {
|
||||
const box = circleBoundingBox(padding, fallbackX, fallbackY);
|
||||
regionX = box.x;
|
||||
regionY = box.y;
|
||||
regionWidth = box.width;
|
||||
regionHeight = box.height;
|
||||
}
|
||||
}
|
||||
@@ -32,35 +32,35 @@ PanelWindow {
|
||||
enum CaptureMode { Instant, Crop }
|
||||
property var action: RegionSelection.SnipAction.Copy
|
||||
property var selectionMode: RegionSelection.SelectionMode.RectCorners
|
||||
property var captureMode: RegionSelection.CaptureMode.Instant
|
||||
// int, not var: int properties skip the changed signal on same-value writes, so
|
||||
// redundant assignments (sessionReset, echo paths) can't retrigger onCaptureModeChanged
|
||||
property int captureMode: RegionSelection.CaptureMode.Instant
|
||||
// Crop mode: true while the adjustable crop editor is open (post-release, pre-confirm)
|
||||
property bool cropEditing: false
|
||||
// Selection mode in effect before Crop forced RectCorners; restored on exit
|
||||
property var preCropSelectionMode: RegionSelection.SelectionMode.RectCorners
|
||||
// Freehand true-shape capture: the drawn path, staged by the freehand release branch
|
||||
// for the immediately following snip() (which masks pixels outside it). Empty for
|
||||
// every other path — rect drags, target clicks, monitor buttons, crop confirms.
|
||||
property var pendingSnipPoints: []
|
||||
signal dismiss()
|
||||
|
||||
property bool isCropMode: (root.captureMode === RegionSelection.CaptureMode.Crop)
|
||||
|
||||
// Swapping capture modes resets the editor and any in-progress drag — never the frame.
|
||||
// Crop implies rectangles: force RectCorners while active (toolbar tab bar is disabled
|
||||
// too), remembering the previous mode so leaving Crop restores it (e.g. OCR's circle
|
||||
// config). Lives here so both the C key and the toolbar tabs get the same behavior.
|
||||
// Swapping capture modes resets the editor and any in-progress drag — never the
|
||||
// frame or the selection mode. Circle works in Crop too: the drawn loop's bounding
|
||||
// box seeds the editor. Lives here so both the C key and the toolbar tabs get the
|
||||
// same behavior.
|
||||
onCaptureModeChanged: {
|
||||
root.cropEditing = false;
|
||||
root.pendingSnipPoints = [];
|
||||
dragState.reset();
|
||||
if (root.isCropMode) {
|
||||
root.preCropSelectionMode = root.selectionMode;
|
||||
root.selectionMode = RegionSelection.SelectionMode.RectCorners;
|
||||
} else {
|
||||
root.selectionMode = root.preCropSelectionMode;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore session defaults when a capture shortcut re-triggers while the overlay is
|
||||
// already open. Local writes (C key, toolbar Synchronizer) break the captureMode
|
||||
// binding from RegionSelector, so the reset must be pushed as a call, not a binding.
|
||||
// already open. Local writes (the C key) break the captureMode binding from
|
||||
// RegionSelector, so the reset must be pushed as a call, not a binding.
|
||||
function sessionReset() {
|
||||
root.cropEditing = false;
|
||||
root.pendingSnipPoints = [];
|
||||
dragState.reset();
|
||||
root.captureMode = RegionSelection.CaptureMode.Instant;
|
||||
}
|
||||
@@ -107,6 +107,7 @@ PanelWindow {
|
||||
|
||||
// Capture the full region of this monitor
|
||||
function captureFullMonitorLocal() {
|
||||
root.pendingSnipPoints = []; // full-monitor capture is never freehand-masked
|
||||
if (root.isCropMode) {
|
||||
// Crop: seed the editor with the full-monitor rect instead of snipping
|
||||
root.openCropEditor(0, 0, root.screen.width, root.screen.height);
|
||||
@@ -255,17 +256,18 @@ PanelWindow {
|
||||
// Table of command builders indexed by SnipAction enum value.
|
||||
// Each builder takes (rx, ry, rw, rh, absX, absY) and returns a command array.
|
||||
readonly property var commandBuilders: ({
|
||||
[RegionSelection.SnipAction.Copy]: (rx, ry, rw, rh, absX, absY) =>
|
||||
SnipCommands.buildCopyCommand(root.screenshotPath, rx, ry, rw, rh, root.saveScreenshotDir, Config.options.screenSnip.copyAlsoSaves),
|
||||
[RegionSelection.SnipAction.Edit]: (rx, ry, rw, rh, absX, absY) =>
|
||||
SnipCommands.buildEditCommand(root.screenshotPath, rx, ry, rw, rh, root.saveScreenshotDir, Config.options.screenSnip.copyAlsoSaves),
|
||||
[RegionSelection.SnipAction.Search]: (rx, ry, rw, rh, absX, absY) =>
|
||||
SnipCommands.buildSearchCommand(root.screenshotPath, rx, ry, rw, rh, "https://lens.google.com"),
|
||||
[RegionSelection.SnipAction.CharRecognition]: (rx, ry, rw, rh, absX, absY) =>
|
||||
SnipCommands.buildOcrCommand(root.screenshotPath, rx, ry, rw, rh),
|
||||
[RegionSelection.SnipAction.Record]: (rx, ry, rw, rh, absX, absY) =>
|
||||
[RegionSelection.SnipAction.Copy]: (rx, ry, rw, rh, absX, absY, polygon) =>
|
||||
SnipCommands.buildCopyCommand(root.screenshotPath, rx, ry, rw, rh, root.saveScreenshotDir, Config.options.screenSnip.copyAlsoSaves, polygon),
|
||||
[RegionSelection.SnipAction.Edit]: (rx, ry, rw, rh, absX, absY, polygon) =>
|
||||
SnipCommands.buildEditCommand(root.screenshotPath, rx, ry, rw, rh, root.saveScreenshotDir, Config.options.screenSnip.copyAlsoSaves, polygon),
|
||||
[RegionSelection.SnipAction.Search]: (rx, ry, rw, rh, absX, absY, polygon) =>
|
||||
SnipCommands.buildSearchCommand(root.screenshotPath, rx, ry, rw, rh, "https://lens.google.com", polygon),
|
||||
[RegionSelection.SnipAction.CharRecognition]: (rx, ry, rw, rh, absX, absY, polygon) =>
|
||||
SnipCommands.buildOcrCommand(root.screenshotPath, rx, ry, rw, rh, polygon),
|
||||
// Recording is rectangle-only (wf-recorder captures rects); polygon unused
|
||||
[RegionSelection.SnipAction.Record]: (rx, ry, rw, rh, absX, absY, polygon) =>
|
||||
SnipCommands.buildRecordCommand(Directories.recordScriptPath, absX, absY, rw, rh, false),
|
||||
[RegionSelection.SnipAction.RecordWithSound]: (rx, ry, rw, rh, absX, absY) =>
|
||||
[RegionSelection.SnipAction.RecordWithSound]: (rx, ry, rw, rh, absX, absY, polygon) =>
|
||||
SnipCommands.buildRecordCommand(Directories.recordScriptPath, absX, absY, rw, rh, true)
|
||||
})
|
||||
|
||||
@@ -304,6 +306,23 @@ PanelWindow {
|
||||
const absX = rx + Math.round(root.monitorOffsetX * root.monitorScale);
|
||||
const absY = ry + Math.round(root.monitorOffsetY * root.monitorScale);
|
||||
|
||||
// Freehand true-shape mask: transform the staged path to crop-local physical
|
||||
// pixels, downsampled to <= 150 vertices (a slow scribble records hundreds of
|
||||
// samples; ImageMagick doesn't need them all). Empty string = plain bbox crop.
|
||||
let polygon = "";
|
||||
if (root.pendingSnipPoints.length >= 3) {
|
||||
const pts = root.pendingSnipPoints;
|
||||
const stride = Math.max(1, Math.ceil(pts.length / 150));
|
||||
const parts = [];
|
||||
for (let i = 0; i < pts.length; i += stride) {
|
||||
const px = Math.round((pts[i].x - clamped.x) * root.monitorScale);
|
||||
const py = Math.round((pts[i].y - clamped.y) * root.monitorScale);
|
||||
parts.push(`${px},${py}`);
|
||||
}
|
||||
polygon = parts.join(" ");
|
||||
}
|
||||
root.pendingSnipPoints = [];
|
||||
|
||||
// Build command using table-driven approach
|
||||
const builder = commandBuilders[root.action];
|
||||
if (!builder) {
|
||||
@@ -311,7 +330,7 @@ PanelWindow {
|
||||
root.dismiss();
|
||||
return;
|
||||
}
|
||||
snipProc.command = builder(rx, ry, rw, rh, absX, absY);
|
||||
snipProc.command = builder(rx, ry, rw, rh, absX, absY, polygon);
|
||||
|
||||
snipProc.startDetached();
|
||||
root.dismiss();
|
||||
@@ -355,7 +374,13 @@ PanelWindow {
|
||||
root.captureMode = root.isCropMode
|
||||
? RegionSelection.CaptureMode.Instant
|
||||
: RegionSelection.CaptureMode.Crop;
|
||||
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && root.cropEditing && !event.isAutoRepeat) {
|
||||
} else if (event.key === Qt.Key_R && event.modifiers === Qt.NoModifier && !dragState.dragging && !root.cropEditing) {
|
||||
// Rect/Freehand toggle — same guards as C (no mid-drag swaps), plus
|
||||
// inert while the editor is open, matching the toolbar tab gating
|
||||
root.selectionMode = root.selectionMode === RegionSelection.SelectionMode.RectCorners
|
||||
? RegionSelection.SelectionMode.Circle
|
||||
: RegionSelection.SelectionMode.RectCorners;
|
||||
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Space) && root.cropEditing && !event.isAutoRepeat) {
|
||||
root.confirmCrop();
|
||||
}
|
||||
}
|
||||
@@ -372,6 +397,9 @@ PanelWindow {
|
||||
|
||||
// Controls
|
||||
onPressed: (mouse) => {
|
||||
// Every new gesture invalidates any freehand mask staged by a previous
|
||||
// (e.g. Esc-cancelled) selection
|
||||
root.pendingSnipPoints = [];
|
||||
dragState.startDrag(mouse.x, mouse.y, mouse.button);
|
||||
root.updateTargetedRegion(mouse.x, mouse.y);
|
||||
}
|
||||
@@ -402,6 +430,22 @@ PanelWindow {
|
||||
// Circle dragging?
|
||||
else if (root.selectionMode === RegionSelection.SelectionMode.Circle) {
|
||||
const padding = Config.options.regionSelector.circle.padding + Config.options.regionSelector.circle.strokeWidth / 2;
|
||||
// True-shape capture: stage the path for snip()'s polygon mask.
|
||||
// Degenerate paths (< 3 vertices) fall back to the bounding box.
|
||||
// Staged in Crop too — the mask is screen-anchored (snip() transforms
|
||||
// it against the final rect), so confirm captures loop ∩ adjusted rect.
|
||||
if (dragState.points.length >= 3) {
|
||||
root.pendingSnipPoints = dragState.points.slice();
|
||||
}
|
||||
if (root.isCropMode) {
|
||||
// Crop: seed the editor from the loop's bounding box WITHOUT
|
||||
// setRegionFromCirclePoints — that writes region* directly and
|
||||
// would break the bindings Esc-back-to-draw depends on
|
||||
const box = dragState.circleBoundingBox(padding, mouseArea.mouseX, mouseArea.mouseY);
|
||||
dragState.endDrag();
|
||||
root.openCropEditor(box.x, box.y, box.width, box.height);
|
||||
return;
|
||||
}
|
||||
dragState.setRegionFromCirclePoints(padding, mouseArea.mouseX, mouseArea.mouseY);
|
||||
}
|
||||
dragState.endDrag();
|
||||
@@ -422,7 +466,9 @@ PanelWindow {
|
||||
Loader {
|
||||
z: 2
|
||||
anchors.fill: parent
|
||||
active: root.selectionMode === RegionSelection.SelectionMode.RectCorners
|
||||
// Also active while crop-editing regardless of selection mode: the editor
|
||||
// is always rectangular (a circle seed becomes its bounding box)
|
||||
active: root.selectionMode === RegionSelection.SelectionMode.RectCorners || root.cropEditing
|
||||
sourceComponent: RectCornersSelectionDetails {
|
||||
regionX: dragState.regionX
|
||||
regionY: dragState.regionY
|
||||
@@ -455,7 +501,9 @@ PanelWindow {
|
||||
Loader {
|
||||
z: 2
|
||||
anchors.fill: parent
|
||||
active: root.selectionMode === RegionSelection.SelectionMode.Circle
|
||||
// Hidden while crop-editing: the drawn loop has been consumed into the
|
||||
// editor's rect (and dragState.points survive until the next reset)
|
||||
active: root.selectionMode === RegionSelection.SelectionMode.Circle && !root.cropEditing
|
||||
sourceComponent: CircleSelectionDetails {
|
||||
color: root.selectionBorderColor
|
||||
overlayColor: root.overlayColor
|
||||
@@ -544,15 +592,21 @@ PanelWindow {
|
||||
OptionsToolbar {
|
||||
monitors: root.allMonitors
|
||||
cropEditing: root.cropEditing
|
||||
// Single source of truth is root.captureMode/selectionMode: state flows
|
||||
// down through these bindings, tab clicks flow up as *Selected requests.
|
||||
// (The old bidirectional Synchronizers + index write-backs desynced once
|
||||
// the C key became an external writer.)
|
||||
selectionMode: root.selectionMode
|
||||
captureMode: root.captureMode
|
||||
onSelectionModeSelected: (mode) => {
|
||||
if (root.selectionMode !== mode) root.selectionMode = mode;
|
||||
}
|
||||
onCaptureModeSelected: (mode) => {
|
||||
if (root.captureMode !== mode) root.captureMode = mode;
|
||||
}
|
||||
Synchronizer on action {
|
||||
property alias source: root.action
|
||||
}
|
||||
Synchronizer on selectionMode {
|
||||
property alias source: root.selectionMode
|
||||
}
|
||||
Synchronizer on captureMode {
|
||||
property alias source: root.captureMode
|
||||
}
|
||||
onDismiss: root.dismiss();
|
||||
onCaptureFullMonitor: (monitorName) => root.captureFullMonitor(monitorName)
|
||||
onEditFullMonitor: (monitorName) => root.editFullMonitor(monitorName)
|
||||
|
||||
@@ -27,7 +27,7 @@ Scope {
|
||||
|
||||
property var action: RegionSelection.SnipAction.Copy
|
||||
property var selectionMode: RegionSelection.SelectionMode.RectCorners
|
||||
property var captureMode: RegionSelection.CaptureMode.Instant
|
||||
property int captureMode: RegionSelection.CaptureMode.Instant
|
||||
|
||||
// Emitted by openWithAction so live RegionSelection instances (whose property
|
||||
// bindings may have been broken by local writes) reset to session defaults
|
||||
|
||||
@@ -20,9 +20,20 @@ Singleton {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Build ImageMagick crop command base
|
||||
function buildCropBase(screenshotPath: string, rx: int, ry: int, rw: int, rh: int): string {
|
||||
return `magick ${StringUtils.shellSingleQuoteEscape(screenshotPath)} -crop ${rw}x${rh}+${rx}+${ry}`;
|
||||
// Build ImageMagick crop command base.
|
||||
// polygon: optional "x,y x,y ..." vertex list in crop-local physical pixels (freehand
|
||||
// true-shape capture) — pixels outside it become transparent via a CopyOpacity mask.
|
||||
// flattenWhite: replace that transparency with white (tesseract dislikes alpha).
|
||||
function buildCropBase(screenshotPath: string, rx: int, ry: int, rw: int, rh: int, polygon: string, flattenWhite: bool): string {
|
||||
let cmd = `magick ${StringUtils.shellSingleQuoteEscape(screenshotPath)} -crop ${rw}x${rh}+${rx}+${ry}`;
|
||||
if (polygon !== "") {
|
||||
// +repage zeroes the virtual canvas offset so the mask composites aligned
|
||||
cmd += ` +repage \\( -size ${rw}x${rh} xc:black -fill white -draw 'polygon ${polygon}' \\) -alpha off -compose CopyOpacity -composite`;
|
||||
if (flattenWhite) {
|
||||
cmd += ` -background white -alpha remove -alpha off`;
|
||||
}
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
|
||||
// Build cleanup command
|
||||
@@ -53,8 +64,8 @@ Singleton {
|
||||
}
|
||||
|
||||
// Copy to clipboard (optionally also saves to disk if copyAlsoSaves is true)
|
||||
function buildCopyCommand(screenshotPath: string, rx: int, ry: int, rw: int, rh: int, saveDir: string, alsoSave: bool): list<string> {
|
||||
const cropBase = buildCropBase(screenshotPath, rx, ry, rw, rh);
|
||||
function buildCopyCommand(screenshotPath: string, rx: int, ry: int, rw: int, rh: int, saveDir: string, alsoSave: bool, polygon: string): list<string> {
|
||||
const cropBase = buildCropBase(screenshotPath, rx, ry, rw, rh, polygon, false);
|
||||
const cropToStdout = `${cropBase} -`;
|
||||
const cleanup = buildCleanup(screenshotPath);
|
||||
|
||||
@@ -84,8 +95,8 @@ Singleton {
|
||||
// with NO XDG_CONFIG_HOME override, so GTK theming stays native. satty shows its own
|
||||
// notifications. Falls back to a bare swappy invocation (end-4 style) when satty is disabled
|
||||
// or not installed.
|
||||
function buildEditCommand(screenshotPath: string, rx: int, ry: int, rw: int, rh: int, saveDir: string, alsoSave: bool): list<string> {
|
||||
const cropBase = buildCropBase(screenshotPath, rx, ry, rw, rh);
|
||||
function buildEditCommand(screenshotPath: string, rx: int, ry: int, rw: int, rh: int, saveDir: string, alsoSave: bool, polygon: string): list<string> {
|
||||
const cropBase = buildCropBase(screenshotPath, rx, ry, rw, rh, polygon, false);
|
||||
const cropToStdout = `${cropBase} -`;
|
||||
const cleanup = buildCleanup(screenshotPath);
|
||||
|
||||
@@ -118,8 +129,8 @@ Singleton {
|
||||
|
||||
// Image search (clipboard-based: copy to clipboard + open search page)
|
||||
function buildSearchCommand(screenshotPath: string, rx: int, ry: int, rw: int, rh: int,
|
||||
searchPageUrl: string): list<string> {
|
||||
const cropBase = buildCropBase(screenshotPath, rx, ry, rw, rh);
|
||||
searchPageUrl: string, polygon: string): list<string> {
|
||||
const cropBase = buildCropBase(screenshotPath, rx, ry, rw, rh, polygon, false);
|
||||
const cropToStdout = `${cropBase} -`;
|
||||
const cleanup = buildCleanup(screenshotPath);
|
||||
// Copy to clipboard AND open search page - user pastes with Ctrl+V
|
||||
@@ -127,8 +138,8 @@ Singleton {
|
||||
}
|
||||
|
||||
// OCR with tesseract
|
||||
function buildOcrCommand(screenshotPath: string, rx: int, ry: int, rw: int, rh: int): list<string> {
|
||||
const cropBase = buildCropBase(screenshotPath, rx, ry, rw, rh);
|
||||
function buildOcrCommand(screenshotPath: string, rx: int, ry: int, rw: int, rh: int, polygon: string): list<string> {
|
||||
const cropBase = buildCropBase(screenshotPath, rx, ry, rw, rh, polygon, true);
|
||||
const cropInPlace = `${cropBase} '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'`;
|
||||
const cleanup = buildCleanup(screenshotPath);
|
||||
const tesseractLangs = `$(tesseract --list-langs | awk 'NR>1{print $1}' | tr '\\n' '+' | sed 's/\\+$/\\n/')`;
|
||||
|
||||
Reference in new issue
Block a user