MauiKit Controls

AnimatedImageViewer.qml
1/*
2 * SPDX-FileCopyrightText: (C) 2015 Vishesh Handa <vhanda@kde.org>
3 * SPDX-FileCopyrightText: (C) 2017 Atul Sharma <atulsharma406@gmail.com>
4 * SPDX-FileCopyrightText: (C) 2017 Marco Martin <mart@kde.org>
5 *
6 * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
7 */
8
9import QtQuick
10import QtQuick.Controls
11import org.mauikit.controls as Maui
12
13/**
14 * @brief A view for displaying animated images, such as GIF documents.
15 *
16 * <a href="https://doc.qt.io/qt-6/qml-qtquick-controls-flickable.html">This controls inherits from QQC2 Flickable, to checkout its inherited properties refer to the Qt Docs.</a>
17 *
18 * This control along with the ImageViewer are meant to display images and support to zoom in and out with touch and mouse gestures and keyboard shortcuts.
19 *
20 */
21Flickable
22{
23 id: flick
24
25 contentWidth: width
26 contentHeight: height
27 boundsBehavior: Flickable.StopAtBounds
28 boundsMovement: Flickable.StopAtBounds
29 interactive: contentWidth > width || contentHeight > height
30 clip: true
32 ScrollBar.vertical: ScrollBar {
33 visible: false
34 }
35 ScrollBar.horizontal: ScrollBar {
36 visible: false
37 }
38
39 readonly property bool zooming: contentHeight != height || contentWidth != width
40
41 /**
42 * @brief This an alias to the actual control painting the image.
43 * This control is handled by a QQC2 AnimatedImage.
44 * @property AnimatedImage AnimatedImageViewer::image
45 */
46 property alias image: image
47
48 /**
49 * @brief The painted size of the image.
50 * As taken from Qt documentation: This property holds the scaled width and height of the full-frame image.
51 * Unlike the width and height properties, which scale the painting of the image, this property sets the maximum number of pixels stored for the loaded image so that large images do not use more memory than necessary.
52 * @property size AnimatedImageViewer::sourceSize
53 */
54 property alias sourceSize : image.sourceSize
55
56 /**
57 * @brief The fill mode of the image. The possible values can be found on the Image control documentation from Qt.
58 * By default this is set to `Image.PreserveAspectFit`.
59 * @property enumaration AnimatedImageViewer::fillMode
60 */
61 property alias fillMode: image.fillMode
63 /**
64 * @brief Whether the image should be loaded asynchronously.
65 * By default this is set to `true`.
66 * @property bool AnimatedImageViewer::asynchronous
67 */
68 property alias asynchronous : image.asynchronous
69
70 /**
71 * @brief If the image should be cached in memory.
72 * The default value is set to `true`
73 * @property bool AnimatedImageViewer::cache
74 */
75 property alias cache: image.cache
76
77 /**
78 * @brief The painted width of the image. This the same as using the image `sourceSize` property to set the width.
79 * @property int AnimatedImageViewer::imageWidth
80 */
81 property alias imageWidth: image.sourceSize.width
82
83 /**
84 * @brief The painted height of the image. This the same as using the image `sourceSize` property to set the height.
85 * @property int AnimatedImageViewer::imageHeight
86 */
87 property alias imageHeight: image.sourceSize.height
88
89 /**
90 * @brief The source of the image. Can be a remote or local file URL.
91 * @property url AnimatedImageViewer::source
92 */
93 property alias source : image.source
94
95 /**
96 * @brief Emitted when the image area has been right clicked with a mouse event.
97 */
98 signal rightClicked()
99
100 /**
101 * @brief Emitted when the image area has been pressed for a few seconds.
102 */
103 signal pressAndHold()
104
105 PinchArea {
106 width: Math.max(flick.contentWidth, flick.width)
107 height: Math.max(flick.contentHeight, flick.height)
108
109 property real initialWidth
110 property real initialHeight
111
112 onPinchStarted: {
113 initialWidth = flick.contentWidth
114 initialHeight = flick.contentHeight
115 }
116
117 onPinchUpdated: {
118 // adjust content pos due to drag
119 flick.contentX += pinch.previousCenter.x - pinch.center.x
120 flick.contentY += pinch.previousCenter.y - pinch.center.y
121
122 // resize content
123 flick.resizeContent(Math.max(flick.width*0.7, initialWidth * pinch.scale), Math.max(flick.height*0.7, initialHeight * pinch.scale), pinch.center)
124 }
125
126 onPinchFinished: {
127 // Move its content within bounds.
128 if (flick.contentWidth < flick.width ||
129 flick.contentHeight < flick.height) {
130 zoomAnim.x = 0;
131 zoomAnim.y = 0;
132 zoomAnim.width = flick.width;
133 zoomAnim.height = flick.height;
134 zoomAnim.running = true;
135 } else {
136 flick.returnToBounds();
137 }
138 }
139
140 ParallelAnimation {
141 id: zoomAnim
142 property real x: 0
143 property real y: 0
144 property real width: flick.width
145 property real height: flick.height
146 NumberAnimation {
147 target: flick
148 property: "contentWidth"
149 from: flick.contentWidth
150 to: zoomAnim.width
151 duration: Maui.Style.units.longDuration
152 easing.type: Easing.InOutQuad
153 }
154 NumberAnimation {
155 target: flick
156 property: "contentHeight"
157 from: flick.contentHeight
158 to: zoomAnim.height
159 duration: Maui.Style.units.longDuration
160 easing.type: Easing.InOutQuad
161 }
162 NumberAnimation {
163 target: flick
164 property: "contentY"
165 from: flick.contentY
166 to: zoomAnim.y
167 duration: Maui.Style.units.longDuration
168 easing.type: Easing.InOutQuad
169 }
170 NumberAnimation {
171 target: flick
172 property: "contentX"
173 from: flick.contentX
174 to: zoomAnim.x
175 duration: Maui.Style.units.longDuration
176 easing.type: Easing.InOutQuad
177 }
178 }
179
180 AnimatedImage
181 {
182 id: image
183 width: flick.contentWidth
184 height: flick.contentHeight
185 fillMode: AnimatedImage.PreserveAspectFit
186 autoTransform: true
187 asynchronous: true
188 onStatusChanged:
189 {
190 playing = (status == AnimatedImage.Ready)
191 }
192
193 BusyIndicator
194 {
195 anchors.centerIn: parent
196 running: parent.status === AnimatedImage.Loading
197 }
198
199 MouseArea {
200 anchors.fill: parent
201 acceptedButtons: Qt.RightButton | Qt.LeftButton
202 onClicked: (mouse) =>
203 {
204 if(!Maui.Handy.isMobile && mouse.button === Qt.RightButton)
205 {
206 flick.rightClicked()
207 }
208 }
209
210 onPressAndHold: flick.pressAndHold()
211 onDoubleClicked: (mouse) =>
212 {
213 if (flick.interactive) {
214 zoomAnim.x = 0;
215 zoomAnim.y = 0;
216 zoomAnim.width = flick.width;
217 zoomAnim.height = flick.height;
218 zoomAnim.running = true;
219 flick.interactive = !flick.interactive
220 } else {
221 zoomAnim.x = mouse.x * 2;
222 zoomAnim.y = mouse.y *2;
223 zoomAnim.width = flick.width * 3;
224 zoomAnim.height = flick.height * 3;
225 zoomAnim.running = true;
226 flick.interactive = !flick.interactive
227 }
228 }
229 onWheel: (wheel) =>
230 {
231 if (wheel.modifiers & Qt.ControlModifier) {
232 if (wheel.angleDelta.y != 0) {
233 var factor = 1 + wheel.angleDelta.y / 600;
234 zoomAnim.running = false;
235
236 zoomAnim.width = Math.min(Math.max(flick.width, zoomAnim.width * factor), flick.width * 4);
237 zoomAnim.height = Math.min(Math.max(flick.height, zoomAnim.height * factor), flick.height * 4);
238
239 //actual factors, may be less than factor
240 var xFactor = zoomAnim.width / flick.contentWidth;
241 var yFactor = zoomAnim.height / flick.contentHeight;
242
243 zoomAnim.x = flick.contentX * xFactor + (((wheel.x - flick.contentX) * xFactor) - (wheel.x - flick.contentX))
244 zoomAnim.y = flick.contentY * yFactor + (((wheel.y - flick.contentY) * yFactor) - (wheel.y - flick.contentY))
245 zoomAnim.running = true;
246
247 } else if (wheel.pixelDelta.y != 0) {
248 flick.resizeContent(Math.min(Math.max(flick.width, flick.contentWidth + wheel.pixelDelta.y), flick.width * 4),
249 Math.min(Math.max(flick.height, flick.contentHeight + wheel.pixelDelta.y), flick.height * 4),
250 wheel);
251 }
252 } else {
253 flick.contentX += wheel.pixelDelta.x;
254 flick.contentY += wheel.pixelDelta.y;
255 }
256 }
257 }
258 }
259 }
260
261 /**
262 * @brief Forces the image to fit in the viewport.
263 */
264 function fit()
265 {
266 image.width = image.sourceSize.width
267 }
268
269 /**
270 * @brief Forces the image to fill-in the viewport, this is done horizontally, so the image might be out of view vertically.
271 */
272 function fill()
273 {
274 image.width = parent.width
275 }
276
277 /**
278 * @brief Forces the image to be rotated 90 degrees to the left.
279 */
280 function rotateLeft()
281 {
282 image.rotation = image.rotation - 90
283 }
284
285 /**
286 * @brief Forces the image to be rotated 90 degrees to the right.
287 */
288 function rotateRight()
289 {
290 image.rotation = image.rotation + 90
291 }
292}
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri May 2 2025 11:57:11 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.