Skip to content

Commit 9780e83

Browse files
committed
feat(captureOverlay): add selection resizing and drag functionality
1 parent 62e3ae2 commit 9780e83

1 file changed

Lines changed: 245 additions & 22 deletions

File tree

CaptureOverlay.qml

Lines changed: 245 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ Window {
1515

1616
property real startX: 0
1717
property real startY: 0
18+
property real dragSelectionX: 0
19+
property real dragSelectionY: 0
20+
property real dragSelectionWidth: 0
21+
property real dragSelectionHeight: 0
1822
property real selectionX: 0
1923
property real selectionY: 0
2024
property real selectionWidth: 0
@@ -29,11 +33,16 @@ Window {
2933
property bool textEditing: false
3034
property string activeTool: ""
3135
property string previousDrawTool: "pen"
36+
property string selectionDragMode: ""
3237
property string selectedColor: "#FF5967"
3338
property string hoveredPixelColor: "#000000"
3439
property var annotations: []
3540
property var draftAnnotation: null
3641
readonly property color shadeColor: "#52000000"
42+
readonly property real minimumSelectionSize: 8
43+
readonly property real newSelectionDragThreshold: 3
44+
readonly property real resizeHitMargin: 7
45+
readonly property real selectionHandleSize: 8
3746
readonly property bool selectionValid: selectionWidth >= 8 && selectionHeight >= 8
3847
readonly property real imageLeft: (width - baseImage.paintedWidth) / 2
3948
readonly property real imageTop: (height - baseImage.paintedHeight) / 2
@@ -70,6 +79,7 @@ Window {
7079
textEditing = false
7180
activeTool = ""
7281
previousDrawTool = "pen"
82+
selectionDragMode = ""
7383
annotations = []
7484
draftAnnotation = null
7585
showFullScreen()
@@ -84,6 +94,171 @@ Window {
8494
return Math.max(selectableTop, Math.min(selectableBottom, value))
8595
}
8696

97+
function resizeModeAt(x, y) {
98+
if (!editing || !selectionValid)
99+
return ""
100+
101+
const left = selectionX
102+
const top = selectionY
103+
const right = selectionX + selectionWidth
104+
const bottom = selectionY + selectionHeight
105+
const nearLeft = Math.abs(x - left) <= resizeHitMargin
106+
const nearRight = Math.abs(x - right) <= resizeHitMargin
107+
const nearTop = Math.abs(y - top) <= resizeHitMargin
108+
const nearBottom = Math.abs(y - bottom) <= resizeHitMargin
109+
const withinHorizontal = x >= left - resizeHitMargin
110+
&& x <= right + resizeHitMargin
111+
const withinVertical = y >= top - resizeHitMargin
112+
&& y <= bottom + resizeHitMargin
113+
114+
if (nearLeft && nearTop) return "nw"
115+
if (nearRight && nearTop) return "ne"
116+
if (nearLeft && nearBottom) return "sw"
117+
if (nearRight && nearBottom) return "se"
118+
if (nearTop && withinHorizontal) return "n"
119+
if (nearBottom && withinHorizontal) return "s"
120+
if (nearLeft && withinVertical) return "w"
121+
if (nearRight && withinVertical) return "e"
122+
return ""
123+
}
124+
125+
function resizeCursor(mode) {
126+
if (mode === "n" || mode === "s") return Qt.SizeVerCursor
127+
if (mode === "e" || mode === "w") return Qt.SizeHorCursor
128+
if (mode === "nw" || mode === "se") return Qt.SizeFDiagCursor
129+
if (mode === "ne" || mode === "sw") return Qt.SizeBDiagCursor
130+
return Qt.ArrowCursor
131+
}
132+
133+
function selectionCursorShape() {
134+
if (selectionDragMode === "new" || selectionDragMode === "pendingNew")
135+
return Qt.CrossCursor
136+
const mode = selectionDragMode !== ""
137+
? selectionDragMode : resizeModeAt(pointerX, pointerY)
138+
if (mode !== "")
139+
return resizeCursor(mode)
140+
if (!editing || !pointInsideSelection(pointerX, pointerY))
141+
return Qt.CrossCursor
142+
if (activeTool === "text")
143+
return Qt.IBeamCursor
144+
return activeTool !== "" ? Qt.CrossCursor : Qt.ArrowCursor
145+
}
146+
147+
function handleCenterX(mode) {
148+
if (mode.indexOf("w") !== -1) return selectionX
149+
if (mode.indexOf("e") !== -1) return selectionX + selectionWidth
150+
return selectionX + selectionWidth / 2
151+
}
152+
153+
function handleCenterY(mode) {
154+
if (mode.indexOf("n") !== -1) return selectionY
155+
if (mode.indexOf("s") !== -1) return selectionY + selectionHeight
156+
return selectionY + selectionHeight / 2
157+
}
158+
159+
function clearSelectionContent() {
160+
editing = false
161+
activeTool = ""
162+
colorPaletteVisible = false
163+
textEditing = false
164+
textInput.text = ""
165+
annotations = []
166+
draftAnnotation = null
167+
annotationLayer.requestPaint()
168+
}
169+
170+
function beginNewSelection(x, y) {
171+
clearSelectionContent()
172+
const imageX = clampImageX(x)
173+
const imageY = clampImageY(y)
174+
startX = imageX
175+
startY = imageY
176+
selectionX = imageX
177+
selectionY = imageY
178+
selectionWidth = 0
179+
selectionHeight = 0
180+
selectionDragMode = "new"
181+
}
182+
183+
function prepareNewSelection(x, y) {
184+
startX = clampImageX(x)
185+
startY = clampImageY(y)
186+
colorPaletteVisible = false
187+
selectionDragMode = "pendingNew"
188+
}
189+
190+
function beginSelectionResize(mode) {
191+
selectionDragMode = mode
192+
dragSelectionX = selectionX
193+
dragSelectionY = selectionY
194+
dragSelectionWidth = selectionWidth
195+
dragSelectionHeight = selectionHeight
196+
colorPaletteVisible = false
197+
draftAnnotation = null
198+
}
199+
200+
function translateAnnotations(deltaX, deltaY) {
201+
if ((deltaX === 0 && deltaY === 0) || annotations.length === 0)
202+
return
203+
204+
const translated = []
205+
for (let i = 0; i < annotations.length; ++i) {
206+
const item = annotations[i]
207+
const copy = {}
208+
for (let key in item)
209+
copy[key] = item[key]
210+
if (typeof item.x1 === "number") copy.x1 = item.x1 + deltaX
211+
if (typeof item.y1 === "number") copy.y1 = item.y1 + deltaY
212+
if (typeof item.x2 === "number") copy.x2 = item.x2 + deltaX
213+
if (typeof item.y2 === "number") copy.y2 = item.y2 + deltaY
214+
if (item.points) {
215+
copy.points = []
216+
for (let j = 0; j < item.points.length; ++j) {
217+
copy.points.push({
218+
x: item.points[j].x + deltaX,
219+
y: item.points[j].y + deltaY
220+
})
221+
}
222+
}
223+
translated.push(copy)
224+
}
225+
annotations = translated
226+
}
227+
228+
function updateSelectionResize(x, y) {
229+
const mode = selectionDragMode
230+
const originalRight = dragSelectionX + dragSelectionWidth
231+
const originalBottom = dragSelectionY + dragSelectionHeight
232+
const imageX = clampImageX(x)
233+
const imageY = clampImageY(y)
234+
let left = dragSelectionX
235+
let top = dragSelectionY
236+
let right = originalRight
237+
let bottom = originalBottom
238+
239+
if (mode.indexOf("w") !== -1)
240+
left = Math.max(selectableLeft,
241+
Math.min(originalRight - minimumSelectionSize, imageX))
242+
if (mode.indexOf("e") !== -1)
243+
right = Math.min(selectableRight,
244+
Math.max(dragSelectionX + minimumSelectionSize, imageX))
245+
if (mode.indexOf("n") !== -1)
246+
top = Math.max(selectableTop,
247+
Math.min(originalBottom - minimumSelectionSize, imageY))
248+
if (mode.indexOf("s") !== -1)
249+
bottom = Math.min(selectableBottom,
250+
Math.max(dragSelectionY + minimumSelectionSize, imageY))
251+
252+
const deltaX = selectionX - left
253+
const deltaY = selectionY - top
254+
selectionX = left
255+
selectionY = top
256+
selectionWidth = right - left
257+
selectionHeight = bottom - top
258+
translateAnnotations(deltaX, deltaY)
259+
annotationLayer.requestPaint()
260+
}
261+
87262
function cancel() {
88263
if (finishing)
89264
return
@@ -103,16 +278,10 @@ Window {
103278
}
104279

105280
function resetSelection() {
106-
editing = false
107-
activeTool = ""
108-
colorPaletteVisible = false
109-
textEditing = false
110-
textInput.text = ""
111-
annotations = []
112-
draftAnnotation = null
281+
selectionDragMode = ""
282+
clearSelectionContent()
113283
selectionWidth = 0
114284
selectionHeight = 0
115-
annotationLayer.requestPaint()
116285
}
117286

118287
function undo() {
@@ -259,13 +428,35 @@ Window {
259428
z: 3
260429
}
261430

431+
Item {
432+
anchors.fill: parent
433+
visible: overlay.editing && overlay.selectionValid
434+
z: 6
435+
436+
Repeater {
437+
model: ["nw", "n", "ne", "e", "se", "s", "sw", "w"]
438+
439+
delegate: Rectangle {
440+
id: selectionHandle
441+
required property string modelData
442+
x: overlay.handleCenterX(modelData) - width / 2
443+
y: overlay.handleCenterY(modelData) - height / 2
444+
width: overlay.selectionHandleSize
445+
height: overlay.selectionHandleSize
446+
radius: 1
447+
color: "white"
448+
border.width: 1
449+
border.color: "#4358E8"
450+
}
451+
}
452+
}
453+
262454
MouseArea {
263455
id: selectMouse
264456
anchors.fill: parent
265457
enabled: !overlay.textEditing
266-
cursorShape: !overlay.editing ? Qt.CrossCursor
267-
: overlay.activeTool === "text" ? Qt.IBeamCursor
268-
: overlay.activeTool !== "" ? Qt.CrossCursor : Qt.ArrowCursor
458+
hoverEnabled: true
459+
cursorShape: overlay.selectionCursorShape()
269460
z: 5
270461

271462
onPressed: mouse => {
@@ -274,17 +465,20 @@ Window {
274465
overlay.pointerX = mouse.x
275466
overlay.pointerY = mouse.y
276467
if (!overlay.editing) {
277-
const imageX = overlay.clampImageX(mouse.x)
278-
const imageY = overlay.clampImageY(mouse.y)
279-
overlay.startX = imageX
280-
overlay.startY = imageY
281-
overlay.selectionX = imageX
282-
overlay.selectionY = imageY
283-
overlay.selectionWidth = 0
284-
overlay.selectionHeight = 0
468+
overlay.beginNewSelection(mouse.x, mouse.y)
285469
return
286470
}
287-
if (overlay.activeTool === "" || !overlay.pointInsideSelection(mouse.x, mouse.y))
471+
472+
const resizeMode = overlay.resizeModeAt(mouse.x, mouse.y)
473+
if (resizeMode !== "") {
474+
overlay.beginSelectionResize(resizeMode)
475+
return
476+
}
477+
if (!overlay.pointInsideSelection(mouse.x, mouse.y)) {
478+
overlay.prepareNewSelection(mouse.x, mouse.y)
479+
return
480+
}
481+
if (overlay.activeTool === "")
288482
return
289483

290484
if (overlay.activeTool === "picker") {
@@ -323,7 +517,17 @@ Window {
323517
}
324518
if (!pressed || overlay.finishing)
325519
return
326-
if (!overlay.editing) {
520+
if (overlay.selectionDragMode === "pendingNew") {
521+
const pendingX = overlay.clampImageX(mouse.x)
522+
const pendingY = overlay.clampImageY(mouse.y)
523+
if (Math.abs(pendingX - overlay.startX) < overlay.newSelectionDragThreshold
524+
&& Math.abs(pendingY - overlay.startY)
525+
< overlay.newSelectionDragThreshold) {
526+
return
527+
}
528+
overlay.beginNewSelection(overlay.startX, overlay.startY)
529+
}
530+
if (overlay.selectionDragMode === "new") {
327531
const imageX = overlay.clampImageX(mouse.x)
328532
const imageY = overlay.clampImageY(mouse.y)
329533
overlay.selectionX = Math.min(overlay.startX, imageX)
@@ -332,6 +536,10 @@ Window {
332536
overlay.selectionHeight = Math.abs(imageY - overlay.startY)
333537
return
334538
}
539+
if (overlay.selectionDragMode !== "") {
540+
overlay.updateSelectionResize(mouse.x, mouse.y)
541+
return
542+
}
335543
if (!overlay.draftAnnotation)
336544
return
337545
const p = overlay.localPoint(mouse.x, mouse.y)
@@ -345,12 +553,21 @@ Window {
345553
onReleased: {
346554
if (overlay.finishing)
347555
return
348-
if (!overlay.editing) {
556+
if (overlay.selectionDragMode === "pendingNew") {
557+
overlay.selectionDragMode = ""
558+
return
559+
}
560+
if (overlay.selectionDragMode === "new") {
561+
overlay.selectionDragMode = ""
349562
if (overlay.selectionValid) {
350563
overlay.editing = true
351564
}
352565
return
353566
}
567+
if (overlay.selectionDragMode !== "") {
568+
overlay.selectionDragMode = ""
569+
return
570+
}
354571
if (!overlay.draftAnnotation)
355572
return
356573
const dx = Math.abs(overlay.draftAnnotation.x2 - overlay.draftAnnotation.x1)
@@ -359,6 +576,12 @@ Window {
359576
overlay.annotations = overlay.annotations.concat([overlay.draftAnnotation])
360577
overlay.draftAnnotation = null
361578
}
579+
580+
onCanceled: {
581+
overlay.selectionDragMode = ""
582+
overlay.draftAnnotation = null
583+
overlay.editing = overlay.selectionValid
584+
}
362585
}
363586

364587
Rectangle {

0 commit comments

Comments
 (0)