~elopio/gallery-app/revert_workaround-1302706-click_toolbar_button_failure

« back to all changes in this revision

Viewing changes to rc/qml/MediaViewer/ZoomablePhotoComponent.qml

  • Committer: Leo Arias
  • Date: 2015-05-15 08:05:23 UTC
  • mfrom: (954.1.241 gallery-app)
  • Revision ID: leo.arias@canonical.com-20150515080523-i2of3vr8h7dioj59
Now the toolbar object is not needed at all.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
/*
2
 
 * Copyright (C) 2012 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.0
22
 
import "../../js/Gallery.js" as Gallery
23
 
import "../../js/GalleryUtility.js" as GalleryUtility
24
 
import "../../js/GraphicsRoutines.js" as GraphicsRoutines
25
 
import "../Components"
26
 
 
27
 
// PhotoComponent that allows you to zoom in on the photo.
28
 
Rectangle {
29
 
    id: zoomablePhotoComponent
30
 
 
31
 
    /*!
32
 
    */
33
 
    signal loaded()
34
 
    /*!
35
 
    */
36
 
    signal clicked()
37
 
    /*!
38
 
    */
39
 
    signal zoomed()
40
 
    /*!
41
 
    */
42
 
    signal unzoomed()
43
 
 
44
 
    /*!
45
 
    */
46
 
    property var mediaSource
47
 
    /*!
48
 
    */
49
 
    property bool load: false
50
 
    /*!
51
 
    */
52
 
    property bool isPreview
53
 
    /*!
54
 
    */
55
 
    property string ownerName
56
 
 
57
 
    // read-only
58
 
    /*!
59
 
    */
60
 
    property alias paintedWidth: unzoomedPhoto.paintedWidth
61
 
    /*!
62
 
    */
63
 
    property alias paintedHeight: unzoomedPhoto.paintedHeight
64
 
    /*!
65
 
    */
66
 
    property alias isLoaded: unzoomedPhoto.isLoaded
67
 
    /*!
68
 
    */
69
 
    property int zoomFocusX: 0 // Relative to zoomablePhotoComponent.
70
 
    /*!
71
 
    */
72
 
    property int zoomFocusY: 0
73
 
    /*!
74
 
    */
75
 
    property real zoomFactor: 1
76
 
    /*!
77
 
    */
78
 
    property bool fullyUnzoomed: (state === "unzoomed" && zoomFactor === 1)
79
 
    /*!
80
 
    */
81
 
    property bool fullyZoomed: (state === "full_zoom" && zoomFactor === maxZoomFactor)
82
 
 
83
 
    // Though little documented, Qt has a dedicated background thread, separate
84
 
    // from the main GUI thread, in which it renders on-screen objects (see
85
 
    // http://blog.qt.digia.com/blog/2012/08/20/render-thread-animations-in-qt-quick-2-0/
86
 
    // for a discussion of this topic). Unfortunately, animation ticks are timed
87
 
    // by the main GUI thread, but actual drawing in response to these ticks
88
 
    // is done in the separate rendering thread. Because of this, you can get
89
 
    // into a situation in which an animation reports that it has completed but
90
 
    // the separate rendering thread still has a frame to draw. In all of my
91
 
    // testing, I've never seen this timing mismatch exceed 1/30th of a second,
92
 
    // which makes sense because the QML animation clock ticks every 1/60th of a
93
 
    // second, according to the docs (see http://qt-project.org/doc/qt-5.0/qtqml/qml-qtquick2-timer.html),
94
 
    // though implementations appear to be free to drop to half this rate
95
 
    // (see https://bugreports.qt-project.org/browse/QTBUG-28487). So we define
96
 
    // an animation frame as 1/30th of a second and wait this long before doing
97
 
    // more drawing in response to the completion of an animation to prevent
98
 
    // stuttering.
99
 
    property int oneFrame: Math.ceil(1000 / 30);
100
 
 
101
 
    // internal
102
 
    /*!
103
 
    */
104
 
    property real maxZoomFactor: 2.5
105
 
    /*!
106
 
    */
107
 
    property real photoFocusX: zoomFocusX - unzoomedPhoto.leftEdge
108
 
    /*!
109
 
    */
110
 
    property real photoFocusY: zoomFocusY - unzoomedPhoto.topEdge
111
 
    /*!
112
 
    */
113
 
    property bool isZoomAnimationInProgress: false
114
 
 
115
 
    clip: true
116
 
 
117
 
    /*!
118
 
    */
119
 
    function zoom(x, y) {
120
 
        zoomFocusX = x;
121
 
        zoomFocusY = y;
122
 
        state = "full_zoom";
123
 
    }
124
 
 
125
 
    /*!
126
 
    */
127
 
    function unzoom() {
128
 
        state = "unzoomed";
129
 
    }
130
 
 
131
 
    /*!
132
 
    */
133
 
    function toggleZoom(x, y) {
134
 
        if (state === "unzoomed")
135
 
            zoom(x, y);
136
 
        else
137
 
            unzoom();
138
 
    }
139
 
 
140
 
    states: [
141
 
        State { name: "unzoomed";
142
 
            PropertyChanges { target: zoomablePhotoComponent; zoomFactor: 1; }
143
 
        },
144
 
        State { name: "full_zoom";
145
 
            PropertyChanges { target: zoomablePhotoComponent; zoomFactor: maxZoomFactor; }
146
 
        },
147
 
        State { name: "pinching";
148
 
            // Setting the zoom factor to itself seems odd, but it's necessary to
149
 
            // prevent zoomFactor from jumping when you start pinching.
150
 
            PropertyChanges { target: zoomablePhotoComponent; zoomFactor: zoomFactor;
151
 
                explicit: true; }
152
 
        }
153
 
    ]
154
 
 
155
 
    transitions: [
156
 
        // Double-click transitions.
157
 
        Transition { from: "full_zoom"; to: "unzoomed";
158
 
            SequentialAnimation {
159
 
                ScriptAction { script: isZoomAnimationInProgress = true; }
160
 
                NumberAnimation { properties: "zoomFactor"; easing.type: Easing.InQuad;
161
 
                    duration: Gallery.FAST_DURATION; }
162
 
                PauseAnimation { duration: oneFrame }
163
 
                ScriptAction { script: isZoomAnimationInProgress = false; }
164
 
            }
165
 
        },
166
 
 
167
 
        Transition { from: "unzoomed"; to: "full_zoom";
168
 
            SequentialAnimation {
169
 
                ScriptAction { script: isZoomAnimationInProgress = true; }
170
 
                NumberAnimation { properties: "zoomFactor"; easing.type: Easing.InQuad;
171
 
                    duration: Gallery.FAST_DURATION; }
172
 
                PauseAnimation { duration: oneFrame }
173
 
                ScriptAction { script: isZoomAnimationInProgress = false; }
174
 
            }
175
 
        },
176
 
 
177
 
        Transition { from: "pinching"; to: "unzoomed";
178
 
            SequentialAnimation {
179
 
                ScriptAction { script: isZoomAnimationInProgress = true; }
180
 
                NumberAnimation { properties: "zoomFactor"; easing.type: Easing.Linear;
181
 
                    duration: Gallery.SNAP_DURATION; }
182
 
                PauseAnimation { duration: oneFrame }
183
 
                ScriptAction { script: isZoomAnimationInProgress = false; }
184
 
            }
185
 
        },
186
 
 
187
 
        Transition { from: "pinching"; to: "full_zoom";
188
 
            SequentialAnimation {
189
 
                ScriptAction { script: isZoomAnimationInProgress = true; }
190
 
                NumberAnimation { properties: "zoomFactor"; easing.type: Easing.Linear;
191
 
                    duration: Gallery.SNAP_DURATION; }
192
 
                PauseAnimation { duration: oneFrame }
193
 
                ScriptAction { script: isZoomAnimationInProgress = false; }
194
 
            }
195
 
        }
196
 
    ]
197
 
 
198
 
    state: "unzoomed"
199
 
 
200
 
    onStateChanged: {
201
 
        if (state === "full_zoom")
202
 
            zoomed();
203
 
        else if (state === "unzoomed")
204
 
            unzoomed();
205
 
    }
206
 
 
207
 
    GalleryPhotoComponent {
208
 
        id: unzoomedPhoto
209
 
 
210
 
        property real leftEdge: (parent.width - paintedWidth) / 2
211
 
        property real topEdge: (parent.height - paintedHeight) / 2
212
 
 
213
 
        function isInsidePhoto(x, y) {
214
 
            return (x >= leftEdge && x < leftEdge + paintedWidth &&
215
 
                    y >= topEdge && y < topEdge + paintedHeight);
216
 
        }
217
 
 
218
 
        anchors.fill: parent
219
 
        visible: fullyUnzoomed
220
 
        color: zoomablePhotoComponent.color
221
 
 
222
 
        mediaSource: zoomablePhotoComponent.mediaSource
223
 
        load: zoomablePhotoComponent.load && zoomablePhotoComponent.fullyUnzoomed
224
 
        isPreview: zoomablePhotoComponent.isPreview
225
 
        ownerName: zoomablePhotoComponent.ownerName + "unzoomedPhoto"
226
 
    }
227
 
 
228
 
    PinchArea {
229
 
        id: pinchArea
230
 
 
231
 
        property bool zoomingIn // Splaying to zoom in, vs. pinching to zoom out.
232
 
        property real initialZoomFactor
233
 
 
234
 
        anchors.fill: parent
235
 
 
236
 
        // QML seems to ignore these, so we have to manually keep scale in check
237
 
        // inside onPinchUpdated.  The 0.9 and 1.1 are just fudge factors to give
238
 
        // us a little bounce when you go past the zoom limit.
239
 
        pinch.minimumScale: 1 / initialZoomFactor * 0.9
240
 
        pinch.maximumScale: maxZoomFactor / initialZoomFactor * 1.1
241
 
 
242
 
        onPinchStarted: {
243
 
            zoomingIn = false;
244
 
            initialZoomFactor = zoomFactor;
245
 
 
246
 
            if (fullyUnzoomed) {
247
 
                if (unzoomedPhoto.isInsidePhoto(pinch.center.x, pinch.center.y)) {
248
 
                    zoomFocusX = pinch.center.x;
249
 
                    zoomFocusY = pinch.center.y;
250
 
                } else {
251
 
                    zoomFocusX = parent.width / 2;
252
 
                    zoomFocusY = parent.height / 2;
253
 
                }
254
 
            }
255
 
 
256
 
            zoomablePhotoComponent.state = "pinching";
257
 
        }
258
 
 
259
 
        onPinchUpdated: {
260
 
            // Determine if we're still zooming in or out.  Allow for a small
261
 
            // variance to account for touch noise.
262
 
            if (Math.abs(pinch.scale - pinch.previousScale) > 0.001)
263
 
                zoomingIn = (pinch.scale > pinch.previousScale);
264
 
 
265
 
            // For some reason, the PinchArea ignores these settings.
266
 
            var scale = GraphicsRoutines.clamp(pinch.scale,
267
 
                                               pinchArea.pinch.minimumScale, pinchArea.pinch.maximumScale);
268
 
 
269
 
            zoomFactor = initialZoomFactor * scale;
270
 
        }
271
 
 
272
 
        onPinchFinished: zoomablePhotoComponent.state = (zoomingIn ? "full_zoom" : "unzoomed")
273
 
 
274
 
        MouseAreaWithMultipoint {
275
 
            desktop: APP.desktopMode
276
 
            anchors.fill: parent
277
 
            enabled: fullyUnzoomed
278
 
 
279
 
            onClicked: zoomablePhotoComponent.clicked()
280
 
            onDoubleClicked: {
281
 
                if (unzoomedPhoto.isInsidePhoto(mouse.x, mouse.y))
282
 
                    zoom(mouse.x, mouse.y);
283
 
                else
284
 
                    zoomablePhotoComponent.clicked();
285
 
            }
286
 
        }
287
 
    }
288
 
 
289
 
    Loader {
290
 
        id: zoomAssemblyLoader
291
 
 
292
 
        anchors.fill: parent
293
 
 
294
 
        sourceComponent: (fullyUnzoomed ? undefined : zoomAssemblyComponent)
295
 
 
296
 
        Component {
297
 
            id: zoomAssemblyComponent
298
 
 
299
 
            Item {
300
 
                anchors.fill: parent
301
 
 
302
 
                Flickable {
303
 
                    id: zoomArea
304
 
 
305
 
                    property real zoomAreaZoomFactor: maxZoomFactor
306
 
                    property real minContentFocusX: (contentWidth < parent.width
307
 
                                                     ? contentWidth : parent.width) / 2
308
 
                    property real maxContentFocusX: contentWidth - minContentFocusX
309
 
                    property real minContentFocusY: (contentHeight < parent.height
310
 
                                                     ? contentHeight : parent.height) / 2
311
 
                    property real maxContentFocusY: contentHeight - minContentFocusY
312
 
                    property real contentFocusX: GraphicsRoutines.clamp(
313
 
                                                     photoFocusX * zoomAreaZoomFactor,
314
 
                                                     minContentFocusX, maxContentFocusX)
315
 
                    property real contentFocusY: GraphicsRoutines.clamp(
316
 
                                                     photoFocusY * zoomAreaZoomFactor,
317
 
                                                     minContentFocusY, maxContentFocusY)
318
 
                    // Translate between focus point and top/left point.  Note: you might think
319
 
                    // that this should take into account the left and top margins, but
320
 
                    // apparently not.
321
 
                    property real contentFocusLeft: contentFocusX - parent.width / 2
322
 
                    property real contentFocusTop: contentFocusY - parent.height / 2
323
 
 
324
 
                    anchors.fill: parent
325
 
                    visible: fullyZoomed && !isZoomAnimationInProgress
326
 
 
327
 
                    onVisibleChanged: {
328
 
                        if (visible) {
329
 
                            contentX = contentFocusLeft;
330
 
                            contentY = contentFocusTop;
331
 
                        }
332
 
                    }
333
 
 
334
 
                    onContentXChanged: {
335
 
                        var contentFocusX = contentX + width / 2;
336
 
                        var photoFocusX = contentFocusX / zoomAreaZoomFactor;
337
 
                        zoomFocusX = photoFocusX + unzoomedPhoto.leftEdge;
338
 
                    }
339
 
 
340
 
                    onContentYChanged: {
341
 
                        var contentFocusY = contentY + height / 2;
342
 
                        var photoFocusY = contentFocusY / zoomAreaZoomFactor;
343
 
                        zoomFocusY = photoFocusY + unzoomedPhoto.topEdge;
344
 
                    }
345
 
 
346
 
                    flickableDirection: Flickable.HorizontalAndVerticalFlick
347
 
                    contentWidth: unzoomedPhoto.paintedWidth * zoomAreaZoomFactor
348
 
                    contentHeight: unzoomedPhoto.paintedHeight * zoomAreaZoomFactor
349
 
 
350
 
                    leftMargin: Math.max(0, (parent.width - contentWidth) / 2)
351
 
                    rightMargin: leftMargin
352
 
                    topMargin: Math.max(0, (parent.height - contentHeight) / 2)
353
 
                    bottomMargin: topMargin
354
 
 
355
 
                    GalleryPhotoComponent {
356
 
                        id: zoomedPhoto
357
 
 
358
 
                        anchors.fill: parent
359
 
                        color: zoomablePhotoComponent.color
360
 
 
361
 
                        mediaSource: zoomablePhotoComponent.mediaSource
362
 
                        load: zoomablePhotoComponent.load && fullyZoomed
363
 
 
364
 
                        isPreview: zoomablePhotoComponent.isPreview
365
 
                        ownerName: zoomablePhotoComponent.ownerName + "zoomedPhoto"
366
 
 
367
 
                        MouseAreaWithMultipoint {
368
 
                            desktop: APP.desktopMode
369
 
                            anchors.fill: parent
370
 
 
371
 
                            onClicked: zoomablePhotoComponent.clicked()
372
 
                            onDoubleClicked: unzoom()
373
 
                        }
374
 
                    }
375
 
                }
376
 
 
377
 
                GalleryPhotoComponent {
378
 
                    id: transitionPhoto
379
 
 
380
 
                    property real unzoomedX: unzoomedPhoto.leftEdge
381
 
                    property real unzoomedY: unzoomedPhoto.topEdge
382
 
                    property real zoomedX: -zoomArea.contentFocusLeft
383
 
                    property real zoomedY: -zoomArea.contentFocusTop
384
 
 
385
 
                    property real zoomFraction: (zoomFactor - 1) / (maxZoomFactor - 1)
386
 
 
387
 
                    x: GalleryUtility.interpolate(unzoomedX, zoomedX, zoomFraction)
388
 
                    y: GalleryUtility.interpolate(unzoomedY, zoomedY, zoomFraction)
389
 
                    width: unzoomedPhoto.paintedWidth
390
 
                    height: unzoomedPhoto.paintedHeight
391
 
                    scale: zoomFactor
392
 
                    transformOrigin: Item.TopLeft
393
 
 
394
 
                    visible: zoomablePhotoComponent.isZoomAnimationInProgress ||
395
 
                             zoomablePhotoComponent.state == "pinching" ||
396
 
                             !zoomedPhoto.isLoaded
397
 
 
398
 
                    color: zoomablePhotoComponent.color
399
 
 
400
 
                    mediaSource: zoomablePhotoComponent.mediaSource
401
 
                    load: zoomablePhotoComponent.load
402
 
                    isPreview: zoomablePhotoComponent.isPreview
403
 
                    isAnimate: true
404
 
                    ownerName: zoomablePhotoComponent.ownerName + "transitionPhoto"
405
 
                }
406
 
            }
407
 
        }
408
 
    }
409
 
}