2
* Copyright (C) 2012 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 "../../js/Gallery.js" as Gallery
23
import "../../js/GalleryUtility.js" as GalleryUtility
24
import "../../js/GraphicsRoutines.js" as GraphicsRoutines
25
import "../Components"
27
// PhotoComponent that allows you to zoom in on the photo.
29
id: zoomablePhotoComponent
46
property var mediaSource
49
property bool load: false
52
property bool isPreview
55
property string ownerName
60
property alias paintedWidth: unzoomedPhoto.paintedWidth
63
property alias paintedHeight: unzoomedPhoto.paintedHeight
66
property alias isLoaded: unzoomedPhoto.isLoaded
69
property int zoomFocusX: 0 // Relative to zoomablePhotoComponent.
72
property int zoomFocusY: 0
75
property real zoomFactor: 1
78
property bool fullyUnzoomed: (state === "unzoomed" && zoomFactor === 1)
81
property bool fullyZoomed: (state === "full_zoom" && zoomFactor === maxZoomFactor)
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
99
property int oneFrame: Math.ceil(1000 / 30);
104
property real maxZoomFactor: 2.5
107
property real photoFocusX: zoomFocusX - unzoomedPhoto.leftEdge
110
property real photoFocusY: zoomFocusY - unzoomedPhoto.topEdge
113
property bool isZoomAnimationInProgress: false
119
function zoom(x, y) {
133
function toggleZoom(x, y) {
134
if (state === "unzoomed")
141
State { name: "unzoomed";
142
PropertyChanges { target: zoomablePhotoComponent; zoomFactor: 1; }
144
State { name: "full_zoom";
145
PropertyChanges { target: zoomablePhotoComponent; zoomFactor: maxZoomFactor; }
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;
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; }
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; }
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; }
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; }
201
if (state === "full_zoom")
203
else if (state === "unzoomed")
207
GalleryPhotoComponent {
210
property real leftEdge: (parent.width - paintedWidth) / 2
211
property real topEdge: (parent.height - paintedHeight) / 2
213
function isInsidePhoto(x, y) {
214
return (x >= leftEdge && x < leftEdge + paintedWidth &&
215
y >= topEdge && y < topEdge + paintedHeight);
219
visible: fullyUnzoomed
220
color: zoomablePhotoComponent.color
222
mediaSource: zoomablePhotoComponent.mediaSource
223
load: zoomablePhotoComponent.load && zoomablePhotoComponent.fullyUnzoomed
224
isPreview: zoomablePhotoComponent.isPreview
225
ownerName: zoomablePhotoComponent.ownerName + "unzoomedPhoto"
231
property bool zoomingIn // Splaying to zoom in, vs. pinching to zoom out.
232
property real initialZoomFactor
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
244
initialZoomFactor = zoomFactor;
247
if (unzoomedPhoto.isInsidePhoto(pinch.center.x, pinch.center.y)) {
248
zoomFocusX = pinch.center.x;
249
zoomFocusY = pinch.center.y;
251
zoomFocusX = parent.width / 2;
252
zoomFocusY = parent.height / 2;
256
zoomablePhotoComponent.state = "pinching";
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);
265
// For some reason, the PinchArea ignores these settings.
266
var scale = GraphicsRoutines.clamp(pinch.scale,
267
pinchArea.pinch.minimumScale, pinchArea.pinch.maximumScale);
269
zoomFactor = initialZoomFactor * scale;
272
onPinchFinished: zoomablePhotoComponent.state = (zoomingIn ? "full_zoom" : "unzoomed")
274
MouseAreaWithMultipoint {
275
desktop: APP.desktopMode
277
enabled: fullyUnzoomed
279
onClicked: zoomablePhotoComponent.clicked()
281
if (unzoomedPhoto.isInsidePhoto(mouse.x, mouse.y))
282
zoom(mouse.x, mouse.y);
284
zoomablePhotoComponent.clicked();
290
id: zoomAssemblyLoader
294
sourceComponent: (fullyUnzoomed ? undefined : zoomAssemblyComponent)
297
id: zoomAssemblyComponent
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
321
property real contentFocusLeft: contentFocusX - parent.width / 2
322
property real contentFocusTop: contentFocusY - parent.height / 2
325
visible: fullyZoomed && !isZoomAnimationInProgress
329
contentX = contentFocusLeft;
330
contentY = contentFocusTop;
335
var contentFocusX = contentX + width / 2;
336
var photoFocusX = contentFocusX / zoomAreaZoomFactor;
337
zoomFocusX = photoFocusX + unzoomedPhoto.leftEdge;
341
var contentFocusY = contentY + height / 2;
342
var photoFocusY = contentFocusY / zoomAreaZoomFactor;
343
zoomFocusY = photoFocusY + unzoomedPhoto.topEdge;
346
flickableDirection: Flickable.HorizontalAndVerticalFlick
347
contentWidth: unzoomedPhoto.paintedWidth * zoomAreaZoomFactor
348
contentHeight: unzoomedPhoto.paintedHeight * zoomAreaZoomFactor
350
leftMargin: Math.max(0, (parent.width - contentWidth) / 2)
351
rightMargin: leftMargin
352
topMargin: Math.max(0, (parent.height - contentHeight) / 2)
353
bottomMargin: topMargin
355
GalleryPhotoComponent {
359
color: zoomablePhotoComponent.color
361
mediaSource: zoomablePhotoComponent.mediaSource
362
load: zoomablePhotoComponent.load && fullyZoomed
364
isPreview: zoomablePhotoComponent.isPreview
365
ownerName: zoomablePhotoComponent.ownerName + "zoomedPhoto"
367
MouseAreaWithMultipoint {
368
desktop: APP.desktopMode
371
onClicked: zoomablePhotoComponent.clicked()
372
onDoubleClicked: unzoom()
377
GalleryPhotoComponent {
380
property real unzoomedX: unzoomedPhoto.leftEdge
381
property real unzoomedY: unzoomedPhoto.topEdge
382
property real zoomedX: -zoomArea.contentFocusLeft
383
property real zoomedY: -zoomArea.contentFocusTop
385
property real zoomFraction: (zoomFactor - 1) / (maxZoomFactor - 1)
387
x: GalleryUtility.interpolate(unzoomedX, zoomedX, zoomFraction)
388
y: GalleryUtility.interpolate(unzoomedY, zoomedY, zoomFraction)
389
width: unzoomedPhoto.paintedWidth
390
height: unzoomedPhoto.paintedHeight
392
transformOrigin: Item.TopLeft
394
visible: zoomablePhotoComponent.isZoomAnimationInProgress ||
395
zoomablePhotoComponent.state == "pinching" ||
396
!zoomedPhoto.isLoaded
398
color: zoomablePhotoComponent.color
400
mediaSource: zoomablePhotoComponent.mediaSource
401
load: zoomablePhotoComponent.load
402
isPreview: zoomablePhotoComponent.isPreview
404
ownerName: zoomablePhotoComponent.ownerName + "transitionPhoto"