Warning, /graphics/koko/src/qml/ZoomArea.qml is written in an unsupported language. File is not indexed.
0001 /*
0002 * SPDX-FileCopyrightText: (C) 2015 Vishesh Handa <vhanda@kde.org>
0003 * SPDX-FileCopyrightText: (C) 2017 Atul Sharma <atulsharma406@gmail.com>
0004 * SPDX-FileCopyrightText: (C) 2017 Marco Martin <mart@kde.org>
0005 * SPDX-FileCopyrightText: (C) 2021 Noah Davis <noahadvs@gmail.com>
0006 *
0007 * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0008 */
0009
0010 import QtQuick 2.15
0011 import QtQml 2.15
0012 import QtMultimedia 5.15
0013 import org.kde.kirigami 2.15 as Kirigami
0014 import org.kde.koko 0.1
0015 import org.kde.koko.image 0.1
0016
0017 MouseArea {
0018 id: root
0019 readonly property bool interactive: Math.floor(contentItem.width) > root.width || Math.floor(contentItem.height) > root.height
0020 property bool dragging: root.drag.active || pinchArea.pinch.active
0021
0022 /**
0023 * Properties used for contentItem manipulation.
0024 */
0025 readonly property alias contentItem: contentItem
0026 default property alias contentData: contentItem.data
0027 property alias contentChildren: contentItem.children
0028 // NOTE: Unlike Flickable, contentX and contentY do not have reversed signs.
0029 // NOTE: contentX and contentY can be NaN/undefined sometimes even when
0030 // contentItem.x and contentItem.y aren't and I'm not sure why.
0031 property alias contentX: contentItem.x
0032 property alias contentY: contentItem.y
0033 property alias contentWidth: contentItem.width
0034 property alias contentHeight: contentItem.height
0035 property alias implicitContentWidth: contentItem.implicitWidth
0036 property alias implicitContentHeight: contentItem.implicitHeight
0037 readonly property rect defaultContentRect: {
0038 const size = fittedContentSize(contentItem.implicitWidth, contentItem.implicitHeight)
0039 return Qt.rect(centerContentX(size.width), centerContentY(size.height), size.width, size.height)
0040 }
0041 readonly property real contentAspectRatio: contentItem.implicitWidth / contentItem.implicitHeight
0042 readonly property real viewAspectRatio: root.width / root.height
0043 // Should be the same for both width and height
0044 readonly property real zoomFactor: contentItem.width / contentItem.implicitWidth
0045
0046 // Minimum is a size because a factor doesn't necessarily
0047 // limit based on what is visible on the user's screen.
0048 // NOTE: if the implicit content size is smaller, that will be used instead.
0049 property int minimumZoomSize: 8
0050 // Maximum is a factor because scaling up in proportion to the
0051 // original size is the most common behavior for zoom controls.
0052 property real maximumZoomFactor: 100
0053
0054 // Fit to root unless arguments are smaller than the size of root.
0055 // Returning size instead of using separate width and height functions
0056 // since they both need to be calculated together.
0057 function fittedContentSize(w, h) {
0058 const factor = root.contentAspectRatio >= root.viewAspectRatio ?
0059 root.width / w : root.height / h
0060 if (w > root.width || h > root.height) {
0061 w = w * factor
0062 h = h * factor
0063 }
0064 return Qt.size(w, h)
0065 }
0066
0067 // Get the X value that would center the contentItem with the given content width.
0068 function centerContentX(cWidth = contentItem.width) {
0069 return Math.round((root.width - cWidth) / 2)
0070 }
0071
0072 // Get the Y value that would center the contentItem with the given content height.
0073 function centerContentY(cHeight = contentItem.height) {
0074 return Math.round((root.height - cHeight) / 2)
0075 }
0076
0077 // Right side of content touches right side of root.
0078 function minContentX(cWidth = contentItem.width) {
0079 return cWidth > root.width ? root.width - cWidth : centerContentX(cWidth)
0080 }
0081 // Left side of content touches left side of root.
0082 function maxContentX(cWidth = contentItem.width) {
0083 return cWidth > root.width ? 0 : centerContentX(cWidth)
0084 }
0085 // Bottom side of content touches bottom side of root.
0086 function minContentY(cHeight = contentItem.height) {
0087 return cHeight > root.height ? root.height - cHeight : centerContentY(cHeight)
0088 }
0089 // Top side of content touches top side of root.
0090 function maxContentY(cHeight = contentItem.height) {
0091 return cHeight > root.height ? 0 : centerContentY(cHeight)
0092 }
0093
0094 function bound(min, value, max) {
0095 return Math.min(Math.max(min, value), max)
0096 }
0097
0098 function boundedContentWidth(newWidth) {
0099 return bound(Math.min(contentItem.implicitWidth, root.minimumZoomSize),
0100 newWidth,
0101 contentItem.implicitWidth * root.maximumZoomFactor)
0102 }
0103 function boundedContentHeight(newHeight) {
0104 return bound(Math.min(contentItem.implicitHeight, root.minimumZoomSize),
0105 newHeight,
0106 contentItem.implicitHeight * root.maximumZoomFactor)
0107 }
0108
0109 function boundedContentX(newX, cWidth = contentItem.width) {
0110 return Math.round(bound(minContentX(cWidth), newX, maxContentX(cWidth)))
0111 }
0112 function boundedContentY(newY, cHeight = contentItem.height) {
0113 return Math.round(bound(minContentY(cHeight), newY, maxContentY(cHeight)))
0114 }
0115
0116 function heightForWidth(w = contentItem.width) {
0117 return w / root.contentAspectRatio
0118 }
0119 function widthForHeight(h = contentItem.height) {
0120 return h * root.contentAspectRatio
0121 }
0122
0123 function addContentSize(value, w = contentItem.width, h = contentItem.height) {
0124 if (root.contentAspectRatio >= 1) {
0125 w = boundedContentWidth(w + value)
0126 h = heightForWidth(w)
0127 } else {
0128 h = boundedContentHeight(h + value)
0129 w = widthForHeight(h)
0130 }
0131 return Qt.size(w, h)
0132 }
0133
0134 function multiplyContentSize(value, w = contentItem.width, h = contentItem.height) {
0135 if (root.contentAspectRatio >= 1) {
0136 w = boundedContentWidth(w * value)
0137 h = heightForWidth(w)
0138 } else {
0139 h = boundedContentHeight(h * value)
0140 w = widthForHeight(h)
0141 }
0142 return Qt.size(w, h)
0143 }
0144
0145 /**
0146 * Basic formula: (qreal) steps * singleStep * wheelScrollLines
0147 * 120 delta units == 1 step.
0148 * singleStep is the step amount in pixels.
0149 * wheelScrollLines is the step multiplier.
0150 *
0151 * There is no real standard for scroll speed.
0152 * - QScrollArea uses `singleStep = 20`
0153 * - QGraphicsView uses `singleStep = dimension / 20`
0154 * - Kirigami WheelHandler uses `singleStep = delta / 8`
0155 * - Some apps use `singleStep = QFontMetrics::height()`
0156 */
0157 function angleDeltaToPixels(delta, dimension) {
0158 const singleStep = dimension !== undefined ? dimension / 20 : 20
0159 return delta / 120 * singleStep * Qt.styleHints.wheelScrollLines
0160 }
0161
0162 clip: true
0163 acceptedButtons: root.interactive ? Qt.LeftButton | Qt.MiddleButton : Qt.LeftButton
0164 cursorShape: if (root.interactive) {
0165 return pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
0166 } else {
0167 return Qt.ArrowCursor
0168 }
0169
0170 drag {
0171 axis: Drag.XAndYAxis
0172 target: root.interactive ? contentItem : undefined
0173 minimumX: root.minContentX(contentItem.width)
0174 maximumX: root.maxContentX(contentItem.width)
0175 minimumY: root.minContentY(contentItem.height)
0176 maximumY: root.maxContentY(contentItem.height)
0177 }
0178
0179 Item {
0180 id: contentItem
0181 width: root.defaultContentRect.width
0182 height: root.defaultContentRect.height
0183 x: root.defaultContentRect.x
0184 y: root.defaultContentRect.y
0185 }
0186
0187 // Auto center
0188 Binding {
0189 // we tried using delayed here but that causes flicker issues
0190 target: contentItem; property: "x"
0191 when: contentItem.implicitWidth > 0 && Math.floor(contentItem.width) <= root.width && !root.dragging
0192 value: root.centerContentX(contentItem.width)
0193 restoreMode: Binding.RestoreNone
0194 }
0195 Binding {
0196 target: contentItem; property: "y"
0197 when: contentItem.implicitHeight > 0 && Math.floor(contentItem.height) <= root.height && !root.dragging
0198 value: root.centerContentY(contentItem.height)
0199 restoreMode: Binding.RestoreNone
0200 }
0201
0202 onWidthChanged: if (contentItem.width > width) {
0203 contentItem.x = boundedContentX(contentItem.x)
0204 }
0205 onHeightChanged: if (contentItem.height > height) {
0206 contentItem.y = boundedContentY(contentItem.y)
0207 }
0208
0209 // TODO: test this with a device capable of generating pinch events
0210 PinchArea {
0211 id: pinchArea
0212 property real initialWidth: 0
0213 property real initialHeight: 0
0214 parent: root
0215 anchors.fill: parent
0216 enabled: root.enabled
0217 pinch {
0218 dragAxis: Pinch.XAndYAxis
0219 target: root.drag.target
0220 minimumX: root.drag.minimumX
0221 maximumX: root.drag.maximumX
0222 minimumY: root.drag.minimumY
0223 maximumY: root.drag.maximumY
0224 minimumScale: 1
0225 maximumScale: 1
0226 minimumRotation: 0
0227 maximumRotation: 0
0228 }
0229
0230 onPinchStarted: {
0231 initialWidth = contentItem.width
0232 initialHeight = contentItem.height
0233 }
0234
0235 onPinchUpdated: {
0236 // adjust content pos due to drag
0237 //contentItem.x = pinch.previousCenter.x - pinch.center.x + contentItem.x
0238 //contentItem.y = pinch.previousCenter.y - pinch.center.y + contentItem.y
0239
0240 // resize content
0241 const newSize = root.multiplyContentSize(pinch.scale, initialWidth, initialHeight)
0242 contentItem.width = newSize.width
0243 contentItem.height = newSize.height
0244 //contentItem.x = boundedContentX(contentItem.x - pinch.center.x)
0245 //contentItem.y = boundedContentY(contentItem.y - pinch.center.y)
0246 }
0247 }
0248
0249 onDoubleClicked: if (mouse.button === Qt.LeftButton) {
0250 if (Kirigami.Settings.isMobile) { applicationWindow().controlsVisible = false }
0251 if (contentItem.width !== root.defaultContentRect.width || contentItem.height !== root.defaultContentRect.height) {
0252 contentItem.width = Qt.binding(() => root.defaultContentRect.width)
0253 contentItem.height = Qt.binding(() => root.defaultContentRect.height)
0254 } else {
0255 const cX = contentItem.x, cY = contentItem.y
0256 contentItem.width = root.defaultContentRect.width * 2
0257 contentItem.height = root.defaultContentRect.height * 2
0258 // content position * factor - mouse position
0259 contentItem.x = root.boundedContentX(cX * 2 - mouse.x, contentItem.width)
0260 contentItem.y = root.boundedContentY(cY * 2 - mouse.y, contentItem.height)
0261 }
0262 }
0263 onWheel: {
0264 if (wheel.modifiers & Qt.ControlModifier || wheel.modifiers & Qt.ShiftModifier) {
0265 const pixelDeltaX = wheel.pixelDelta.x !== 0 ?
0266 wheel.pixelDelta.x : angleDeltaToPixels(wheel.angleDelta.x, root.width)
0267 const pixelDeltaY = wheel.pixelDelta.y !== 0 ?
0268 wheel.pixelDelta.y : angleDeltaToPixels(wheel.angleDelta.y, root.height)
0269 if (pixelDeltaX !== 0 && pixelDeltaY !== 0) {
0270 contentItem.x = root.boundedContentX(pixelDeltaX + contentItem.x)
0271 contentItem.y = root.boundedContentY(pixelDeltaY + contentItem.y)
0272 } else if (pixelDeltaX !== 0 && pixelDeltaY === 0) {
0273 contentItem.x = root.boundedContentX(pixelDeltaX + contentItem.x)
0274 } else if (pixelDeltaX === 0 && pixelDeltaY !== 0 && wheel.modifiers & Qt.ShiftModifier) {
0275 contentItem.x = root.boundedContentX(pixelDeltaY + contentItem.x)
0276 } else {
0277 contentItem.y = root.boundedContentY(pixelDeltaY + contentItem.y)
0278 }
0279 } else {
0280 let factor = 1 + Math.abs(wheel.angleDelta.y / 600)
0281 if (wheel.angleDelta.y < 0) {
0282 factor = 1 / factor
0283 }
0284 const oldRect = Qt.rect(contentItem.x, contentItem.y, contentItem.width, contentItem.height)
0285 const newSize = root.multiplyContentSize(factor)
0286 // round to default size if within ±1
0287 if ((newSize.height > root.defaultContentRect.height - 1
0288 && newSize.height < root.defaultContentRect.height + 1)
0289 || (newSize.width > root.defaultContentRect.width - 1
0290 && newSize.width < root.defaultContentRect.width + 1)
0291 ) {
0292 contentItem.width = root.defaultContentRect.width
0293 contentItem.height = root.defaultContentRect.height
0294 } else {
0295 contentItem.width = newSize.width
0296 contentItem.height = newSize.height
0297 }
0298 if (root.interactive) {
0299 contentItem.x = root.boundedContentX(wheel.x - contentItem.width * ((wheel.x - oldRect.x)/oldRect.width))
0300 contentItem.y = root.boundedContentY(wheel.y - contentItem.height * ((wheel.y - oldRect.y)/oldRect.height))
0301 }
0302 }
0303 }
0304 }