~ci-train-bot/gallery-app/gallery-app-ubuntu-xenial-landing-002

« back to all changes in this revision

Viewing changes to rc/qml/MediaViewer/PhotoEditor/CropOverlay.qml

Remove local copy of PhotoEditor and keep using only the Ubuntu Extras one Fixes: #1524973

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
/*
2
 
 * Copyright (C) 2012-2015 Canonical Ltd
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 version 3 as
6
 
 * published by the Free Software Foundation.
7
 
 *
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.
12
 
 *
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/>.
15
 
 *
16
 
 * Authors:
17
 
 * Charles Lindsay <chaz@yorba.org>
18
 
 * Lucas Beeler <lucas@yorba.org>
19
 
 */
20
 
 
21
 
import QtQuick 2.4
22
 
import Ubuntu.Components 1.3
23
 
import "GraphicsRoutines.js" as GraphicsRoutines
24
 
 
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.
34
 
 */
35
 
Item {
36
 
    id: cropOverlay
37
 
 
38
 
    // public properties
39
 
    /*!
40
 
    */
41
 
    property Item viewport
42
 
    /*!
43
 
    */
44
 
    property Item photo
45
 
    /*!
46
 
    */
47
 
    property string matteColor: "red"
48
 
    /*!
49
 
    */
50
 
    property real matteOpacity: 0.85
51
 
    /*!
52
 
    */
53
 
    property int initialFrameX: -1
54
 
    /*!
55
 
    */
56
 
    property int initialFrameY: -1
57
 
    /*!
58
 
    */
59
 
    property int initialFrameWidth: -1
60
 
    /*!
61
 
    */
62
 
    property int initialFrameHeight: -1
63
 
 
64
 
    // private properties -- Frame Fit Animation parameters
65
 
    property real interpolationFactor: 1.0
66
 
    /*!
67
 
    */
68
 
    property variant startFrame
69
 
    /*!
70
 
    */
71
 
    property variant endFrame
72
 
    /*!
73
 
    */
74
 
    property variant startPhoto
75
 
    /*!
76
 
    */
77
 
    property real referencePhotoWidth: -1
78
 
    /*!
79
 
    */
80
 
    property real referencePhotoHeight: -1
81
 
    /*!
82
 
    */
83
 
    property real endPhotoX
84
 
    /*!
85
 
    */
86
 
    property real endPhotoY
87
 
    /*!
88
 
    */
89
 
    property real endPhotoWidth
90
 
    /*!
91
 
    */
92
 
    property real endPhotoHeight
93
 
 
94
 
    /*!
95
 
    */
96
 
    signal userAlteredFrame()
97
 
    /*!
98
 
    */
99
 
    signal runFrameFitAnimation()
100
 
    /*!
101
 
    */
102
 
    signal matteRegionPressed()
103
 
    /*!
104
 
    */
105
 
    signal cropButtonPressed()
106
 
 
107
 
    /*!
108
 
    */
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;
122
 
        }
123
 
    }
124
 
 
125
 
    /* Return the (x, y) position and the width and height of the viewport
126
 
    */
127
 
    function getViewportExtentRect() {
128
 
        return GraphicsRoutines.cloneRect(viewport);
129
 
    }
130
 
 
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
133
 
     * preview.
134
 
     */
135
 
    function getPhotoExtentRect() {
136
 
        return GraphicsRoutines.cloneRect(photoExtent);
137
 
    }
138
 
 
139
 
    /*!
140
 
    */
141
 
    function getRelativeFrameRect() {
142
 
        return GraphicsRoutines.getRelativeRect(frame.getExtentRect(),
143
 
                                                getPhotoExtentRect());
144
 
    }
145
 
 
146
 
    anchors.fill: parent
147
 
 
148
 
    Item {
149
 
        id: photoExtent
150
 
 
151
 
        property real panStartX
152
 
        property real panStartY
153
 
 
154
 
        function startPan() {
155
 
            panStartX = x;
156
 
            panStartY = y;
157
 
        }
158
 
 
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;
163
 
 
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);
168
 
        }
169
 
 
170
 
        function stopPan() {
171
 
        }
172
 
 
173
 
        x: initialFrameX
174
 
        y: initialFrameY
175
 
        width: initialFrameWidth
176
 
        height: initialFrameHeight
177
 
        z: 1
178
 
 
179
 
        onXChanged: {
180
 
            if (photo)
181
 
                photo.x = x;
182
 
        }
183
 
 
184
 
        onYChanged: {
185
 
            if (photo)
186
 
                photo.y = y;
187
 
        }
188
 
 
189
 
        onWidthChanged: {
190
 
            if (photo && referencePhotoWidth > 0)
191
 
                photo.scale = width / referencePhotoWidth;
192
 
        }
193
 
 
194
 
        onHeightChanged: {
195
 
            if (photo && referencePhotoHeight > 0)
196
 
                photo.scale = height / referencePhotoHeight;
197
 
        }
198
 
    }
199
 
 
200
 
    //
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
204
 
    // overlay.
205
 
    //
206
 
    Rectangle {
207
 
        id: leftMatte
208
 
 
209
 
        color: cropOverlay.matteColor
210
 
        opacity: cropOverlay.matteOpacity
211
 
 
212
 
        anchors.top: topMatte.bottom
213
 
        anchors.bottom: frame.bottom
214
 
        anchors.left: parent.left
215
 
        anchors.right: frame.left
216
 
 
217
 
        MouseArea {
218
 
            anchors.fill: parent;
219
 
 
220
 
            onPressed: cropOverlay.matteRegionPressed();
221
 
        }
222
 
    }
223
 
 
224
 
    Rectangle {
225
 
        id: topMatte
226
 
 
227
 
        color: cropOverlay.matteColor
228
 
        opacity: cropOverlay.matteOpacity
229
 
 
230
 
        anchors.top: parent.top
231
 
        anchors.bottom: frame.top
232
 
        anchors.left: parent.left
233
 
        anchors.right: parent.right
234
 
 
235
 
        MouseArea {
236
 
            anchors.fill: parent;
237
 
 
238
 
            onPressed: cropOverlay.matteRegionPressed();
239
 
        }
240
 
    }
241
 
 
242
 
    Rectangle {
243
 
        id: rightMatte
244
 
 
245
 
        color: cropOverlay.matteColor
246
 
        opacity: cropOverlay.matteOpacity
247
 
 
248
 
        anchors.top: topMatte.bottom
249
 
        anchors.bottom: bottomMatte.top
250
 
        anchors.left: frame.right
251
 
        anchors.right: parent.right
252
 
 
253
 
        MouseArea {
254
 
            anchors.fill: parent;
255
 
 
256
 
            onPressed: cropOverlay.matteRegionPressed();
257
 
        }
258
 
    }
259
 
 
260
 
    Rectangle {
261
 
        id: bottomMatte
262
 
 
263
 
        color: cropOverlay.matteColor
264
 
        opacity: cropOverlay.matteOpacity
265
 
 
266
 
        anchors.top: frame.bottom
267
 
        anchors.bottom: parent.bottom
268
 
        anchors.left: parent.left
269
 
        anchors.right: parent.right
270
 
 
271
 
        MouseArea {
272
 
            anchors.fill: parent;
273
 
 
274
 
            onPressed: cropOverlay.matteRegionPressed();
275
 
        }
276
 
    }
277
 
 
278
 
    //
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
281
 
    // applied.
282
 
    //
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.
291
 
    //
292
 
    Rectangle {
293
 
        id: frame
294
 
 
295
 
        signal resizedX(bool left, real dx)
296
 
        signal resizedY(bool top, real dy)
297
 
 
298
 
        property variant dragStartRect
299
 
 
300
 
        function getExtentRect() {
301
 
            var result = { };
302
 
 
303
 
            result.x = x;
304
 
            result.y = y;
305
 
            result.width = width;
306
 
            result.height = height;
307
 
 
308
 
            return result;
309
 
        }
310
 
 
311
 
        x: cropOverlay.initialFrameX
312
 
        y: cropOverlay.initialFrameY
313
 
        width: cropOverlay.initialFrameWidth
314
 
        height: cropOverlay.initialFrameHeight
315
 
 
316
 
        color: "transparent"
317
 
 
318
 
        border.width: units.gu(0.2)
319
 
        border.color: "#19B6EE"
320
 
 
321
 
        MouseArea {
322
 
            id: panArea
323
 
 
324
 
            property int dragStartX;
325
 
            property int dragStartY;
326
 
 
327
 
            anchors.fill: parent
328
 
            anchors.margins: 2
329
 
 
330
 
            onPressed: {
331
 
                dragStartX = mouse.x;
332
 
                dragStartY = mouse.y;
333
 
 
334
 
                photoExtent.startPan();
335
 
            }
336
 
 
337
 
            onReleased: {
338
 
                photoExtent.stopPan();
339
 
            }
340
 
 
341
 
            onPositionChanged: {
342
 
                photoExtent.updatePan(mouse.x - dragStartX, mouse.y - dragStartY);
343
 
            }
344
 
        }
345
 
 
346
 
        Button {
347
 
            objectName: "centerCropIcon"
348
 
            anchors.centerIn: parent
349
 
            text: i18n.tr("Crop")
350
 
            color: frame.border.color
351
 
            opacity: 0.9
352
 
            onClicked: cropOverlay.cropButtonPressed()
353
 
        }
354
 
 
355
 
        // Left drag bar.
356
 
        CropDragArea {
357
 
            x: -units.gu(2)
358
 
            width: units.gu(3)
359
 
            anchors.verticalCenter: parent.center
360
 
            height: parent.height - units.gu(2)
361
 
 
362
 
            onDragged: {
363
 
                frame.resizedX(true, dx);
364
 
                frame.updateOnAltered(false);
365
 
            }
366
 
 
367
 
            onDragStarted: frame.dragStartRect = frame.getExtentRect();
368
 
            onDragCompleted: frame.updateOnAltered(true);
369
 
        }
370
 
 
371
 
        // Top drag bar.
372
 
        CropDragArea {
373
 
            y: -units.gu(2)
374
 
            height: units.gu(3)
375
 
            anchors.horizontalCenter: parent.center
376
 
            width: parent.width - units.gu(2)
377
 
 
378
 
            onDragged: {
379
 
                frame.resizedY(true, dy);
380
 
                frame.updateOnAltered(false);
381
 
            }
382
 
 
383
 
            onDragStarted: frame.dragStartRect = frame.getExtentRect();
384
 
            onDragCompleted: frame.updateOnAltered(true);
385
 
        }
386
 
 
387
 
        // Right drag bar.
388
 
        CropDragArea {
389
 
            x: parent.width - units.gu(1)
390
 
            width: units.gu(3)
391
 
            anchors.verticalCenter: parent.center
392
 
            height: parent.height - units.gu(2)
393
 
 
394
 
            onDragged: {
395
 
                frame.resizedX(false, dx);
396
 
                frame.updateOnAltered(false);
397
 
            }
398
 
 
399
 
            onDragStarted: frame.dragStartRect = frame.getExtentRect();
400
 
            onDragCompleted: frame.updateOnAltered(true);
401
 
        }
402
 
 
403
 
        // Bottom drag bar.
404
 
        CropDragArea {
405
 
            y: parent.height - units.gu(1)
406
 
            height: units.gu(3)
407
 
            anchors.horizontalCenter: parent.center
408
 
            width: parent.width - units.gu(2)
409
 
 
410
 
            onDragged: {
411
 
                frame.resizedY(false, dy);
412
 
                frame.updateOnAltered(false);
413
 
            }
414
 
 
415
 
            onDragStarted: frame.dragStartRect = frame.getExtentRect();
416
 
            onDragCompleted: frame.updateOnAltered(true);
417
 
        }
418
 
 
419
 
        // Top-left corner.
420
 
        CropCorner {
421
 
            objectName: "topLeftCropCorner"
422
 
            isLeft: true
423
 
            isTop: true
424
 
 
425
 
            onDragged: {
426
 
                frame.resizedX(isLeft, dx);
427
 
                frame.resizedY(isTop, dy);
428
 
                frame.updateOnAltered(false);
429
 
            }
430
 
 
431
 
            onDragStarted: frame.dragStartRect = frame.getExtentRect();
432
 
            onDragCompleted: frame.updateOnAltered(true);
433
 
        }
434
 
 
435
 
        // Top-right corner.
436
 
        CropCorner {
437
 
            objectName: "topRightCropCorner"
438
 
            isLeft: false
439
 
            isTop: true
440
 
 
441
 
            onDragged: {
442
 
                frame.resizedX(isLeft, dx);
443
 
                frame.resizedY(isTop, dy);
444
 
                frame.updateOnAltered(false);
445
 
            }
446
 
 
447
 
            onDragStarted: frame.dragStartRect = frame.getExtentRect();
448
 
            onDragCompleted: frame.updateOnAltered(true);
449
 
        }
450
 
 
451
 
        // Bottom-left corner.
452
 
        CropCorner {
453
 
            objectName: "bottonLeftCropCorner"
454
 
            isLeft: true
455
 
            isTop: false
456
 
 
457
 
            onDragged: {
458
 
                frame.resizedX(isLeft, dx);
459
 
                frame.resizedY(isTop, dy);
460
 
                frame.updateOnAltered(false);
461
 
            }
462
 
 
463
 
            onDragStarted: frame.dragStartRect = frame.getExtentRect();
464
 
            onDragCompleted: frame.updateOnAltered(true);
465
 
        }
466
 
 
467
 
        // Bottom-right corner.
468
 
        CropCorner {
469
 
            id: bottomRightCrop
470
 
            objectName: "bottomRightCropCorner"
471
 
            isLeft: false
472
 
            isTop: false
473
 
 
474
 
            onDragged: {
475
 
                frame.resizedX(isLeft, dx);
476
 
                frame.resizedY(isTop, dy);
477
 
                frame.updateOnAltered(false);
478
 
            }
479
 
 
480
 
            onDragStarted: frame.dragStartRect = frame.getExtentRect();
481
 
            onDragCompleted: frame.updateOnAltered(true);
482
 
        }
483
 
 
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();
492
 
 
493
 
            if (first) {
494
 
                // Left/top side.
495
 
                if (frame[position] + delta < constraintRegion[position])
496
 
                    delta = constraintRegion[position] - frame[position]
497
 
 
498
 
                if (frame[span] - delta < minSize)
499
 
                    delta = frame[span] - minSize;
500
 
 
501
 
                frame[position] += delta;
502
 
                frame[span] -= delta;
503
 
            } else {
504
 
                // Right/bottom side.
505
 
                if (frame[span] + delta < minSize)
506
 
                    delta = minSize - frame[span];
507
 
 
508
 
                if ((frame[position] + frame[span] + delta) >
509
 
                        (constraintRegion[position] + constraintRegion[span]))
510
 
                    delta = constraintRegion[position] + constraintRegion[span] -
511
 
                            frame[position] - frame[span];
512
 
 
513
 
                frame[span] += delta;
514
 
            }
515
 
        }
516
 
 
517
 
        onResizedX: resizeFrame(left, dx, "x", "width")
518
 
        onResizedY: resizeFrame(top, dy, "y", "height")
519
 
 
520
 
        function updateOnAltered(finalUpdate) {
521
 
            var start = frame.dragStartRect;
522
 
            var end = frame.getExtentRect();
523
 
            if (!GraphicsRoutines.areEqual(end, start)) {
524
 
                if (finalUpdate ||
525
 
                    (end.width * end.height >= start.width * start.height)) {
526
 
                    cropOverlay.userAlteredFrame();
527
 
                    cropOverlay.runFrameFitAnimation();
528
 
                }
529
 
            }
530
 
        }
531
 
    }
532
 
 
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
538
 
     */
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.
544
 
 
545
 
        startFrame = GraphicsRoutines.cloneRect(frame);
546
 
 
547
 
        endFrame = GraphicsRoutines.fitRect(getViewportExtentRect(),
548
 
                                            frame.getExtentRect());
549
 
 
550
 
        startPhoto = GraphicsRoutines.cloneRect(photoExtent);
551
 
 
552
 
        var frameRelativeToPhoto = getRelativeFrameRect();
553
 
        var scaleFactor = endFrame.width / frame.width;
554
 
 
555
 
        endPhotoWidth = photoExtent.width * scaleFactor;
556
 
        endPhotoHeight = photoExtent.height * scaleFactor;
557
 
        endPhotoX = endFrame.x - (frameRelativeToPhoto.x * endPhotoWidth);
558
 
        endPhotoY = endFrame.y - (frameRelativeToPhoto.y * endPhotoHeight)
559
 
 
560
 
        photo.transformOrigin = Item.TopLeft;
561
 
    }
562
 
 
563
 
    onRunFrameFitAnimation: NumberAnimation { target: cropOverlay;
564
 
        property: "interpolationFactor"; from: 0.0; to: 1.0 }
565
 
 
566
 
    onInterpolationFactorChanged: {
567
 
        var endPhotoRect = { };
568
 
        endPhotoRect.x = endPhotoX;
569
 
        endPhotoRect.y = endPhotoY;
570
 
        endPhotoRect.width = endPhotoWidth;
571
 
        endPhotoRect.height = endPhotoHeight;
572
 
 
573
 
        var interpolatedRect = GraphicsRoutines.interpolateRect(startFrame,
574
 
                                                                endFrame, interpolationFactor);
575
 
        GraphicsRoutines.sizeToRect(interpolatedRect, frame);
576
 
 
577
 
        interpolatedRect = GraphicsRoutines.interpolateRect(startPhoto,
578
 
                                                            endPhotoRect, interpolationFactor);
579
 
        GraphicsRoutines.sizeToRect(interpolatedRect, photoExtent);
580
 
    }
581
 
}