/**************************************************************************** ** ** Copyright (C) 2020 The Qt Company Ltd. ** Contact: http://www.qt.io/licensing/ ** ** This file is part of the QtPDF module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL3$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see http://www.qt.io/terms-conditions. For further ** information use the contact form at http://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPLv3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or later as published by the Free ** Software Foundation and appearing in the file LICENSE.GPL included in ** the packaging of this file. Please review the following information to ** ensure the GNU General Public License version 2.0 requirements will be ** met: http://www.gnu.org/licenses/gpl-2.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ import QtQuick 2.14 import QtQuick.Controls 2.14 import QtQuick.Pdf 5.15 import QtQuick.Shapes 1.14 import Qt.labs.animation 1.0 Rectangle { // public API // TODO 5.15: required property property var document: undefined property alias status: image.status property alias selectedText: selection.text function copySelectionToClipboard() { selection.copyToClipboard() } // page navigation property alias currentPage: navigationStack.currentPage property alias backEnabled: navigationStack.backAvailable property alias forwardEnabled: navigationStack.forwardAvailable function back() { navigationStack.back() } function forward() { navigationStack.forward() } function goToPage(page) { goToLocation(page, Qt.point(0, 0), 0) } function goToLocation(page, location, zoom) { if (zoom > 0) root.renderScale = zoom navigationStack.push(page, location, zoom) } // page scaling property real renderScale: 1 property alias sourceSize: image.sourceSize function resetScale() { image.sourceSize.width = 0 image.sourceSize.height = 0 root.x = 0 root.y = 0 root.scale = 1 } function scaleToWidth(width, height) { var halfRotation = Math.abs(root.rotation % 180) image.sourceSize = Qt.size((halfRotation > 45 && halfRotation < 135) ? height : width, 0) root.x = 0 root.y = 0 image.centerInSize = Qt.size(width, height) image.centerOnLoad = true image.vCenterOnLoad = (halfRotation > 45 && halfRotation < 135) root.scale = 1 } function scaleToPage(width, height) { var windowAspect = width / height var halfRotation = Math.abs(root.rotation % 180) var pagePointSize = document.pagePointSize(navigationStack.currentPage) if (halfRotation > 45 && halfRotation < 135) { // rotated 90 or 270ยบ var pageAspect = pagePointSize.height / pagePointSize.width if (windowAspect > pageAspect) { image.sourceSize = Qt.size(height, 0) } else { image.sourceSize = Qt.size(0, width) } } else { var pageAspect = pagePointSize.width / pagePointSize.height if (windowAspect > pageAspect) { image.sourceSize = Qt.size(0, height) } else { image.sourceSize = Qt.size(width, 0) } } image.centerInSize = Qt.size(width, height) image.centerOnLoad = true image.vCenterOnLoad = true root.scale = 1 } // text search property alias searchModel: searchModel property alias searchString: searchModel.searchString function searchBack() { --searchModel.currentResult } function searchForward() { ++searchModel.currentResult } // implementation id: root width: image.width height: image.height PdfSelection { id: selection document: root.document page: navigationStack.currentPage fromPoint: Qt.point(textSelectionDrag.centroid.pressPosition.x / image.pageScale, textSelectionDrag.centroid.pressPosition.y / image.pageScale) toPoint: Qt.point(textSelectionDrag.centroid.position.x / image.pageScale, textSelectionDrag.centroid.position.y / image.pageScale) hold: !textSelectionDrag.active && !tapHandler.pressed } PdfSearchModel { id: searchModel document: root.document === undefined ? null : root.document onCurrentPageChanged: root.goToPage(currentPage) } PdfNavigationStack { id: navigationStack onCurrentPageChanged: searchModel.currentPage = currentPage // TODO onCurrentLocationChanged: position currentLocation.x and .y in middle // currentPageChanged() MUST occur first! onCurrentZoomChanged: root.renderScale = currentZoom // TODO deal with horizontal location (need WheelHandler or Flickable probably) } Image { id: image currentFrame: navigationStack.currentPage source: document.status === PdfDocument.Ready ? document.source : "" asynchronous: true fillMode: Image.PreserveAspectFit property bool centerOnLoad: false property bool vCenterOnLoad: false property size centerInSize property real pageScale: image.paintedWidth / document.pagePointSize(navigationStack.currentPage).width function reRenderIfNecessary() { var newSourceWidth = image.sourceSize.width * root.scale var ratio = newSourceWidth / image.sourceSize.width if (ratio > 1.1 || ratio < 0.9) { image.sourceSize.width = newSourceWidth image.sourceSize.height = 0 root.scale = 1 } } onStatusChanged: if (status == Image.Ready && centerOnLoad) { root.x = (centerInSize.width - image.implicitWidth) / 2 root.y = vCenterOnLoad ? (centerInSize.height - image.implicitHeight) / 2 : 0 centerOnLoad = false vCenterOnLoad = false } } onRenderScaleChanged: { image.sourceSize.width = document.pagePointSize(navigationStack.currentPage).width * renderScale image.sourceSize.height = 0 root.scale = 1 } Shape { anchors.fill: parent opacity: 0.25 visible: image.status === Image.Ready ShapePath { strokeWidth: 1 strokeColor: "cyan" fillColor: "steelblue" scale: Qt.size(image.pageScale, image.pageScale) PathMultiline { paths: searchModel.currentPageBoundingPolygons } } ShapePath { strokeWidth: 1 strokeColor: "orange" fillColor: "cyan" scale: Qt.size(image.pageScale, image.pageScale) PathMultiline { paths: searchModel.currentResultBoundingPolygons } } ShapePath { fillColor: "orange" scale: Qt.size(image.pageScale, image.pageScale) PathMultiline { paths: selection.geometry } } } Repeater { model: PdfLinkModel { id: linkModel document: root.document page: navigationStack.currentPage } delegate: Rectangle { color: "transparent" border.color: "lightgrey" x: rect.x * image.pageScale y: rect.y * image.pageScale width: rect.width * image.pageScale height: rect.height * image.pageScale MouseArea { // TODO switch to TapHandler / HoverHandler in 5.15 anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { if (page >= 0) navigationStack.push(page, Qt.point(0, 0), root.renderScale) else Qt.openUrlExternally(url) } } } } PinchHandler { id: pinch minimumScale: 0.1 maximumScale: 10 minimumRotation: 0 maximumRotation: 0 onActiveChanged: if (!active) image.reRenderIfNecessary() grabPermissions: PinchHandler.TakeOverForbidden // don't allow takeover if pinch has started } DragHandler { id: pageMovingTouchDrag acceptedDevices: PointerDevice.TouchScreen } DragHandler { id: pageMovingMiddleMouseDrag acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus acceptedButtons: Qt.MiddleButton snapMode: DragHandler.NoSnap } DragHandler { id: textSelectionDrag acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus target: null } TapHandler { id: tapHandler acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus } // prevent it from being scrolled out of view BoundaryRule on x { minimum: 100 - root.width maximum: root.parent.width - 100 } BoundaryRule on y { minimum: 100 - root.height maximum: root.parent.height - 100 } }