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 }