mirror of
https://github.com/thesleepingsage/hypr-lens.git
synced 2026-11-01 06:56:42 +01:00
feat(regionSelector): add Crop Mode with adjustable-handle editor
CaptureMode { Instant, Crop } toggled by C or Instant/Crop toolbar tabs.
In Crop, release seeds an 8-handle + interior-move editor over the
retained frame (min-size and screen clamps); Enter or the confirm FAB
dispatches through the existing snip(), Esc cancels back to draw.
Action-agnostic: screenshots and recording both confirm through it.
New config: regionSelector.crop.minSize / handleSize.
This commit is contained in:
1 parent
fc69fe6642
commit
285a7b4e5f
11 files changed
+418
-4
No files matched your search
@@ -9,6 +9,7 @@ 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`
|
||||
- **`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
|
||||
|
||||
@@ -44,6 +44,17 @@ Toggle between modes in config:
|
||||
}
|
||||
```
|
||||
|
||||
### Crop Mode
|
||||
|
||||
By default captures happen instantly when you release the mouse. Press `C` (or use the Instant/Crop toolbar tabs) to switch the current monitor's overlay into Crop mode:
|
||||
|
||||
- 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
|
||||
- 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.
|
||||
|
||||
## Screenshot Actions
|
||||
|
||||
### Copy (Super+Shift+S)
|
||||
@@ -224,6 +235,10 @@ Full config schema (`~/.config/hypr-lens/config.json`):
|
||||
"circle": {
|
||||
"strokeWidth": 6, // Circle selection line thickness
|
||||
"padding": 10 // Extra padding around circle bounds
|
||||
},
|
||||
"crop": {
|
||||
"minSize": 20, // Minimum crop width/height in px
|
||||
"handleSize": 12 // Visual size of resize handles in px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +162,17 @@ Annotation tool used by the **Edit** action.
|
||||
|
||||
With satty, **Enter** (or the copy/save toolbar buttons) keeps the edit — copying to the clipboard and, when `screenSnip.copyAlsoSaves` is `true`, saving into the hypr-lens save dir — while **Escape** discards it. satty is configured via the bundled `~/.config/hypr-lens/satty.toml` (edit it to taste; it never touches your own `~/.config/satty`). The swappy fallback is a plain hand-off — swappy uses its own Save/Copy buttons and its own save directory.
|
||||
|
||||
### 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.
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `minSize` | `20` | Minimum crop width/height in pixels, enforced per axis while resizing |
|
||||
| `handleSize` | `12` | Visual size of the resize handles in pixels |
|
||||
|
||||
Values are sanitized on use: `minSize` is clamped to at least `1` and `handleSize` to at least `4`, so zero/negative values can't collapse the crop rect or make the handles ungrabbable.
|
||||
|
||||
---
|
||||
|
||||
## Tips
|
||||
|
||||
@@ -45,6 +45,10 @@
|
||||
},
|
||||
"annotation": {
|
||||
"useSatty": true
|
||||
},
|
||||
"crop": {
|
||||
"minSize": 20,
|
||||
"handleSize": 12
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,11 @@ Singleton {
|
||||
property JsonObject annotation: JsonObject {
|
||||
property bool useSatty: true // true = satty (preferred), false = swappy. Falls back to swappy if satty isn't installed.
|
||||
}
|
||||
// Crop mode adjustable editor (toggled with the C key)
|
||||
property JsonObject crop: JsonObject {
|
||||
property int minSize: 20 // Minimum crop width/height in px
|
||||
property int handleSize: 12 // Visual size of resize handles in px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import "../common"
|
||||
import "RegionUtils.js" as RegionUtils
|
||||
import QtQuick
|
||||
|
||||
// Crop editor interaction layer: 8 resize handles (4 corners + 4 edges) plus an
|
||||
// interior move-grab over the current selection region.
|
||||
//
|
||||
// Load-bearing invariant: dragState.region* are bindings over the drag endpoints
|
||||
// (dragStart*/dragging*). This component NEVER assigns region* directly — every
|
||||
// mutation goes through dragState.seedRect (endpoint writes), which also keeps the
|
||||
// rect normalized so "drag left edge past right" cannot flip min/abs on us.
|
||||
// startDrag is never called either, so dragState.mouseButton (the Copy/Edit
|
||||
// discriminator from the seeding gesture) survives the whole edit session.
|
||||
Item {
|
||||
id: root
|
||||
required property var dragState
|
||||
required property real screenWidth
|
||||
required property real screenHeight
|
||||
required property color handleColor
|
||||
required property color handleBorderColor
|
||||
|
||||
// Config values clamped at the consumption site (same idea as the
|
||||
// Math.max(0, ...dragThreshold) guard in RegionSelection): minSize <= 0 would
|
||||
// let the rect collapse to nothing, handleSize <= 0 would be ungrabbable
|
||||
readonly property int minSize: Math.max(1, Config.options.regionSelector.crop.minSize)
|
||||
readonly property int handleSize: Math.max(4, Config.options.regionSelector.crop.handleSize)
|
||||
// Hit target is larger than the visual so handles stay grabbable when small,
|
||||
// but shrinks with the rect (floored at the visual size) so the 8 hit areas
|
||||
// can't fully overlap — and shadow each other — at min rect size
|
||||
readonly property real hitSize: Math.max(handleSize,
|
||||
Math.min(handleSize * 2, Math.min(dragState.regionWidth, dragState.regionHeight) / 2))
|
||||
|
||||
// Rect + pointer position captured at press; deltas are applied against these
|
||||
// so per-event rounding/clamping never accumulates
|
||||
property real pressRectX: 0
|
||||
property real pressRectY: 0
|
||||
property real pressRectW: 0
|
||||
property real pressRectH: 0
|
||||
property real pressMouseX: 0
|
||||
property real pressMouseY: 0
|
||||
|
||||
function beginGrab(item, mouse) {
|
||||
pressRectX = root.dragState.regionX;
|
||||
pressRectY = root.dragState.regionY;
|
||||
pressRectW = root.dragState.regionWidth;
|
||||
pressRectH = root.dragState.regionHeight;
|
||||
const p = item.mapToItem(root, mouse.x, mouse.y);
|
||||
pressMouseX = p.x;
|
||||
pressMouseY = p.y;
|
||||
}
|
||||
|
||||
// Resize: move only the grabbed edges, clamping each axis to >= minSize and
|
||||
// to the screen bounds. Edges that aren't grabbed stay anchored.
|
||||
function applyResize(item, mouse, grabLeft, grabTop, grabRight, grabBottom) {
|
||||
const p = item.mapToItem(root, mouse.x, mouse.y);
|
||||
const dx = p.x - pressMouseX;
|
||||
const dy = p.y - pressMouseY;
|
||||
let left = pressRectX;
|
||||
let top = pressRectY;
|
||||
let right = pressRectX + pressRectW;
|
||||
let bottom = pressRectY + pressRectH;
|
||||
if (grabLeft) left = Math.min(Math.max(0, pressRectX + dx), right - root.minSize);
|
||||
if (grabTop) top = Math.min(Math.max(0, pressRectY + dy), bottom - root.minSize);
|
||||
if (grabRight) right = Math.max(Math.min(root.screenWidth, right + dx), left + root.minSize);
|
||||
if (grabBottom) bottom = Math.max(Math.min(root.screenHeight, bottom + dy), top + root.minSize);
|
||||
seedClamped(left, top, right - left, bottom - top);
|
||||
}
|
||||
|
||||
// Move: translate the whole rect, keeping its size
|
||||
function applyMove(item, mouse) {
|
||||
const p = item.mapToItem(root, mouse.x, mouse.y);
|
||||
const x = Math.max(0, Math.min(pressRectX + (p.x - pressMouseX), root.screenWidth - pressRectW));
|
||||
const y = Math.max(0, Math.min(pressRectY + (p.y - pressMouseY), root.screenHeight - pressRectH));
|
||||
seedClamped(x, y, pressRectW, pressRectH);
|
||||
}
|
||||
|
||||
// Final guard on every mutation: clamp to screen, then write endpoints only
|
||||
function seedClamped(x, y, w, h) {
|
||||
const clamped = RegionUtils.clampRegionToScreen(
|
||||
{ x: x, y: y, width: w, height: h },
|
||||
root.screenWidth, root.screenHeight
|
||||
);
|
||||
root.dragState.seedRect(clamped.x, clamped.y, clamped.width, clamped.height);
|
||||
}
|
||||
|
||||
// Interior move-grab (below the handles so they win on overlap)
|
||||
MouseArea {
|
||||
id: moveArea
|
||||
z: 1
|
||||
x: root.dragState.regionX
|
||||
y: root.dragState.regionY
|
||||
width: root.dragState.regionWidth
|
||||
height: root.dragState.regionHeight
|
||||
cursorShape: Qt.SizeAllCursor
|
||||
acceptedButtons: Qt.LeftButton
|
||||
preventStealing: true
|
||||
onPressed: (mouse) => root.beginGrab(moveArea, mouse)
|
||||
onPositionChanged: (mouse) => {
|
||||
if (moveArea.pressed) root.applyMove(moveArea, mouse);
|
||||
}
|
||||
}
|
||||
|
||||
// 8 handles: fx/fy are the handle's fractional position on the rect
|
||||
// (0 = left/top edge, 0.5 = edge midpoint, 1 = right/bottom edge)
|
||||
Repeater {
|
||||
model: [
|
||||
{ fx: 0.0, fy: 0.0, cursor: Qt.SizeFDiagCursor }, // top-left
|
||||
{ fx: 0.5, fy: 0.0, cursor: Qt.SizeVerCursor }, // top
|
||||
{ fx: 1.0, fy: 0.0, cursor: Qt.SizeBDiagCursor }, // top-right
|
||||
{ fx: 0.0, fy: 0.5, cursor: Qt.SizeHorCursor }, // left
|
||||
{ fx: 1.0, fy: 0.5, cursor: Qt.SizeHorCursor }, // right
|
||||
{ fx: 0.0, fy: 1.0, cursor: Qt.SizeBDiagCursor }, // bottom-left
|
||||
{ fx: 0.5, fy: 1.0, cursor: Qt.SizeVerCursor }, // bottom
|
||||
{ fx: 1.0, fy: 1.0, cursor: Qt.SizeFDiagCursor } // bottom-right
|
||||
]
|
||||
delegate: MouseArea {
|
||||
id: handleArea
|
||||
required property var modelData
|
||||
// Corners above edge midpoints: on small rects their hit areas still
|
||||
// brush against each other, and the corner must win the overlap
|
||||
z: (handleArea.modelData.fx === 0.5 || handleArea.modelData.fy === 0.5) ? 2 : 3
|
||||
x: root.dragState.regionX + handleArea.modelData.fx * root.dragState.regionWidth - root.hitSize / 2
|
||||
y: root.dragState.regionY + handleArea.modelData.fy * root.dragState.regionHeight - root.hitSize / 2
|
||||
width: root.hitSize
|
||||
height: root.hitSize
|
||||
cursorShape: handleArea.modelData.cursor
|
||||
acceptedButtons: Qt.LeftButton
|
||||
preventStealing: true
|
||||
onPressed: (mouse) => root.beginGrab(handleArea, mouse)
|
||||
onPositionChanged: (mouse) => {
|
||||
if (handleArea.pressed) {
|
||||
root.applyResize(handleArea, mouse,
|
||||
handleArea.modelData.fx === 0.0, handleArea.modelData.fy === 0.0,
|
||||
handleArea.modelData.fx === 1.0, handleArea.modelData.fy === 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle visual: filled dot matching the selection border color
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: root.handleSize
|
||||
height: root.handleSize
|
||||
radius: root.handleSize / 2
|
||||
color: root.handleColor
|
||||
border.color: root.handleBorderColor
|
||||
border.width: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,9 @@ Toolbar {
|
||||
// Use a synchronizer on these
|
||||
property var action
|
||||
property var selectionMode
|
||||
property var captureMode
|
||||
// True while the adjustable crop editor is open (plain binding from RegionSelection)
|
||||
property bool cropEditing: false
|
||||
// Monitor list for full-screen capture buttons
|
||||
property var monitors: []
|
||||
// Signals
|
||||
@@ -31,6 +34,18 @@ Toolbar {
|
||||
// Use ActionConfig singleton for action metadata
|
||||
readonly property var actionConfig: ActionConfig.getConfig(root.action)
|
||||
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)
|
||||
onCaptureModeChanged: {
|
||||
const idx = root.cropActive ? 1 : 0;
|
||||
if (captureModeTabBar.currentIndex !== idx) captureModeTabBar.setCurrentIndex(idx);
|
||||
}
|
||||
onSelectionModeChanged: {
|
||||
const idx = root.selectionMode === RegionSelection.SelectionMode.RectCorners ? 0 : 1;
|
||||
if (tabBar.currentIndex !== idx) tabBar.setCurrentIndex(idx);
|
||||
}
|
||||
|
||||
// Action indicator shape
|
||||
MaterialShape {
|
||||
@@ -50,9 +65,11 @@ Toolbar {
|
||||
}
|
||||
}
|
||||
|
||||
// Selection mode tabs (Rect/Circle)
|
||||
// Selection mode tabs (Rect/Circle) - inert while Crop is active (Crop implies rectangles)
|
||||
ToolbarTabBar {
|
||||
id: tabBar
|
||||
enabled: !root.cropActive
|
||||
opacity: enabled ? 1 : 0.4
|
||||
tabButtonList: [
|
||||
{"icon": "activity_zone", "name": Translation.tr("Rect")},
|
||||
{"icon": "gesture", "name": Translation.tr("Circle")}
|
||||
@@ -65,6 +82,25 @@ Toolbar {
|
||||
}
|
||||
}
|
||||
|
||||
// Capture mode tabs (Instant/Crop) - inert while the crop editor is open so a
|
||||
// stray click/wheel can't flip to Instant and destroy the adjusted crop
|
||||
// (Esc, C and the FABs are the editor exits)
|
||||
ToolbarTabBar {
|
||||
id: captureModeTabBar
|
||||
enabled: !root.cropEditing
|
||||
opacity: enabled ? 1 : 0.4
|
||||
tabButtonList: [
|
||||
{"icon": "bolt", "name": Translation.tr("Instant")},
|
||||
{"icon": "crop", "name": Translation.tr("Crop")}
|
||||
]
|
||||
Component.onCompleted: {
|
||||
currentIndex = root.cropActive ? 1 : 0
|
||||
}
|
||||
onCurrentIndexChanged: {
|
||||
root.captureMode = currentIndex === 0 ? RegionSelection.CaptureMode.Instant : RegionSelection.CaptureMode.Crop;
|
||||
}
|
||||
}
|
||||
|
||||
// Monitor capture buttons - one-click full-screen capture for each monitor
|
||||
// Left-click: capture and copy to clipboard
|
||||
// Right-click: capture and edit with swappy
|
||||
|
||||
@@ -61,6 +61,17 @@ QtObject {
|
||||
regionHeight = targetedRegionHeight + padding * 2;
|
||||
}
|
||||
|
||||
// Seed the selection region by writing drag endpoints (Crop mode).
|
||||
// The only sanctioned way Crop paths set geometry: region* are bindings
|
||||
// over dragStart*/dragging*, and assigning them directly would break the
|
||||
// bindings for the rest of the overlay session (Esc-back-to-draw).
|
||||
function seedRect(x: real, y: real, w: real, h: real) {
|
||||
dragStartX = x;
|
||||
dragStartY = y;
|
||||
draggingX = x + w;
|
||||
draggingY = y + h;
|
||||
}
|
||||
|
||||
function startDrag(x: real, y: real, button) {
|
||||
dragStartX = x;
|
||||
dragStartY = y;
|
||||
|
||||
@@ -29,10 +29,42 @@ PanelWindow {
|
||||
|
||||
enum SnipAction { Copy, Edit, Search, CharRecognition, Record, RecordWithSound }
|
||||
enum SelectionMode { RectCorners, Circle }
|
||||
enum CaptureMode { Instant, Crop }
|
||||
property var action: RegionSelection.SnipAction.Copy
|
||||
property var selectionMode: RegionSelection.SelectionMode.RectCorners
|
||||
property var 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
|
||||
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.
|
||||
onCaptureModeChanged: {
|
||||
root.cropEditing = false;
|
||||
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.
|
||||
function sessionReset() {
|
||||
root.cropEditing = false;
|
||||
dragState.reset();
|
||||
root.captureMode = RegionSelection.CaptureMode.Instant;
|
||||
}
|
||||
|
||||
// Monitor capture support
|
||||
property var allMonitors: []
|
||||
property string targetMonitorCapture: ""
|
||||
@@ -44,8 +76,42 @@ PanelWindow {
|
||||
captureFullMonitorLocal();
|
||||
}
|
||||
|
||||
// Open the crop editor seeded with the given rect, normalized for editability:
|
||||
// clamped to screen bounds, then grown per axis to >= minSize while staying on
|
||||
// screen. Every editor entry path (drag release, targeted click, full monitor)
|
||||
// goes through here so sub-minSize or offscreen seeds can't produce an editor
|
||||
// with unreachable handles or jumping clamps.
|
||||
// Seeds via dragState.seedRect (endpoint writes) — region* bindings stay intact.
|
||||
function openCropEditor(x, y, w, h) {
|
||||
const minSize = Math.max(1, Config.options.regionSelector.crop.minSize);
|
||||
const clamped = RegionUtils.clampRegionToScreen(
|
||||
{ x: x, y: y, width: w, height: h },
|
||||
root.screen.width, root.screen.height
|
||||
);
|
||||
const rw = Math.min(root.screen.width, Math.max(clamped.width, minSize));
|
||||
const rh = Math.min(root.screen.height, Math.max(clamped.height, minSize));
|
||||
const rx = Math.min(clamped.x, root.screen.width - rw);
|
||||
const ry = Math.min(clamped.y, root.screen.height - rh);
|
||||
dragState.seedRect(rx, ry, rw, rh);
|
||||
dragState.endDrag();
|
||||
root.cropEditing = true;
|
||||
}
|
||||
|
||||
// Confirm the crop editor: latch it closed before dispatching so a held Enter
|
||||
// key or a queued FAB click can't fire snip() a second time before teardown
|
||||
function confirmCrop() {
|
||||
if (!root.cropEditing) return;
|
||||
root.cropEditing = false;
|
||||
root.snip();
|
||||
}
|
||||
|
||||
// Capture the full region of this monitor
|
||||
function captureFullMonitorLocal() {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
dragState.regionX = 0;
|
||||
dragState.regionY = 0;
|
||||
dragState.regionWidth = root.screen.width;
|
||||
@@ -261,14 +327,36 @@ PanelWindow {
|
||||
}
|
||||
|
||||
ScreencopyView {
|
||||
id: screencopyView
|
||||
anchors.fill: parent
|
||||
live: false
|
||||
captureSource: root.screen
|
||||
|
||||
focus: root.visible
|
||||
Keys.onPressed: (event) => { // Esc to close
|
||||
// QQC2 buttons (toolbar tabs, FABs, monitor buttons) grab focus on click and
|
||||
// would silently kill Esc/C/Enter. Nothing else in the overlay takes keyboard
|
||||
// input, so always reclaim it while visible.
|
||||
onActiveFocusChanged: {
|
||||
if (root.visible && !screencopyView.activeFocus) screencopyView.forceActiveFocus();
|
||||
}
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.dismiss();
|
||||
if (root.cropEditing) {
|
||||
// First Esc: cancel the editor, back to draw (frame retained)
|
||||
root.cropEditing = false;
|
||||
dragState.reset();
|
||||
} else {
|
||||
root.dismiss();
|
||||
}
|
||||
} else if (event.key === Qt.Key_C && event.modifiers === Qt.NoModifier && !dragState.dragging) {
|
||||
// Hardcoded toggle, per-screen scope (same as the Rect/Circle toolbar
|
||||
// toggle). Ignored mid-drag: the mode swap resets dragState, and the
|
||||
// orphaned release would then dismiss the overlay as an empty click.
|
||||
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) {
|
||||
root.confirmCrop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,6 +366,9 @@ PanelWindow {
|
||||
cursorShape: Qt.CrossCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
hoverEnabled: true
|
||||
// While the crop editor is open, CropHandles owns the mouse
|
||||
// (MouseArea.enabled only mutes this area, not its children)
|
||||
enabled: !root.cropEditing
|
||||
|
||||
// Controls
|
||||
onPressed: (mouse) => {
|
||||
@@ -289,7 +380,19 @@ PanelWindow {
|
||||
if (!dragState.draggedAway) {
|
||||
if (dragState.targetedRegionValid()) {
|
||||
const padding = Config.options.regionSelector.targetRegions.selectionPadding;
|
||||
dragState.setRegionToTargeted(padding);
|
||||
if (root.isCropMode) {
|
||||
// Crop: open the editor seeded with the padded target
|
||||
// (openCropEditor normalizes edge-of-screen overhang)
|
||||
root.openCropEditor(
|
||||
dragState.targetedRegionX - padding,
|
||||
dragState.targetedRegionY - padding,
|
||||
dragState.targetedRegionWidth + padding * 2,
|
||||
dragState.targetedRegionHeight + padding * 2
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
dragState.setRegionToTargeted(padding);
|
||||
}
|
||||
} else {
|
||||
dragState.endDrag();
|
||||
root.dismiss();
|
||||
@@ -302,6 +405,13 @@ PanelWindow {
|
||||
dragState.setRegionFromCirclePoints(padding, mouseArea.mouseX, mouseArea.mouseY);
|
||||
}
|
||||
dragState.endDrag();
|
||||
if (root.isCropMode) {
|
||||
// Crop: open the adjustable editor instead of dispatching
|
||||
// (openCropEditor normalizes sub-minSize/one-axis drags)
|
||||
root.openCropEditor(dragState.regionX, dragState.regionY,
|
||||
dragState.regionWidth, dragState.regionHeight);
|
||||
return;
|
||||
}
|
||||
root.snip();
|
||||
}
|
||||
onPositionChanged: (mouse) => {
|
||||
@@ -322,6 +432,23 @@ PanelWindow {
|
||||
mouseY: mouseArea.mouseY
|
||||
color: root.selectionBorderColor
|
||||
overlayColor: root.overlayColor
|
||||
// mouseArea is muted while crop-editing, so hide the (frozen) crosshair
|
||||
showAimLines: Config.options.regionSelector.rect.showAimLines && !root.cropEditing
|
||||
}
|
||||
}
|
||||
|
||||
// Crop editor: resize handles + interior move-grab, rendered above the
|
||||
// selection details (border/label stay live via the region* bindings)
|
||||
Loader {
|
||||
z: 5
|
||||
anchors.fill: parent
|
||||
active: root.cropEditing
|
||||
sourceComponent: CropHandles {
|
||||
dragState: dragState
|
||||
screenWidth: root.screen.width
|
||||
screenHeight: root.screen.height
|
||||
handleColor: root.selectionBorderColor
|
||||
handleBorderColor: root.onBorderColor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,16 +543,41 @@ PanelWindow {
|
||||
|
||||
OptionsToolbar {
|
||||
monitors: root.allMonitors
|
||||
cropEditing: root.cropEditing
|
||||
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)
|
||||
}
|
||||
Item {
|
||||
visible: root.cropEditing
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
implicitWidth: confirmFab.implicitWidth
|
||||
implicitHeight: confirmFab.implicitHeight
|
||||
StyledRectangularShadow {
|
||||
target: confirmFab
|
||||
radius: confirmFab.buttonRadius
|
||||
}
|
||||
FloatingActionButton {
|
||||
id: confirmFab
|
||||
baseSize: 48
|
||||
iconText: "check"
|
||||
onClicked: root.confirmCrop();
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Confirm")
|
||||
}
|
||||
}
|
||||
}
|
||||
Item {
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
|
||||
@@ -27,6 +27,11 @@ Scope {
|
||||
|
||||
property var action: RegionSelection.SnipAction.Copy
|
||||
property var selectionMode: RegionSelection.SelectionMode.RectCorners
|
||||
property var 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
|
||||
signal sessionReset()
|
||||
|
||||
// ─── Monitor List Management ─────────────────────────────────────────────────
|
||||
property var monitorList: []
|
||||
@@ -63,6 +68,11 @@ Scope {
|
||||
|
||||
function captureMonitor(monitorName: string) {
|
||||
root.targetMonitorCapture = monitorName;
|
||||
// Clear immediately (the change signal has already fired synchronously on the
|
||||
// target instance): in Crop mode the capture seeds the editor without
|
||||
// dismissing, and a stale value would make re-clicking the same monitor
|
||||
// button a silent no-op
|
||||
root.targetMonitorCapture = "";
|
||||
}
|
||||
|
||||
// ─── Region Selection Windows ────────────────────────────────────────────────
|
||||
@@ -74,6 +84,7 @@ Scope {
|
||||
active: GlobalStates.regionSelectorOpen
|
||||
|
||||
sourceComponent: RegionSelection {
|
||||
id: selectionWindow
|
||||
screen: regionSelectorLoader.modelData
|
||||
allMonitors: root.monitorList
|
||||
targetMonitorCapture: root.targetMonitorCapture
|
||||
@@ -81,6 +92,18 @@ Scope {
|
||||
onCaptureMonitorRequested: (monitorName) => root.captureMonitor(monitorName)
|
||||
action: root.action
|
||||
selectionMode: root.selectionMode
|
||||
captureMode: root.captureMode
|
||||
|
||||
// Re-triggering a capture shortcut while the overlay is open must
|
||||
// restore Instant/draw defaults even on instances whose captureMode/
|
||||
// selectionMode bindings were broken by local writes (C key, toolbar)
|
||||
Connections {
|
||||
target: root
|
||||
function onSessionReset() {
|
||||
selectionWindow.sessionReset();
|
||||
selectionWindow.selectionMode = root.selectionMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,6 +114,10 @@ Scope {
|
||||
function openWithAction(action) {
|
||||
root.action = action;
|
||||
root.selectionMode = getSelectionModeForAction(action);
|
||||
root.captureMode = RegionSelection.CaptureMode.Instant; // Never persists across opens
|
||||
// Instances alive from a still-open overlay may hold locally-written state
|
||||
// (broken bindings); push the Instant/draw defaults to them explicitly
|
||||
root.sessionReset();
|
||||
GlobalStates.regionSelectorOpen = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,4 +9,5 @@ MonitorButton 1.0 MonitorButton.qml
|
||||
OptionsToolbar 1.0 OptionsToolbar.qml
|
||||
TargetRegion 1.0 TargetRegion.qml
|
||||
CircleSelectionDetails 1.0 CircleSelectionDetails.qml
|
||||
CropHandles 1.0 CropHandles.qml
|
||||
RectCornersSelectionDetails 1.0 RectCornersSelectionDetails.qml
|
||||
Reference in new issue
Block a user