MauiKit Controls

ImageViewer.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 * @inherit QtQuick.Flickable
15 * @brief A view container for displaying images.
16 *
17 * <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>
18 *
19 * This control along with the AnimatedImageViewer are meant to display images, with support for zooming in and out with touch or mouse gestures, and keyboard shortcuts.
20 *
21 */
22Flickable
23{
24 id: flick
25
26 contentWidth: width
27 contentHeight: height
28 boundsBehavior: Flickable.StopAtBounds
29 boundsMovement: Flickable.StopAtBounds
30 interactive: contentWidth > width || contentHeight > height
31 clip: false
32
33 ScrollBar.vertical: ScrollBar
34 {
35 visible: false
36 }
37
38 ScrollBar.horizontal: ScrollBar
39 {
40 visible: false
41 }
42
43 readonly property bool zooming: contentHeight != height || contentWidth != width
44
45 // readonly property int zoomFactor:
46 /**
47 * @brief This an alias to the actual control painting the image.
48 * This control is handled by a QQC2 Image.
49 * @note See Qt documentation for more information about the Image control.
50 * @property Image ImageViewer::image
51 */
52 readonly property alias image: image
53
54 /**
55 * @brief The painted size of the image.
56 * As taken from Qt documentation: This property holds the scaled width and height of the full-frame image.
57 * 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.
58 * @property size ImageViewer::sourceSize
59 */
60 property alias sourceSize : image.sourceSize
61
62 /**
63 * @brief The fill mode of the image. The possible values can be found on the Image control documentation from Qt.
64 * By default this is set to `Image.PreserveAspectFit`.
65 * @property enumaration ImageViewer::fillMode
66 */
67 property alias fillMode: image.fillMode
68
69 /**
70 * @brief Whether the image should be loaded asynchronously.
71 * By default this is set to `true`.
72 * @property bool ImageViewer::asynchronous
73 */
74 property alias asynchronous : image.asynchronous
75
76 /**
77 * @brief If the image should be cached in memory.
78 * The default value is set to `true`
79 * @property bool ImageViewer::cache
80 */
81 property alias cache: image.cache
82
83 /**
84 * @brief The painted width of the image. This the same as using the image `sourceSize` property to set the width.
85 * @property int ImageViewer::imageWidth
86 */
87 property alias imageWidth: image.sourceSize.width
88
89 /**
90 * @brief The painted height of the image. This the same as using the image `sourceSize` property to set the height.
91 * @property int ImageViewer::imageHeight
92 */
93 property alias imageHeight: image.sourceSize.height
94
95 /**
96 * @brief The source of the image. Can be a remote or local file URL.
97 * @property url ImageViewer::source
98 */
99 property alias source : image.source
100
101 /**
102 * @brief Emitted when the image area has been right clicked with a mouse event.
103 */
104 signal rightClicked()
105
106 /**
107 * @brief Emitted when the image area has been pressed for a few seconds.
108 */
109 signal pressAndHold()
110
111 PinchArea
112 {
113 width: Math.max(flick.contentWidth, flick.width)
114 height: Math.max(flick.contentHeight, flick.height)
115
116 property real initialWidth
117 property real initialHeight
118
119 onPinchStarted: {
120 initialWidth = flick.contentWidth
121 initialHeight = flick.contentHeight
122 }
123
124 onPinchUpdated: (pinch) => {
125 // adjust content pos due to drag
126 flick.contentX += pinch.previousCenter.x - pinch.center.x
127 flick.contentY += pinch.previousCenter.y - pinch.center.y
128
129 // resize content
130 flick.resizeContent(Math.max(flick.width*0.7, initialWidth * pinch.scale), Math.max(flick.height*0.7, initialHeight * pinch.scale), pinch.center)
131 }
132
133 onPinchFinished: {
134 // Move its content within bounds.
135 if (flick.contentWidth < flick.width ||
136 flick.contentHeight < flick.height) {
137 zoomAnim.x = 0;
138 zoomAnim.y = 0;
139 zoomAnim.width = flick.width;
140 zoomAnim.height = flick.height;
141 zoomAnim.running = true;
142 } else {
143 flick.returnToBounds();
144 }
145 }
146
147 ParallelAnimation {
148 id: zoomAnim
149 property real x: 0
150 property real y: 0
151 property real width: flick.width
152 property real height: flick.height
153 NumberAnimation {
154 target: flick
155 property: "contentWidth"
156 from: flick.contentWidth
157 to: zoomAnim.width
158 duration: Maui.Style.units.longDuration
159 easing.type: Easing.InOutQuad
160 }
161 NumberAnimation {
162 target: flick
163 property: "contentHeight"
164 from: flick.contentHeight
165 to: zoomAnim.height
166 duration: Maui.Style.units.longDuration
167 easing.type: Easing.InOutQuad
168 }
169 NumberAnimation {
170 target: flick
171 property: "contentY"
172 from: flick.contentY
173 to: zoomAnim.y
174 duration: Maui.Style.units.longDuration
175 easing.type: Easing.InOutQuad
176 }
177 NumberAnimation {
178 target: flick
179 property: "contentX"
180 from: flick.contentX
181 to: zoomAnim.x
182 duration: Maui.Style.units.longDuration
183 easing.type: Easing.InOutQuad
184 }
185 }
186
187 Image
188 {
189 id: image
190 width: flick.contentWidth
191 height: flick.contentHeight
192 fillMode: Image.PreserveAspectFit
193 autoTransform: true
194 asynchronous: true
195
196 Maui.ProgressIndicator
197 {
198 width: parent.width
199 anchors.bottom: parent.bottom
200 visible: image.status === Image.Loading
201 }
202
203 Maui.Holder
204 {
205 anchors.fill: parent
206 visible: image.status === Image.Error || image.status === Image.Null
207 title: i18nd("mauikit", "Oops!")
208 body: i18nd("mauikit", "The image could not be loaded.")
209 emoji: "qrc:/assets/dialog-information.svg"
210 }
211
212 MouseArea {
213 anchors.fill: parent
214 acceptedButtons: Qt.RightButton | Qt.LeftButton
215 onClicked: (mouse) =>
216 {
217 if(!Maui.Handy.isMobile && mouse.button === Qt.RightButton)
218 {
219 flick.rightClicked()
220 }
221 }
222
223 onPressAndHold: flick.pressAndHold()
224
225 onDoubleClicked: (mouse) =>
226 {
227 if (flick.interactive)
228 {
229 zoomAnim.x = 0;
230 zoomAnim.y = 0;
231 zoomAnim.width = flick.width;
232 zoomAnim.height = flick.height;
233 zoomAnim.running = true;
234 flick.interactive = !flick.interactive
235 } else
236 {
237 zoomAnim.x = mouse.x * 2;
238 zoomAnim.y = mouse.y *2;
239 zoomAnim.width = flick.width * 3;
240 zoomAnim.height = flick.height * 3;
241 zoomAnim.running = true;
242 flick.interactive = !flick.interactive
243 }
244 }
245
246 onWheel: (wheel) =>
247 {
248 if (wheel.modifiers & Qt.ControlModifier) {
249 if (wheel.angleDelta.y != 0) {
250 var factor = 1 + wheel.angleDelta.y / 600;
251 zoomAnim.running = false;
252
253 zoomAnim.width = Math.min(Math.max(flick.width, zoomAnim.width * factor), flick.width * 4);
254 zoomAnim.height = Math.min(Math.max(flick.height, zoomAnim.height * factor), flick.height * 4);
255
256 //actual factors, may be less than factor
257 var xFactor = zoomAnim.width / flick.contentWidth;
258 var yFactor = zoomAnim.height / flick.contentHeight;
259
260 zoomAnim.x = flick.contentX * xFactor + (((wheel.x - flick.contentX) * xFactor) - (wheel.x - flick.contentX))
261 zoomAnim.y = flick.contentY * yFactor + (((wheel.y - flick.contentY) * yFactor) - (wheel.y - flick.contentY))
262 zoomAnim.running = true;
263
264 } else if (wheel.pixelDelta.y != 0) {
265 flick.resizeContent(Math.min(Math.max(flick.width, flick.contentWidth + wheel.pixelDelta.y), flick.width * 4),
266 Math.min(Math.max(flick.height, flick.contentHeight + wheel.pixelDelta.y), flick.height * 4),
267 wheel);
268 }
269 } else {
270
271 if(zoomAnim.width !== flick.contentWidth || zoomAnim.height !== flick.contentHeight)
272 {
273 flick.contentX += wheel.pixelDelta.x;
274 flick.contentY += wheel.pixelDelta.y;
275 }else
276 {
277 wheel.accepted = false
278 }
279 }
280 }
281 }
282 }
283 }
284
285 /**
286 * @brief Forces the image to fit in the viewport.
287 */
288 function fit()
289 {
290 image.width = image.sourceSize.width
291 }
292
293 /**
294 * @brief Forces the image to fill-in the viewport, this is done horizontally, so the image might be out of view vertically.
295 */
296 function fill()
297 {
298 image.width = parent.width
299 }
300
301 /**
302 * @brief Forces the image to be rotated 90 degrees to the left.
303 */
304 function rotateLeft()
305 {
306 image.rotation = image.rotation - 90
307 }
308
309 /**
310 * @brief Forces the image to be rotated 90 degrees to the right.
311 */
312 function rotateRight()
313 {
314 image.rotation = image.rotation + 90
315 }
316}
void information(QWidget *parent, const QString &text, const QString &title=QString(), const QString &dontShowAgainName=QString(), Options options=Notify)
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.