1/*
2 * Copyright (c) 2014-2020 Meltytech, LLC
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18import QtQuick 2.1
19
20Item {
21    id: item
22    anchors.fill: parent
23
24    property real widthScale: 1.0
25    property real heightScale: 1.0
26    property real aspectRatio: 0.0
27    property int handleSize: 10
28    property int borderSize: 2
29    property alias rectangle: rectangle
30    property color handleColor: Qt.rgba(1, 1, 1, enabled? 0.9 : 0.2)
31    property int snapMargin: 10
32    property alias withRotation: rotationHandle.visible
33    property alias rotation: rotationGroup.rotation
34    property bool _positionDragLocked: false
35    property bool _positionDragEnabled: false
36
37    signal rectChanged(Rectangle rect)
38    signal rotated(real degrees, var mouse)
39    signal rotationReleased()
40
41    Component.onCompleted: {
42        _positionDragLocked = filter.get('_shotcut:positionDragLocked') === '1'
43    }
44
45    function setHandles(rect) {
46        if ( rect.width < 0 || rect.height < 0)
47            return
48        topLeftHandle.x = (rect.x * widthScale)
49        topLeftHandle.y = (rect.y * heightScale)
50        if (aspectRatio === 0.0) {
51            bottomRightHandle.x = topLeftHandle.x + (rect.width * widthScale) - handleSize
52            bottomRightHandle.y = topLeftHandle.y + (rect.height * heightScale) - handleSize
53        } else if (aspectRatio > 1.0) {
54            bottomRightHandle.x = topLeftHandle.x + (rect.width * widthScale) - handleSize
55            bottomRightHandle.y = topLeftHandle.y + (rect.width * widthScale / aspectRatio) - handleSize
56        } else {
57            bottomRightHandle.x = topLeftHandle.x + (rect.height * heightScale * aspectRatio) - handleSize
58            bottomRightHandle.y = topLeftHandle.y + (rect.height * heightScale) - handleSize
59        }
60        topRightHandle.x = bottomRightHandle.x
61        topRightHandle.y = topLeftHandle.y
62        bottomLeftHandle.x = topLeftHandle.x
63        bottomLeftHandle.y = bottomRightHandle.y
64    }
65
66    function snapGrid(v, gridSize) {
67        var polarity = (v < 0) ? -1 : 1
68        v = v * polarity
69        var delta = v % gridSize
70        if (delta < snapMargin) {
71            v = v - delta
72        } else if ((gridSize - delta) < snapMargin) {
73            v = v + gridSize - delta
74        }
75        return v * polarity
76    }
77
78    function snapX(x) {
79        if (!video.snapToGrid || video.grid === 0) {
80            return x
81        }
82        if (video.grid !== 95 && video.grid !== 8090) {
83            var n = (video.grid > 10000) ? video.grid - 10000 : parent.width / video.grid
84            return snapGrid(x, n)
85        } else {
86            var deltas = null
87            if (video.grid === 8090) {
88                // 80/90% Safe Areas
89                deltas = [0.0, 0.05, 0.1, 0.9, 0.95, 1.0]
90            } else if (video.grid === 95) {
91                // EBU R95 Safe Areas
92                deltas = [0.0, 0.035, 0.05, 0.95, 0.965, 1.0]
93            }
94            if (deltas) {
95                for (var i = 0; i < deltas.length; i++) {
96                    var delta = x - deltas[i] * parent.width
97                    if (Math.abs(delta) < snapMargin)
98                        return x - delta
99                }
100            }
101        }
102        return x
103    }
104
105    function snapY(y) {
106        if (!video.snapToGrid || video.grid === 0) {
107            return y
108        }
109        if (video.grid !== 95 && video.grid !== 8090) {
110            var n = (video.grid > 10000) ? video.grid - 10000 : parent.height / video.grid
111            return snapGrid(y, n)
112        } else {
113            var deltas = null
114            if (video.grid === 8090) {
115                // 80/90% Safe Areas
116                deltas = [0.0, 0.05, 0.1, 0.9, 0.95, 1.0]
117            } else if (video.grid === 95) {
118                // EBU R95 Safe Areas
119                deltas = [0.0, 0.035, 0.05, 0.95, 0.965, 1.0]
120            }
121            if (deltas) {
122                for (var i = 0; i < deltas.length; i++) {
123                    var delta = y - deltas[i] * parent.height
124                    if (Math.abs(delta) < snapMargin)
125                        return y - delta
126                }
127            }
128        }
129        return y
130    }
131
132    function isRotated() {
133        //Math.abs(rotationGroup.rotation - 0) > 0.0001
134        return rotationGroup.rotation !== 0 || rotationLine.rotation !== 0
135    }
136
137    Rectangle {
138        id: rectangle
139        visible: !isRotated()
140        color: 'transparent'
141        border.width: borderSize
142        border.color: handleColor
143        anchors.top: topLeftHandle.top
144        anchors.left: topLeftHandle.left
145        anchors.right: bottomRightHandle.right
146        anchors.bottom: bottomRightHandle.bottom
147        focus: true
148        Keys.onPressed: {
149            if (event.key === Qt.Key_Shift) {
150                _positionDragEnabled = true
151            }
152        }
153        Keys.onReleased: {
154            if (event.key === Qt.Key_Shift) {
155                _positionDragEnabled = false
156            }
157        }
158    }
159    Rectangle {
160        // Provides contrasting thick line to above rectangle.
161        visible: !isRotated()
162        color: 'transparent'
163        border.width: handleSize - borderSize
164        border.color: Qt.rgba(0, 0, 0, item.enabled? 0.4 : 0.2)
165        anchors.fill: rectangle
166        anchors.margins: borderSize
167    }
168
169    Item {
170        id: rotationGroup
171        anchors.fill: rectangle
172
173        Rectangle {
174            id: positionHandle
175            opacity: item.enabled? 0.5 : 0.2
176            border.width: borderSize
177            border.color: handleColor
178            width: handleSize * 2
179            height: handleSize * 2
180            radius: width / 2
181            anchors.centerIn: parent
182            z: 1
183            gradient: Gradient {
184                GradientStop {
185                    position: (_positionDragLocked || _positionDragEnabled || positionMouseArea.pressed)? 0.0 : 1.0
186                    color: 'black'
187                }
188                GradientStop {
189                    position: (_positionDragLocked || _positionDragEnabled || positionMouseArea.pressed)? 1.0 : 0.0
190                    color: 'white'
191                }
192            }
193            function centerX() { return x + width / 2 }
194        }
195
196        MouseArea {
197            id: positionMouseArea
198            anchors.fill: (_positionDragLocked || _positionDragEnabled)? parent : positionHandle
199            acceptedButtons: Qt.LeftButton
200            cursorShape: Qt.SizeAllCursor
201            drag.target: rectangle
202            onDoubleClicked: {
203                _positionDragLocked = !_positionDragLocked
204                filter.set('_shotcut:positionDragLocked', _positionDragLocked)
205            }
206            onEntered: {
207                rectangle.anchors.top = undefined
208                rectangle.anchors.left = undefined
209                rectangle.anchors.right = undefined
210                rectangle.anchors.bottom = undefined
211                topLeftHandle.anchors.left = rectangle.left
212                topLeftHandle.anchors.top = rectangle.top
213                topRightHandle.anchors.right = rectangle.right
214                topRightHandle.anchors.top = rectangle.top
215                bottomLeftHandle.anchors.left = rectangle.left
216                bottomLeftHandle.anchors.bottom = rectangle.bottom
217                bottomRightHandle.anchors.right = rectangle.right
218                bottomRightHandle.anchors.bottom = rectangle.bottom
219            }
220            onPositionChanged: {
221                rectangle.x = snapX(rectangle.x + rectangle.width / 2) - rectangle.width / 2
222                rectangle.y = snapY(rectangle.y + rectangle.height / 2) - rectangle.height / 2
223                rectChanged(rectangle)
224            }
225            onReleased: {
226                rectChanged(rectangle)
227                rectangle.anchors.top = topLeftHandle.top
228                rectangle.anchors.left = topLeftHandle.left
229                rectangle.anchors.right = bottomRightHandle.right
230                rectangle.anchors.bottom = bottomRightHandle.bottom
231                topLeftHandle.anchors.left = undefined
232                topLeftHandle.anchors.top = undefined
233                topRightHandle.anchors.right = undefined
234                topRightHandle.anchors.top = undefined
235                bottomLeftHandle.anchors.left = undefined
236                bottomLeftHandle.anchors.bottom = undefined
237                bottomRightHandle.anchors.right = undefined
238                bottomRightHandle.anchors.bottom = undefined
239            }
240        }
241
242        Rectangle {
243            id: rotationHandle
244            visible: false
245            color: handleColor
246            opacity: item.enabled? 0.5 : 0.2
247            width: handleSize * 1.5
248            height: handleSize * 1.5
249            radius: width / 2
250            z: 1
251            anchors.centerIn: rotationGroup
252            anchors.verticalCenterOffset: -item.height / 4
253            border.width: borderSize
254            border.color: Qt.rgba(0, 0, 0, enabled? 0.9 : 0.2)
255            function centerX() { return x + width / 2 }
256            MouseArea {
257                id: rotationMouseArea
258                anchors.fill: parent
259                acceptedButtons: Qt.LeftButton
260                cursorShape: pressed? Qt.ClosedHandCursor : Qt.OpenHandCursor
261                drag.target: parent
262                property real startRotation: 0
263                function getRotationDegrees() {
264                    var radians = Math.atan2(rotationHandle.centerX() - positionHandle.centerX(), positionHandle.y - rotationHandle.y)
265                    if (radians < 0)
266                        radians += 2 * Math.PI
267                    return 180 / Math.PI * radians
268                }
269
270                onPressed: {
271                    parent.anchors.centerIn = undefined
272                    startRotation = rotationGroup.rotation
273                }
274                onPositionChanged: {
275                    var degrees = getRotationDegrees()
276                    rotated(startRotation + degrees, mouse)
277                    rotationLine.rotation = degrees
278                }
279                onReleased: {
280                    rotationLine.rotation = 0
281                    rotationGroup.rotation = startRotation + getRotationDegrees()
282                    parent.anchors.centerIn = rotationGroup
283                    rotationReleased()
284                }
285            }
286        }
287        Rectangle {
288            id: rotationLine
289            height: -rotationHandle.anchors.verticalCenterOffset - rotationHandle.height + positionHandle.height / 2
290            anchors.horizontalCenter: positionHandle.horizontalCenter
291            anchors.bottom: positionHandle.verticalCenter
292            transformOrigin: Item.Bottom
293            width: 2
294            color: handleColor
295            visible: rotationHandle.visible
296            antialiasing: true
297        }
298    }
299
300    Rectangle {
301        id: topLeftHandle
302        visible: !isRotated()
303        color: handleColor
304        width: handleSize
305        height: handleSize
306        MouseArea {
307            anchors.fill: parent
308            acceptedButtons: Qt.LeftButton
309            cursorShape: Qt.SizeFDiagCursor
310            drag.target: parent
311            onEntered: {
312                rectangle.anchors.top = parent.top
313                rectangle.anchors.left = parent.left
314                topRightHandle.anchors.top = rectangle.top
315                bottomLeftHandle.anchors.left = rectangle.left
316            }
317            onPositionChanged: {
318                topLeftHandle.x = snapX(topLeftHandle.x)
319                topLeftHandle.y = snapY(topLeftHandle.y)
320                if (aspectRatio !== 0.0)
321                    parent.x = topRightHandle.x + handleSize - rectangle.height * aspectRatio
322                parent.x = Math.min(parent.x, bottomRightHandle.x)
323                parent.y = Math.min(parent.y, bottomRightHandle.y)
324                rectChanged(rectangle)
325            }
326            onReleased: {
327                rectChanged(rectangle)
328                topRightHandle.anchors.top = undefined
329                bottomLeftHandle.anchors.left = undefined
330            }
331        }
332    }
333
334    Rectangle {
335        id: topRightHandle
336        visible: !isRotated()
337        color: handleColor
338        width: handleSize
339        height: handleSize
340        MouseArea {
341            anchors.fill: parent
342            acceptedButtons: Qt.LeftButton
343            cursorShape: Qt.SizeBDiagCursor
344            drag.target: parent
345            onEntered: {
346                rectangle.anchors.top = parent.top
347                rectangle.anchors.right = parent.right
348                rectangle.anchors.bottom = bottomLeftHandle.bottom
349                rectangle.anchors.left = bottomLeftHandle.left
350                topLeftHandle.anchors.top = rectangle.top
351                bottomRightHandle.anchors.right = rectangle.right
352            }
353            onPositionChanged: {
354                topRightHandle.x = snapX(topRightHandle.x + handleSize) - handleSize
355                topRightHandle.y = snapY(topRightHandle.y)
356                if (aspectRatio !== 0.0)
357                    parent.x = topLeftHandle.x + rectangle.height * aspectRatio - handleSize
358                parent.x = Math.max(parent.x, bottomLeftHandle.x)
359                parent.y = Math.min(parent.y, bottomLeftHandle.y)
360                rectChanged(rectangle)
361            }
362            onReleased: {
363                rectChanged(rectangle)
364                topLeftHandle.anchors.top = undefined
365                bottomRightHandle.anchors.right = undefined
366            }
367        }
368    }
369
370    Rectangle {
371        id: bottomLeftHandle
372        visible: !isRotated()
373        color: handleColor
374        width: handleSize
375        height: handleSize
376        MouseArea {
377            anchors.fill: parent
378            acceptedButtons: Qt.LeftButton
379            cursorShape: Qt.SizeBDiagCursor
380            drag.target: parent
381            onEntered: {
382                rectangle.anchors.bottom = parent.bottom
383                rectangle.anchors.left = parent.left
384                rectangle.anchors.top = topRightHandle.top
385                rectangle.anchors.right = topRightHandle.right
386                topLeftHandle.anchors.left = rectangle.left
387                bottomRightHandle.anchors.bottom = rectangle.bottom
388            }
389            onPositionChanged: {
390                bottomLeftHandle.x = snapX(bottomLeftHandle.x)
391                bottomLeftHandle.y = snapY(bottomLeftHandle.y + handleSize) - handleSize
392                if (aspectRatio !== 0.0)
393                    parent.x = topRightHandle.x + handleSize - rectangle.height * aspectRatio
394                parent.x = Math.min(parent.x, topRightHandle.x)
395                parent.y = Math.max(parent.y, topRightHandle.y)
396                rectChanged(rectangle)
397            }
398            onReleased: {
399                rectChanged(rectangle)
400                topLeftHandle.anchors.left = undefined
401                bottomRightHandle.anchors.bottom = undefined
402            }
403        }
404    }
405
406    Rectangle {
407        id: bottomRightHandle
408        visible: !isRotated()
409        color: handleColor
410        width: handleSize
411        height: handleSize
412        MouseArea {
413            anchors.fill: parent
414            acceptedButtons: Qt.LeftButton
415            cursorShape: Qt.SizeFDiagCursor
416            drag.target: parent
417            onEntered: {
418                rectangle.anchors.bottom = parent.bottom
419                rectangle.anchors.right = parent.right
420                topRightHandle.anchors.right = rectangle.right
421                bottomLeftHandle.anchors.bottom = rectangle.bottom
422            }
423            onPositionChanged: {
424                bottomRightHandle.x = snapX(bottomRightHandle.x + handleSize) - handleSize
425                bottomRightHandle.y = snapY(bottomRightHandle.y + handleSize) - handleSize
426                if (aspectRatio !== 0.0)
427                    parent.x = topLeftHandle.x + rectangle.height * aspectRatio - handleSize
428                parent.x = Math.max(parent.x, topLeftHandle.x)
429                parent.y = Math.max(parent.y, topLeftHandle.y)
430                rectChanged(rectangle)
431            }
432            onReleased: {
433                rectChanged(rectangle)
434                topRightHandle.anchors.right = undefined
435                bottomLeftHandle.anchors.bottom = undefined
436            }
437        }
438    }
439}
440