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