488 lines
18 KiB
QML
488 lines
18 KiB
QML
// 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
|
|
}
|
|
}
|
|
}
|