diff options
Diffstat (limited to 'src/pdfquick/PdfScrollablePageView.qml')
-rw-r--r-- | src/pdfquick/PdfScrollablePageView.qml | 487 |
1 files changed, 487 insertions, 0 deletions
diff --git a/src/pdfquick/PdfScrollablePageView.qml b/src/pdfquick/PdfScrollablePageView.qml new file mode 100644 index 000000000..9fa0547c6 --- /dev/null +++ b/src/pdfquick/PdfScrollablePageView.qml @@ -0,0 +1,487 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Pdf +import QtQuick.Shapes + +/*! + \qmltype PdfScrollablePageView + \inqmlmodule QtQuick.Pdf + \brief A complete PDF viewer component to show one page a time, with scrolling. + + PdfScrollablePageView provides a PDF viewer component that shows one page + at a time, with scrollbars to move around the page. It also supports + selecting text and copying it to the clipboard, zooming in and out, + clicking an internal link to jump to another section in the document, + rotating the view, and searching for text. The pdfviewer example + demonstrates how to use these features in an application. + + The implementation is a QML assembly of smaller building blocks that are + available separately. In case you want to make changes in your own version + of this component, you can copy the QML, which is installed into the + \c QtQuick/Pdf/qml module directory, and modify it as needed. + + \sa PdfPageView, PdfMultiPageView, PdfStyle +*/ +Flickable { + /*! + \qmlproperty PdfDocument PdfScrollablePageView::document + + A PdfDocument object with a valid \c source URL is required: + + \snippet multipageview.qml 0 + */ + required property PdfDocument document + + /*! + \qmlproperty int PdfScrollablePageView::status + + This property holds the \l {QtQuick::Image::status}{rendering status} of + the \l {currentPage}{current page}. + */ + property alias status: image.status + + /*! + \qmlproperty PdfDocument PdfScrollablePageView::selectedText + + The selected text. + */ + property alias selectedText: selection.text + + /*! + \qmlmethod void PdfScrollablePageView::selectAll() + + Selects all the text on the \l {currentPage}{current page}, and makes it + available as the system \l {QClipboard::Selection}{selection} on systems + that support that feature. + + \sa copySelectionToClipboard() + */ + function selectAll() { + selection.selectAll() + } + + /*! + \qmlmethod void PdfScrollablePageView::copySelectionToClipboard() + + Copies the selected text (if any) to the + \l {QClipboard::Clipboard}{system clipboard}. + + \sa selectAll() + */ + function copySelectionToClipboard() { + selection.copyToClipboard() + } + + // -------------------------------- + // page navigation + + /*! + \qmlproperty int PdfScrollablePageView::currentPage + \readonly + + This property holds the zero-based page number of the page visible in the + scrollable view. If there is no current page, it holds -1. + + This property is read-only, and is typically used in a binding (or + \c onCurrentPageChanged script) to update the part of the user interface + that shows the current page number, such as a \l SpinBox. + + \sa PdfPageNavigator::currentPage + */ + property alias currentPage: pageNavigator.currentPage + + /*! + \qmlproperty bool PdfScrollablePageView::backEnabled + \readonly + + This property indicates if it is possible to go back in the navigation + history to a previous-viewed page. + + \sa PdfPageNavigator::backAvailable, back() + */ + property alias backEnabled: pageNavigator.backAvailable + + /*! + \qmlproperty bool PdfScrollablePageView::forwardEnabled + \readonly + + This property indicates if it is possible to go to next location in the + navigation history. + + \sa PdfPageNavigator::forwardAvailable, forward() + */ + property alias forwardEnabled: pageNavigator.forwardAvailable + + /*! + \qmlmethod void PdfScrollablePageView::back() + + Scrolls the view back to the previous page that the user visited most + recently; or does nothing if there is no previous location on the + navigation stack. + + \sa PdfPageNavigator::back(), currentPage, backEnabled + */ + function back() { pageNavigator.back() } + + /*! + \qmlmethod void PdfScrollablePageView::forward() + + Scrolls the view to the page that the user was viewing when the back() + method was called; or does nothing if there is no "next" location on the + navigation stack. + + \sa PdfPageNavigator::forward(), currentPage + */ + function forward() { pageNavigator.forward() } + + /*! + \qmlmethod void PdfScrollablePageView::goToPage(int page) + + Changes the view to the \a page, if possible. + + \sa PdfPageNavigator::jump(), currentPage + */ + function goToPage(page) { + if (page === pageNavigator.currentPage) + return + goToLocation(page, Qt.point(0, 0), 0) + } + + /*! + \qmlmethod void PdfScrollablePageView::goToLocation(int page, point location, real zoom) + + Scrolls the view to the \a location on the \a page, if possible, + and sets the \a zoom level. + + \sa PdfPageNavigator::jump(), currentPage + */ + function goToLocation(page, location, zoom) { + if (zoom > 0) + root.renderScale = zoom + pageNavigator.jump(page, location, zoom) + } + + // -------------------------------- + // page scaling + + /*! + \qmlproperty real PdfScrollablePageView::renderScale + + This property holds the ratio of pixels to points. The default is \c 1, + meaning one point (1/72 of an inch) equals 1 logical pixel. + */ + property real renderScale: 1 + + /*! + \qmlproperty real PdfScrollablePageView::pageRotation + + This property holds the clockwise rotation of the pages. + + The default value is \c 0 degrees (that is, no rotation relative to the + orientation of the pages as stored in the PDF file). + */ + property real pageRotation: 0 + + /*! + \qmlproperty size PdfScrollablePageView::sourceSize + + This property holds the scaled width and height of the full-frame image. + + \sa {QtQuick::Image::sourceSize}{Image.sourceSize} + */ + property alias sourceSize: image.sourceSize + + /*! + \qmlmethod void PdfScrollablePageView::resetScale() + + Sets \l renderScale back to its default value of \c 1. + */ + function resetScale() { + paper.scale = 1 + root.renderScale = 1 + } + + /*! + \qmlmethod void PdfScrollablePageView::scaleToWidth(real width, real height) + + Sets \l renderScale such that the width of the first page will fit into a + viewport with the given \a width and \a height. If the page is not rotated, + it will be scaled so that its width fits \a width. If it is rotated +/- 90 + degrees, it will be scaled so that its width fits \a height. + */ + function scaleToWidth(width, height) { + const pagePointSize = document.pagePointSize(pageNavigator.currentPage) + root.renderScale = root.width / (paper.rot90 ? pagePointSize.height : pagePointSize.width) + console.log(lcSPV, "scaling", pagePointSize, "to fit", root.width, "rotated?", paper.rot90, "scale", root.renderScale) + root.contentX = 0 + root.contentY = 0 + } + + /*! + \qmlmethod void PdfScrollablePageView::scaleToPage(real width, real height) + + Sets \l renderScale such that the whole first page will fit into a viewport + with the given \a width and \a height. The resulting \l renderScale depends + on \l pageRotation: the page will fit into the viewport at a larger size if + it is first rotated to have a matching aspect ratio. + */ + function scaleToPage(width, height) { + const pagePointSize = document.pagePointSize(pageNavigator.currentPage) + root.renderScale = Math.min( + root.width / (paper.rot90 ? pagePointSize.height : pagePointSize.width), + root.height / (paper.rot90 ? pagePointSize.width : pagePointSize.height) ) + root.contentX = 0 + root.contentY = 0 + } + + // -------------------------------- + // text search + + /*! + \qmlproperty PdfSearchModel PdfScrollablePageView::searchModel + + This property holds a PdfSearchModel containing the list of search results + for a given \l searchString. + + \sa PdfSearchModel + */ + property alias searchModel: searchModel + + /*! + \qmlproperty string PdfScrollablePageView::searchString + + This property holds the search string that the user may choose to search + for. It is typically used in a binding to the \c text property of a + TextField. + + \sa searchModel + */ + property alias searchString: searchModel.searchString + + /*! + \qmlmethod void PdfScrollablePageView::searchBack() + + Decrements the + \l{PdfSearchModel::currentResult}{searchModel's current result} + so that the view will jump to the previous search result. + */ + function searchBack() { --searchModel.currentResult } + + /*! + \qmlmethod void PdfScrollablePageView::searchForward() + + Increments the + \l{PdfSearchModel::currentResult}{searchModel's current result} + so that the view will jump to the next search result. + */ + function searchForward() { ++searchModel.currentResult } + + // -------------------------------- + // implementation + id: root + PdfStyle { id: style } + contentWidth: paper.width + contentHeight: paper.height + ScrollBar.vertical: ScrollBar { + onActiveChanged: + if (!active ) { + const currentLocation = Qt.point((root.contentX + root.width / 2) / root.renderScale, + (root.contentY + root.height / 2) / root.renderScale) + pageNavigator.update(pageNavigator.currentPage, currentLocation, root.renderScale) + } + } + ScrollBar.horizontal: ScrollBar { + onActiveChanged: + if (!active ) { + const currentLocation = Qt.point((root.contentX + root.width / 2) / root.renderScale, + (root.contentY + root.height / 2) / root.renderScale) + pageNavigator.update(pageNavigator.currentPage, currentLocation, root.renderScale) + } + } + + onRenderScaleChanged: { + paper.scale = 1 + const currentLocation = Qt.point((root.contentX + root.width / 2) / root.renderScale, + (root.contentY + root.height / 2) / root.renderScale) + pageNavigator.update(pageNavigator.currentPage, currentLocation, root.renderScale) + } + + PdfSearchModel { + id: searchModel + document: root.document === undefined ? null : root.document + onCurrentResultChanged: pageNavigator.jump(currentResultLink) + } + + PdfPageNavigator { + id: pageNavigator + onJumped: function(current) { + root.renderScale = current.zoom + const dx = Math.max(0, current.location.x * root.renderScale - root.width / 2) - root.contentX + const dy = Math.max(0, current.location.y * root.renderScale - root.height / 2) - root.contentY + // don't jump if location is in the viewport already, i.e. if the "error" between desired and actual contentX/Y is small + if (Math.abs(dx) > root.width / 3) + root.contentX += dx + if (Math.abs(dy) > root.height / 3) + root.contentY += dy + console.log(lcSPV, "going to zoom", current.zoom, "loc", current.location, + "on page", current.page, "ended up @", root.contentX + ", " + root.contentY) + } + onCurrentPageChanged: searchModel.currentPage = currentPage + + property url documentSource: root.document.source + onDocumentSourceChanged: { + pageNavigator.clear() + root.resetScale() + root.contentX = 0 + root.contentY = 0 + } + } + + LoggingCategory { + id: lcSPV + name: "qt.pdf.singlepageview" + } + + Rectangle { + id: paper + width: rot90 ? image.height : image.width + height: rot90 ? image.width : image.height + property real rotationModulus: Math.abs(root.pageRotation % 180) + property bool rot90: rotationModulus > 45 && rotationModulus < 135 + property real minScale: 0.1 + property real maxScale: 10 + + PdfPageImage { + id: image + document: root.document + currentFrame: pageNavigator.currentPage + asynchronous: true + fillMode: Image.PreserveAspectFit + rotation: root.pageRotation + anchors.centerIn: parent + property real pageScale: image.paintedWidth / document.pagePointSize(pageNavigator.currentPage).width + width: document.pagePointSize(pageNavigator.currentPage).width * root.renderScale + height: document.pagePointSize(pageNavigator.currentPage).height * root.renderScale + sourceSize.width: width * Screen.devicePixelRatio + sourceSize.height: 0 + + Shape { + anchors.fill: parent + visible: image.status === Image.Ready + ShapePath { + strokeWidth: -1 + fillColor: style.pageSearchResultsColor + scale: Qt.size(image.pageScale, image.pageScale) + PathMultiline { + paths: searchModel.currentPageBoundingPolygons + } + } + ShapePath { + strokeWidth: style.currentSearchResultStrokeWidth + strokeColor: style.currentSearchResultStrokeColor + fillColor: "transparent" + scale: Qt.size(image.pageScale, image.pageScale) + PathMultiline { + paths: searchModel.currentResultBoundingPolygons + } + } + ShapePath { + fillColor: style.selectionColor + scale: Qt.size(image.pageScale, image.pageScale) + PathMultiline { + paths: selection.geometry + } + } + } + + Repeater { + model: PdfLinkModel { + id: linkModel + document: root.document + page: pageNavigator.currentPage + } + delegate: PdfLinkDelegate { + x: rectangle.x * image.pageScale + y: rectangle.y * image.pageScale + width: rectangle.width * image.pageScale + height: rectangle.height * image.pageScale + visible: image.status === Image.Ready + onTapped: + (link) => { + if (link.page >= 0) + pageNavigator.jump(link.page, link.location, link.zoom) + else + Qt.openUrlExternally(url) + } + } + } + DragHandler { + id: textSelectionDrag + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + target: null + } + TapHandler { + id: mouseClickHandler + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + } + TapHandler { + id: touchTapHandler + acceptedDevices: PointerDevice.TouchScreen + onTapped: { + selection.clear() + selection.focus = true + } + } + } + + PdfSelection { + id: selection + anchors.fill: parent + document: root.document + page: pageNavigator.currentPage + renderScale: image.pageScale == 0 ? 1.0 : image.pageScale + from: textSelectionDrag.centroid.pressPosition + to: textSelectionDrag.centroid.position + hold: !textSelectionDrag.active && !mouseClickHandler.pressed + focus: true + } + + PinchHandler { + id: pinch + minimumScale: paper.minScale / root.renderScale + maximumScale: Math.max(1, paper.maxScale / root.renderScale) + minimumRotation: 0 + maximumRotation: 0 + onActiveChanged: + if (!active) { + const centroidInPoints = Qt.point(pinch.centroid.position.x / root.renderScale, + pinch.centroid.position.y / root.renderScale) + const centroidInFlickable = root.mapFromItem(paper, pinch.centroid.position.x, pinch.centroid.position.y) + const newSourceWidth = image.sourceSize.width * paper.scale + const ratio = newSourceWidth / image.sourceSize.width + console.log(lcSPV, "pinch ended with centroid", pinch.centroid.position, centroidInPoints, "wrt flickable", centroidInFlickable, + "page at", paper.x.toFixed(2), paper.y.toFixed(2), + "contentX/Y were", root.contentX.toFixed(2), root.contentY.toFixed(2)) + if (ratio > 1.1 || ratio < 0.9) { + const centroidOnPage = Qt.point(centroidInPoints.x * root.renderScale * ratio, centroidInPoints.y * root.renderScale * ratio) + paper.scale = 1 + paper.x = 0 + paper.y = 0 + root.contentX = centroidOnPage.x - centroidInFlickable.x + root.contentY = centroidOnPage.y - centroidInFlickable.y + root.renderScale *= ratio // onRenderScaleChanged calls pageNavigator.update() so we don't need to here + console.log(lcSPV, "contentX/Y adjusted to", root.contentX.toFixed(2), root.contentY.toFixed(2)) + } else { + paper.x = 0 + paper.y = 0 + } + } + grabPermissions: PointerHandler.CanTakeOverFromAnything + } + } +} |