2
* Copyright (C) 2012-2015 Canonical Ltd
4
* This program is free software: you can redistribute it and/or modify
5
* it under the terms of the GNU General Public License version 3 as
6
* published by the Free Software Foundation.
8
* This program is distributed in the hope that it will be useful,
9
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
* GNU General Public License for more details.
13
* You should have received a copy of the GNU General Public License
14
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17
* Charles Lindsay <chaz@yorba.org>
18
* Lucas Beeler <lucas@yorba.org>
22
import Ubuntu.Components 1.3
23
import "GraphicsRoutines.js" as GraphicsRoutines
25
/* A CropOverlay is a semi-transparent surface that floats over the photo. It
26
* serves two purposes. First, it provides visual cueing as to what region of
27
* the photo's surface will be preserved when the crop operation is applied.
28
* The preserved region is the region that falls inside of the CropOverlay's
29
* frame. Second, the CropOverlay allows the user to manipulate the
30
* geometry of the crop frame, to chage its location, width, and height. The
31
* geometry of the crop frame is reinforced by a key visual cue: the region of
32
* the photo outside of the crop frame is drawn with a semi-transparent, smoked
33
* matte on top of it. This matte surrounds the crop frame.
41
property Item viewport
47
property string matteColor: "red"
50
property real matteOpacity: 0.85
53
property int initialFrameX: -1
56
property int initialFrameY: -1
59
property int initialFrameWidth: -1
62
property int initialFrameHeight: -1
64
// private properties -- Frame Fit Animation parameters
65
property real interpolationFactor: 1.0
68
property variant startFrame
71
property variant endFrame
74
property variant startPhoto
77
property real referencePhotoWidth: -1
80
property real referencePhotoHeight: -1
83
property real endPhotoX
86
property real endPhotoY
89
property real endPhotoWidth
92
property real endPhotoHeight
96
signal userAlteredFrame()
99
signal runFrameFitAnimation()
102
signal matteRegionPressed()
105
signal cropButtonPressed()
109
function resetFor(rectSet) {
110
if (initialFrameX != -1 && initialFrameY != -1 && initialFrameWidth != -1 &&
111
initialFrameHeight != -1) {
112
frame.x = rectSet.cropFrame.x;
113
frame.y = rectSet.cropFrame.y;
114
frame.width = rectSet.cropFrame.width;
115
frame.height = rectSet.cropFrame.height;
116
photoExtent.x = rectSet.photoExtent.x;
117
photoExtent.y = rectSet.photoExtent.y;
118
photoExtent.width = rectSet.photoExtent.width;
119
photoExtent.height = rectSet.photoExtent.height;
120
referencePhotoWidth = rectSet.photoPreview.width;
121
referencePhotoHeight = rectSet.photoPreview.height;
125
/* Return the (x, y) position and the width and height of the viewport
127
function getViewportExtentRect() {
128
return GraphicsRoutines.cloneRect(viewport);
131
/* Return the (x, y) position and the width and height of the photoExtent.
132
* The photoExtent is the on-screen region that holds the original photo
135
function getPhotoExtentRect() {
136
return GraphicsRoutines.cloneRect(photoExtent);
141
function getRelativeFrameRect() {
142
return GraphicsRoutines.getRelativeRect(frame.getExtentRect(),
143
getPhotoExtentRect());
151
property real panStartX
152
property real panStartY
154
function startPan() {
159
// 'deltaX' and 'deltaY' are offsets relative to the pan start point
160
function updatePan(deltaX, deltaY) {
161
var newX = panStartX + deltaX;
162
var newY = panStartY + deltaY;
164
x = GraphicsRoutines.clamp(newX, frame.x + frame.width -
165
photoExtent.width, frame.x);
166
y = GraphicsRoutines.clamp(newY, frame.y + frame.height -
167
photoExtent.height, frame.y);
175
width: initialFrameWidth
176
height: initialFrameHeight
190
if (photo && referencePhotoWidth > 0)
191
photo.scale = width / referencePhotoWidth;
195
if (photo && referencePhotoHeight > 0)
196
photo.scale = height / referencePhotoHeight;
201
// The following four Rectangles are used to "matte out" the area of the photo
202
// preview that falls outside the frame. This "matting out" visual cue is
203
// accomplished by darkening the matted-out area with a translucent, smoked
209
color: cropOverlay.matteColor
210
opacity: cropOverlay.matteOpacity
212
anchors.top: topMatte.bottom
213
anchors.bottom: frame.bottom
214
anchors.left: parent.left
215
anchors.right: frame.left
218
anchors.fill: parent;
220
onPressed: cropOverlay.matteRegionPressed();
227
color: cropOverlay.matteColor
228
opacity: cropOverlay.matteOpacity
230
anchors.top: parent.top
231
anchors.bottom: frame.top
232
anchors.left: parent.left
233
anchors.right: parent.right
236
anchors.fill: parent;
238
onPressed: cropOverlay.matteRegionPressed();
245
color: cropOverlay.matteColor
246
opacity: cropOverlay.matteOpacity
248
anchors.top: topMatte.bottom
249
anchors.bottom: bottomMatte.top
250
anchors.left: frame.right
251
anchors.right: parent.right
254
anchors.fill: parent;
256
onPressed: cropOverlay.matteRegionPressed();
263
color: cropOverlay.matteColor
264
opacity: cropOverlay.matteOpacity
266
anchors.top: frame.bottom
267
anchors.bottom: parent.bottom
268
anchors.left: parent.left
269
anchors.right: parent.right
272
anchors.fill: parent;
274
onPressed: cropOverlay.matteRegionPressed();
279
// The frame is a grey rectangle with associated drag corners that
280
// frames the region of the photo that will remain when the crop operation is
283
// NB: the frame can be in two states, although the QML state mechanism
284
// isn't sufficiently expressive to describe them. The frame can be
285
// in the FIT state, in which case it is optimally fit inside the
286
// frame constraint region (see getFrameConstraintRect( ) above for
287
// a description of the frame constraint region). Or, the frame can
288
// be in the USER state. In the user state, the user has the mouse button
289
// held down and is actively performing a drag operation to change the
290
// geometry of the frame.
295
signal resizedX(bool left, real dx)
296
signal resizedY(bool top, real dy)
298
property variant dragStartRect
300
function getExtentRect() {
305
result.width = width;
306
result.height = height;
311
x: cropOverlay.initialFrameX
312
y: cropOverlay.initialFrameY
313
width: cropOverlay.initialFrameWidth
314
height: cropOverlay.initialFrameHeight
318
border.width: units.gu(0.2)
319
border.color: "#19B6EE"
324
property int dragStartX;
325
property int dragStartY;
331
dragStartX = mouse.x;
332
dragStartY = mouse.y;
334
photoExtent.startPan();
338
photoExtent.stopPan();
342
photoExtent.updatePan(mouse.x - dragStartX, mouse.y - dragStartY);
347
objectName: "centerCropIcon"
348
anchors.centerIn: parent
349
text: i18n.tr("Crop")
350
color: frame.border.color
352
onClicked: cropOverlay.cropButtonPressed()
359
anchors.verticalCenter: parent.center
360
height: parent.height - units.gu(2)
363
frame.resizedX(true, dx);
364
frame.updateOnAltered(false);
367
onDragStarted: frame.dragStartRect = frame.getExtentRect();
368
onDragCompleted: frame.updateOnAltered(true);
375
anchors.horizontalCenter: parent.center
376
width: parent.width - units.gu(2)
379
frame.resizedY(true, dy);
380
frame.updateOnAltered(false);
383
onDragStarted: frame.dragStartRect = frame.getExtentRect();
384
onDragCompleted: frame.updateOnAltered(true);
389
x: parent.width - units.gu(1)
391
anchors.verticalCenter: parent.center
392
height: parent.height - units.gu(2)
395
frame.resizedX(false, dx);
396
frame.updateOnAltered(false);
399
onDragStarted: frame.dragStartRect = frame.getExtentRect();
400
onDragCompleted: frame.updateOnAltered(true);
405
y: parent.height - units.gu(1)
407
anchors.horizontalCenter: parent.center
408
width: parent.width - units.gu(2)
411
frame.resizedY(false, dy);
412
frame.updateOnAltered(false);
415
onDragStarted: frame.dragStartRect = frame.getExtentRect();
416
onDragCompleted: frame.updateOnAltered(true);
421
objectName: "topLeftCropCorner"
426
frame.resizedX(isLeft, dx);
427
frame.resizedY(isTop, dy);
428
frame.updateOnAltered(false);
431
onDragStarted: frame.dragStartRect = frame.getExtentRect();
432
onDragCompleted: frame.updateOnAltered(true);
437
objectName: "topRightCropCorner"
442
frame.resizedX(isLeft, dx);
443
frame.resizedY(isTop, dy);
444
frame.updateOnAltered(false);
447
onDragStarted: frame.dragStartRect = frame.getExtentRect();
448
onDragCompleted: frame.updateOnAltered(true);
451
// Bottom-left corner.
453
objectName: "bottonLeftCropCorner"
458
frame.resizedX(isLeft, dx);
459
frame.resizedY(isTop, dy);
460
frame.updateOnAltered(false);
463
onDragStarted: frame.dragStartRect = frame.getExtentRect();
464
onDragCompleted: frame.updateOnAltered(true);
467
// Bottom-right corner.
470
objectName: "bottomRightCropCorner"
475
frame.resizedX(isLeft, dx);
476
frame.resizedY(isTop, dy);
477
frame.updateOnAltered(false);
480
onDragStarted: frame.dragStartRect = frame.getExtentRect();
481
onDragCompleted: frame.updateOnAltered(true);
484
// This handles resizing in both dimensions. first is whether we're
485
// resizing the "first" edge, e.g. left or top (in which case we
486
// adjust both position and span) vs. right or bottom (where we just
487
// adjust the span). position should be either "x" or "y", and span
488
// is either "width" or "height". This is a little complicated, and
489
// coule probably be optimized with a little more thought.
490
function resizeFrame(first, delta, position, span) {
491
var constraintRegion = cropOverlay.getPhotoExtentRect();
495
if (frame[position] + delta < constraintRegion[position])
496
delta = constraintRegion[position] - frame[position]
498
if (frame[span] - delta < minSize)
499
delta = frame[span] - minSize;
501
frame[position] += delta;
502
frame[span] -= delta;
504
// Right/bottom side.
505
if (frame[span] + delta < minSize)
506
delta = minSize - frame[span];
508
if ((frame[position] + frame[span] + delta) >
509
(constraintRegion[position] + constraintRegion[span]))
510
delta = constraintRegion[position] + constraintRegion[span] -
511
frame[position] - frame[span];
513
frame[span] += delta;
517
onResizedX: resizeFrame(left, dx, "x", "width")
518
onResizedY: resizeFrame(top, dy, "y", "height")
520
function updateOnAltered(finalUpdate) {
521
var start = frame.dragStartRect;
522
var end = frame.getExtentRect();
523
if (!GraphicsRoutines.areEqual(end, start)) {
525
(end.width * end.height >= start.width * start.height)) {
526
cropOverlay.userAlteredFrame();
527
cropOverlay.runFrameFitAnimation();
533
/* Invoked when the user has changed the geometry of the frame by dragging
534
* one of its corners or edges. Expressed in terms of the states of the
535
* frame described above, the userAlteredFrame signal is fired
536
* when the user stops dragging. This triggers a change of the frame
537
* from the USER state to the FIT state
539
onUserAlteredFrame: {
540
// since the geometry of the frame in the FIT state depends on both
541
// how the user resized the frame when it was in the USER state as well
542
// as the size of the frame constraint region, we have to recompute the
543
// geometry of of the frame for the FIT state every time.
545
startFrame = GraphicsRoutines.cloneRect(frame);
547
endFrame = GraphicsRoutines.fitRect(getViewportExtentRect(),
548
frame.getExtentRect());
550
startPhoto = GraphicsRoutines.cloneRect(photoExtent);
552
var frameRelativeToPhoto = getRelativeFrameRect();
553
var scaleFactor = endFrame.width / frame.width;
555
endPhotoWidth = photoExtent.width * scaleFactor;
556
endPhotoHeight = photoExtent.height * scaleFactor;
557
endPhotoX = endFrame.x - (frameRelativeToPhoto.x * endPhotoWidth);
558
endPhotoY = endFrame.y - (frameRelativeToPhoto.y * endPhotoHeight)
560
photo.transformOrigin = Item.TopLeft;
563
onRunFrameFitAnimation: NumberAnimation { target: cropOverlay;
564
property: "interpolationFactor"; from: 0.0; to: 1.0 }
566
onInterpolationFactorChanged: {
567
var endPhotoRect = { };
568
endPhotoRect.x = endPhotoX;
569
endPhotoRect.y = endPhotoY;
570
endPhotoRect.width = endPhotoWidth;
571
endPhotoRect.height = endPhotoHeight;
573
var interpolatedRect = GraphicsRoutines.interpolateRect(startFrame,
574
endFrame, interpolationFactor);
575
GraphicsRoutines.sizeToRect(interpolatedRect, frame);
577
interpolatedRect = GraphicsRoutines.interpolateRect(startPhoto,
578
endPhotoRect, interpolationFactor);
579
GraphicsRoutines.sizeToRect(interpolatedRect, photoExtent);