diff options
Diffstat (limited to 'src/pdfquick/PdfPageView.qml')
-rw-r--r-- | src/pdfquick/PdfPageView.qml | 439 |
1 files changed, 439 insertions, 0 deletions
diff --git a/src/pdfquick/PdfPageView.qml b/src/pdfquick/PdfPageView.qml new file mode 100644 index 000000000..e1d97f57b --- /dev/null +++ b/src/pdfquick/PdfPageView.qml @@ -0,0 +1,439 @@ +// 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.Pdf +import QtQuick.Shapes + +/*! + \qmltype PdfPageView + \inqmlmodule QtQuick.Pdf + \brief A PDF viewer component to show one page a time. + + PdfPageView provides a PDF viewer component that shows one whole page at a + time, without scrolling. It 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 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 PdfScrollablePageView, PdfMultiPageView, PdfStyle +*/ +Rectangle { + /*! + \qmlproperty PdfDocument PdfPageView::document + + A PdfDocument object with a valid \c source URL is required: + + \snippet pdfpageview.qml 0 + */ + required property PdfDocument document + + /*! + \qmlproperty int PdfPageView::status + + This property holds the \l {QtQuick::Image::status}{rendering status} of + the \l {currentPage}{current page}. + */ + property alias status: image.status + + /*! + \qmlproperty PdfDocument PdfPageView::selectedText + + The selected text. + */ + property alias selectedText: selection.text + + /*! + \qmlmethod void PdfPageView::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 PdfPageView::copySelectionToClipboard() + + Copies the selected text (if any) to the + \l {QClipboard::Clipboard}{system clipboard}. + + \sa selectAll() + */ + function copySelectionToClipboard() { + selection.copyToClipboard() + } + + // -------------------------------- + // page navigation + + /*! + \qmlproperty int PdfPageView::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 PdfPageView::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 PdfPageView::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 PdfPageView::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 PdfPageView::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 PdfPageView::goToPage(int page) + + Changes the view to the \a page, if possible. + + \sa PdfPageNavigator::jump(), currentPage + */ + function goToPage(page) { goToLocation(page, Qt.point(0, 0), 0) } + + /*! + \qmlmethod void PdfPageView::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 bool PdfPageView::zoomEnabled + + This property holds whether the user can use the pinch gesture or + Control + mouse wheel to zoom. The default is \c true. + + When the user zooms the page, the size of PdfPageView changes. + */ + property bool zoomEnabled: true + + /*! + \qmlproperty real PdfPageView::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 size PdfPageView::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 PdfPageView::resetScale() + + Sets \l renderScale back to its default value of \c 1. + */ + function resetScale() { + image.sourceSize.width = 0 + image.sourceSize.height = 0 + root.scale = 1 + } + + /*! + \qmlmethod void PdfPageView::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 halfRotation = Math.abs(root.rotation % 180) + image.sourceSize = Qt.size((halfRotation > 45 && halfRotation < 135) ? height : width, 0) + image.centerInSize = Qt.size(width, height) + image.centerOnLoad = true + image.vCenterOnLoad = (halfRotation > 45 && halfRotation < 135) + root.scale = 1 + } + + /*! + \qmlmethod void PdfPageView::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 page rotation: 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 windowAspect = width / height + const halfRotation = Math.abs(root.rotation % 180) + const pagePointSize = document.pagePointSize(pageNavigator.currentPage) + const pageAspect = pagePointSize.height / pagePointSize.width + if (halfRotation > 45 && halfRotation < 135) { + // rotated 90 or 270ยบ + if (windowAspect > pageAspect) { + image.sourceSize = Qt.size(height, 0) + } else { + image.sourceSize = Qt.size(0, width) + } + } else { + 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 + + /*! + \qmlproperty PdfSearchModel PdfPageView::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 PdfPageView::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 PdfPageView::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 PdfPageView::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 + width: image.width + height: image.height + + PdfSelection { + id: selection + document: root.document + page: pageNavigator.currentPage + from: Qt.point(textSelectionDrag.centroid.pressPosition.x / image.pageScale, textSelectionDrag.centroid.pressPosition.y / image.pageScale) + to: 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) + } + + PdfPageNavigator { + id: pageNavigator + onCurrentPageChanged: searchModel.currentPage = currentPage + onCurrentZoomChanged: root.renderScale = currentZoom + + property url documentSource: root.document.source + onDocumentSourceChanged: { + pageNavigator.clear() + root.goToPage(0) + } + } + + PdfPageImage { + id: image + document: root.document + currentFrame: pageNavigator.currentPage + asynchronous: true + fillMode: Image.PreserveAspectFit + property bool centerOnLoad: false + property bool vCenterOnLoad: false + property size centerInSize + property real pageScale: image.paintedWidth / document.pagePointSize(pageNavigator.currentPage).width + function reRenderIfNecessary() { + const newSourceWidth = image.sourceSize.width * root.scale * Screen.devicePixelRatio + const 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(pageNavigator.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: 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) + else + Qt.openUrlExternally(url) + } + } + } + + PinchHandler { + id: pinch + enabled: root.zoomEnabled && root.scale * root.renderScale <= 10 && root.scale * root.renderScale >= 0.1 + 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 + } + WheelHandler { + enabled: pinch.enabled + acceptedModifiers: Qt.ControlModifier + property: "scale" + onActiveChanged: if (!active) image.reRenderIfNecessary() + } + DragHandler { + id: textSelectionDrag + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + target: null + } + TapHandler { + id: tapHandler + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + } +} |