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 }