diff options
Diffstat (limited to 'src/pdfquick')
24 files changed, 3871 insertions, 0 deletions
diff --git a/src/pdfquick/+Material/PdfStyle.qml b/src/pdfquick/+Material/PdfStyle.qml new file mode 100644 index 000000000..0728616a2 --- /dev/null +++ b/src/pdfquick/+Material/PdfStyle.qml @@ -0,0 +1,15 @@ +// 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 +import QtQuick +import QtQuick.Controls.Material + +QtObject { + property SystemPalette palette: SystemPalette { } + function withAlpha(color, alpha) { + return Qt.hsla(color.hslHue, color.hslSaturation, color.hslLightness, alpha) + } + property color selectionColor: withAlpha(palette.highlight, 0.5) + property color pageSearchResultsColor: withAlpha(Qt.lighter(Material.accentColor, 1.5), 0.5) + property color currentSearchResultStrokeColor: Material.accentColor + property real currentSearchResultStrokeWidth: 2 +} diff --git a/src/pdfquick/+Universal/PdfStyle.qml b/src/pdfquick/+Universal/PdfStyle.qml new file mode 100644 index 000000000..4c559f068 --- /dev/null +++ b/src/pdfquick/+Universal/PdfStyle.qml @@ -0,0 +1,15 @@ +// 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 +import QtQuick +import QtQuick.Controls.Universal + +QtObject { + property SystemPalette palette: SystemPalette { } + function withAlpha(color, alpha) { + return Qt.hsla(color.hslHue, color.hslSaturation, color.hslLightness, alpha) + } + property color selectionColor: withAlpha(palette.highlight, 0.5) + property color pageSearchResultsColor: withAlpha(Qt.lighter(Universal.accent, 1.5), 0.5) + property color currentSearchResultStrokeColor: Universal.accent + property real currentSearchResultStrokeWidth: 2 +} diff --git a/src/pdfquick/CMakeLists.txt b/src/pdfquick/CMakeLists.txt new file mode 100644 index 000000000..d57ce04aa --- /dev/null +++ b/src/pdfquick/CMakeLists.txt @@ -0,0 +1,41 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +find_package(Qt6 ${PROJECT_VERSION} CONFIG REQUIRED COMPONENTS Core Gui Qml Quick) + +set(qml_files + "+Material/PdfStyle.qml" + "+Universal/PdfStyle.qml" + "PdfLinkDelegate.qml" + "PdfMultiPageView.qml" + "PdfPageView.qml" + "PdfScrollablePageView.qml" + "PdfStyle.qml" +) + +qt_internal_add_qml_module(PdfQuick + URI "QtQuick.Pdf" + VERSION "${PROJECT_VERSION}" + PAST_MAJOR_VERSIONS 5 + QML_FILES ${qml_files} + DEPENDENCIES QtQuick/auto + SOURCES + qquickpdfbookmarkmodel.cpp qquickpdfbookmarkmodel_p.h + qquickpdfdocument.cpp qquickpdfdocument_p.h + qquickpdflinkmodel.cpp qquickpdflinkmodel_p.h + qquickpdfpagenavigator.cpp qquickpdfpagenavigator_p.h + qquickpdfpageimage.cpp qquickpdfpageimage_p.h + qquickpdfsearchmodel.cpp qquickpdfsearchmodel_p.h + qquickpdfselection.cpp qquickpdfselection_p.h + qtpdfquickglobal_p.h + INCLUDE_DIRECTORIES + ../3rdparty/chromium + PUBLIC_LIBRARIES + Qt::QuickPrivate + Qt::PdfPrivate + Qt::Core + Qt::Gui + Qt::Qml + NO_GENERATE_CPP_EXPORTS +) + diff --git a/src/pdfquick/PdfLinkDelegate.qml b/src/pdfquick/PdfLinkDelegate.qml new file mode 100644 index 000000000..4ac54d161 --- /dev/null +++ b/src/pdfquick/PdfLinkDelegate.qml @@ -0,0 +1,74 @@ +// 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 +import QtQuick +import QtQuick.Controls + +/*! + \qmltype PdfLinkDelegate + \inqmlmodule QtQuick.Pdf + \brief A component to decorate hyperlinks on a PDF page. + + PdfLinkDelegate provides the component that QML-based PDF viewers + instantiate on top of each hyperlink that is found on each PDF page. + + This component does not provide any visual decoration, because often the + hyperlinks will already be formatted in a distinctive way; but when the + mouse cursor hovers, it changes to Qt::PointingHandCursor, and a tooltip + appears after a delay. Clicking emits the goToLocation() signal if the link + is internal, or calls Qt.openUrlExternally() if the link contains a URL. + + \sa PdfPageView, PdfScrollablePageView, PdfMultiPageView +*/ +Item { + id: root + required property var link + required property rect rectangle + required property url url + required property int page + required property point location + required property real zoom + + /*! + \qmlsignal PdfLinkDelegate::tapped(link) + + Emitted on mouse click or touch tap. The \a link argument is an + instance of QPdfLink with information about the hyperlink. + */ + signal tapped(var link) + + /*! + \qmlsignal PdfLinkDelegate::contextMenuRequested(link) + + Emitted on mouse right-click or touch long-press. The \a link argument + is an instance of QPdfLink with information about the hyperlink. + */ + signal contextMenuRequested(var link) + + HoverHandler { + id: linkHH + cursorShape: Qt.PointingHandCursor + } + TapHandler { + gesturePolicy: TapHandler.ReleaseWithinBounds + onTapped: root.tapped(root.link) + } + TapHandler { + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus + acceptedButtons: Qt.RightButton + gesturePolicy: TapHandler.ReleaseWithinBounds + onTapped: root.contextMenuRequested(root.link) + } + TapHandler { + acceptedDevices: PointerDevice.TouchScreen + onLongPressed: root.contextMenuRequested(root.link) + } + ToolTip { + visible: linkHH.hovered + delay: 1000 + property string destFormat: qsTr("Page %1 location %2, %3 zoom %4") + text: root.page >= 0 ? + destFormat.arg(root.page + 1).arg(root.location.x.toFixed(1)) + .arg(root.location.y.toFixed(1)).arg(root.zoom) : + root.url + } +} diff --git a/src/pdfquick/PdfMultiPageView.qml b/src/pdfquick/PdfMultiPageView.qml new file mode 100644 index 000000000..194d7866e --- /dev/null +++ b/src/pdfquick/PdfMultiPageView.qml @@ -0,0 +1,623 @@ +// 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 PdfMultiPageView + \inqmlmodule QtQuick.Pdf + \brief A complete PDF viewer component for scrolling through multiple pages. + + PdfMultiPageView provides a PDF viewer component that offers a user + experience similar to many common PDF viewer applications. It supports + flicking through the pages in the entire document, with narrow gaps between + the page images. + + PdfMultiPageView 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 + \l {PDF Multipage Viewer 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, PdfScrollablePageView, PdfStyle +*/ +Item { + /*! + \qmlproperty PdfDocument PdfMultiPageView::document + + A PdfDocument object with a valid \c source URL is required: + + \snippet multipageview.qml 0 + */ + required property PdfDocument document + + /*! + \qmlproperty PdfDocument PdfMultiPageView::selectedText + + The selected text. + */ + property string selectedText + + /*! + \qmlmethod void PdfMultiPageView::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() { + const currentItem = tableView.itemAtCell(tableView.cellAtPos(root.width / 2, root.height / 2)) + const pdfSelection = currentItem?.selection as PdfSelection + pdfSelection?.selectAll() + } + + /*! + \qmlmethod void PdfMultiPageView::copySelectionToClipboard() + + Copies the selected text (if any) to the + \l {QClipboard::Clipboard}{system clipboard}. + + \sa selectAll() + */ + function copySelectionToClipboard() { + const currentItem = tableView.itemAtCell(tableView.cellAtPos(root.width / 2, root.height / 2)) + const pdfSelection = currentItem?.selection as PdfSelection + console.log(lcMPV, "currentItem", currentItem, "sel", pdfSelection?.text) + pdfSelection?.copyToClipboard() + } + + // -------------------------------- + // page navigation + + /*! + \qmlproperty int PdfMultiPageView::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 PdfMultiPageView::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 PdfMultiPageView::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 PdfMultiPageView::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 PdfMultiPageView::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 PdfMultiPageView::goToPage(int page) + + Scrolls the view to the given \a page number, if possible. + + \sa PdfPageNavigator::jump(), currentPage + */ + function goToPage(page) { + if (page === pageNavigator.currentPage) + return + goToLocation(page, Qt.point(-1, -1), 0) + } + + /*! + \qmlmethod void PdfMultiPageView::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 (tableView.rows === 0) { + // save this request for later + tableView.pendingRow = page + tableView.pendingLocation = location + tableView.pendingZoom = zoom + return + } + if (zoom > 0) { + pageNavigator.jumping = true // don't call pageNavigator.update() because we will jump() instead + root.renderScale = zoom + pageNavigator.jumping = false + } + pageNavigator.jump(page, location, zoom) // actually jump + } + + /*! + \qmlproperty int PdfMultiPageView::currentPageRenderingStatus + + This property holds the \l {QtQuick::Image::status}{rendering status} of + the \l {currentPage}{current page}. + */ + property int currentPageRenderingStatus: Image.Null + + // -------------------------------- + // page scaling + + /*! + \qmlproperty real PdfMultiPageView::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 PdfMultiPageView::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 + + /*! + \qmlmethod void PdfMultiPageView::resetScale() + + Sets \l renderScale back to its default value of \c 1. + */ + function resetScale() { root.renderScale = 1 } + + /*! + \qmlmethod void PdfMultiPageView::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) { + root.renderScale = width / (tableView.rot90 ? tableView.firstPagePointSize.height : tableView.firstPagePointSize.width) + } + + /*! + \qmlmethod void PdfMultiPageView::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 windowAspect = width / height + const pageAspect = tableView.firstPagePointSize.width / tableView.firstPagePointSize.height + if (tableView.rot90) { + if (windowAspect > pageAspect) { + root.renderScale = height / tableView.firstPagePointSize.width + } else { + root.renderScale = width / tableView.firstPagePointSize.height + } + } else { + if (windowAspect > pageAspect) { + root.renderScale = height / tableView.firstPagePointSize.height + } else { + root.renderScale = width / tableView.firstPagePointSize.width + } + } + } + + // -------------------------------- + // text search + + /*! + \qmlproperty PdfSearchModel PdfMultiPageView::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 PdfMultiPageView::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 PdfMultiPageView::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 PdfMultiPageView::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 } + + LoggingCategory { + id: lcMPV + name: "qt.pdf.multipageview" + } + + id: root + PdfStyle { id: style } + TableView { + id: tableView + property bool debug: false + property real minScale: 0.1 + property real maxScale: 10 + property point jumpLocationMargin: Qt.point(10, 10) // px away from viewport edges + anchors.fill: parent + anchors.leftMargin: 2 + model: root.document ? root.document.pageCount : 0 + rowSpacing: 6 + property real rotationNorm: Math.round((360 + (root.pageRotation % 360)) % 360) + property bool rot90: rotationNorm == 90 || rotationNorm == 270 + onRot90Changed: forceLayout() + onHeightChanged: forceLayout() + onWidthChanged: forceLayout() + property size firstPagePointSize: root.document?.status === PdfDocument.Ready ? root.document.pagePointSize(0) : Qt.size(1, 1) + property real pageHolderWidth: Math.max(root.width, ((rot90 ? root.document?.maxPageHeight : root.document?.maxPageWidth) ?? 0) * root.renderScale) + columnWidthProvider: function(col) { return root.document ? pageHolderWidth + vscroll.width + 2 : 0 } + rowHeightProvider: function(row) { return (rot90 ? root.document.pagePointSize(row).width : root.document.pagePointSize(row).height) * root.renderScale } + + // delayed-jump feature in case the user called goToPage() or goToLocation() too early + property int pendingRow: -1 + property point pendingLocation + property real pendingZoom: -1 + onRowsChanged: { + if (rows > 0 && tableView.pendingRow >= 0) { + console.log(lcMPV, "initiating delayed jump to page", tableView.pendingRow, "loc", tableView.pendingLocation, "zoom", tableView.pendingZoom) + root.goToLocation(tableView.pendingRow, tableView.pendingLocation, tableView.pendingZoom) + tableView.pendingRow = -1 + tableView.pendingLocation = Qt.point(-1, -1) + tableView.pendingZoom = -1 + } + } + + delegate: Rectangle { + id: pageHolder + required property int index + color: tableView.debug ? "beige" : "transparent" + Text { + visible: tableView.debug + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + rotation: -90; text: pageHolder.width.toFixed(1) + "x" + pageHolder.height.toFixed(1) + "\n" + + image.width.toFixed(1) + "x" + image.height.toFixed(1) + } + property alias selection: selection + Rectangle { + id: paper + width: image.width + height: image.height + rotation: root.pageRotation + anchors.centerIn: pinch.active ? undefined : parent + property size pagePointSize: root.document.pagePointSize(pageHolder.index) + property real pageScale: image.paintedWidth / pagePointSize.width + PdfPageImage { + id: image + document: root.document + currentFrame: pageHolder.index + asynchronous: true + fillMode: Image.PreserveAspectFit + width: paper.pagePointSize.width * root.renderScale + height: paper.pagePointSize.height * root.renderScale + property real renderScale: root.renderScale + property real oldRenderScale: 1 + onRenderScaleChanged: { + image.sourceSize.width = paper.pagePointSize.width * renderScale * Screen.devicePixelRatio + image.sourceSize.height = 0 + paper.scale = 1 + searchHighlights.update() + } + onStatusChanged: { + if (pageHolder.index === pageNavigator.currentPage) + root.currentPageRenderingStatus = status + } + } + Shape { + anchors.fill: parent + visible: image.status === Image.Ready + onVisibleChanged: searchHighlights.update() + ShapePath { + strokeWidth: -1 + fillColor: style.pageSearchResultsColor + scale: Qt.size(paper.pageScale, paper.pageScale) + PathMultiline { + id: searchHighlights + function update() { + // paths could be a binding, but we need to be able to "kick" it sometimes + paths = searchModel.boundingPolygonsOnPage(pageHolder.index) + } + } + } + Connections { + target: searchModel + // whenever the highlights on the _current_ page change, they actually need to change on _all_ pages + // (usually because the search string has changed) + function onCurrentPageBoundingPolygonsChanged() { searchHighlights.update() } + } + ShapePath { + strokeWidth: -1 + fillColor: style.selectionColor + scale: Qt.size(paper.pageScale, paper.pageScale) + PathMultiline { + paths: selection.geometry + } + } + } + Shape { + anchors.fill: parent + visible: image.status === Image.Ready && searchModel.currentPage === pageHolder.index + ShapePath { + strokeWidth: style.currentSearchResultStrokeWidth + strokeColor: style.currentSearchResultStrokeColor + fillColor: "transparent" + scale: Qt.size(paper.pageScale, paper.pageScale) + PathMultiline { + paths: searchModel.currentResultBoundingPolygons + } + } + } + PinchHandler { + id: pinch + minimumScale: tableView.minScale / root.renderScale + maximumScale: Math.max(1, tableView.maxScale / root.renderScale) + minimumRotation: root.pageRotation + maximumRotation: root.pageRotation + onActiveChanged: + if (active) { + paper.z = 10 + } else { + paper.z = 0 + const centroidInPoints = Qt.point(pinch.centroid.position.x / root.renderScale, + pinch.centroid.position.y / root.renderScale) + const centroidInFlickable = tableView.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(lcMPV, "pinch ended on page", pageHolder.index, + "with scale", paper.scale.toFixed(3), "ratio", ratio.toFixed(3), + "centroid", pinch.centroid.position, centroidInPoints, + "wrt flickable", centroidInFlickable, + "page at", pageHolder.x.toFixed(2), pageHolder.y.toFixed(2), + "contentX/Y were", tableView.contentX.toFixed(2), tableView.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 + pinch.persistentScale = 1 + paper.x = 0 + paper.y = 0 + root.renderScale *= ratio + tableView.forceLayout() + if (tableView.rotationNorm == 0) { + tableView.contentX = pageHolder.x + tableView.originX + centroidOnPage.x - centroidInFlickable.x + tableView.contentY = pageHolder.y + tableView.originY + centroidOnPage.y - centroidInFlickable.y + } else if (tableView.rotationNorm == 90) { + tableView.contentX = pageHolder.x + tableView.originX + image.height - centroidOnPage.y - centroidInFlickable.x + tableView.contentY = pageHolder.y + tableView.originY + centroidOnPage.x - centroidInFlickable.y + } else if (tableView.rotationNorm == 180) { + tableView.contentX = pageHolder.x + tableView.originX + image.width - centroidOnPage.x - centroidInFlickable.x + tableView.contentY = pageHolder.y + tableView.originY + image.height - centroidOnPage.y - centroidInFlickable.y + } else if (tableView.rotationNorm == 270) { + tableView.contentX = pageHolder.x + tableView.originX + centroidOnPage.y - centroidInFlickable.x + tableView.contentY = pageHolder.y + tableView.originY + image.width - centroidOnPage.x - centroidInFlickable.y + } + console.log(lcMPV, "contentX/Y adjusted to", tableView.contentX.toFixed(2), tableView.contentY.toFixed(2), "y @top", pageHolder.y) + tableView.returnToBounds() + } + } + grabPermissions: PointerHandler.CanTakeOverFromAnything + } + 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.forceActiveFocus() + } + } + Repeater { + model: PdfLinkModel { + id: linkModel + document: root.document + page: image.currentFrame + } + delegate: PdfLinkDelegate { + x: rectangle.x * paper.pageScale + y: rectangle.y * paper.pageScale + width: rectangle.width * paper.pageScale + height: rectangle.height * paper.pageScale + visible: image.status === Image.Ready + onTapped: + (link) => { + if (link.page >= 0) + root.goToLocation(link.page, link.location, link.zoom) + else + Qt.openUrlExternally(url) + } + } + } + PdfSelection { + id: selection + anchors.fill: parent + document: root.document + page: image.currentFrame + renderScale: image.renderScale + from: textSelectionDrag.centroid.pressPosition + to: textSelectionDrag.centroid.position + hold: !textSelectionDrag.active && !mouseClickHandler.pressed + onTextChanged: root.selectedText = text + focus: true + } + } + } + ScrollBar.vertical: ScrollBar { + id: vscroll + property bool moved: false + onPositionChanged: moved = true + onPressedChanged: if (pressed) { + // When the user starts scrolling, push the location where we came from so the user can go "back" there + const cell = tableView.cellAtPos(root.width / 2, root.height / 2) + const currentItem = tableView.itemAtCell(cell) + const currentLocation = currentItem + ? Qt.point((tableView.contentX - currentItem.x + tableView.jumpLocationMargin.x) / root.renderScale, + (tableView.contentY - currentItem.y + tableView.jumpLocationMargin.y) / root.renderScale) + : Qt.point(0, 0) // maybe the delegate wasn't loaded yet + pageNavigator.jump(cell.y, currentLocation, root.renderScale) + } + onActiveChanged: if (!active ) { + // When the scrollbar stops moving, tell navstack where we are, so as to update currentPage etc. + const cell = tableView.cellAtPos(root.width / 2, root.height / 2) + const currentItem = tableView.itemAtCell(cell) + const currentLocation = currentItem + ? Qt.point((tableView.contentX - currentItem.x + tableView.jumpLocationMargin.x) / root.renderScale, + (tableView.contentY - currentItem.y + tableView.jumpLocationMargin.y) / root.renderScale) + : Qt.point(0, 0) // maybe the delegate wasn't loaded yet + pageNavigator.update(cell.y, currentLocation, root.renderScale) + } + } + ScrollBar.horizontal: ScrollBar { } + } + onRenderScaleChanged: { + // if pageNavigator.jumped changes the scale, don't turn around and update the stack again; + // and don't force layout either, because positionViewAtCell() will do that + if (pageNavigator.jumping) + return + // page size changed: TableView needs to redo layout to avoid overlapping delegates or gaps between them + tableView.forceLayout() + const cell = tableView.cellAtPos(root.width / 2, root.height / 2) + const currentItem = tableView.itemAtCell(cell) + if (currentItem) { + const currentLocation = Qt.point((tableView.contentX - currentItem.x + tableView.jumpLocationMargin.x) / root.renderScale, + (tableView.contentY - currentItem.y + tableView.jumpLocationMargin.y) / root.renderScale) + pageNavigator.update(cell.y, currentLocation, renderScale) + } + } + PdfPageNavigator { + id: pageNavigator + property bool jumping: false + property int previousPage: 0 + onJumped: function(current) { + jumping = true + if (current.zoom > 0) + root.renderScale = current.zoom + const pageSize = root.document.pagePointSize(current.page) + if (current.location.y < 0) { + // invalid to indicate that a specific location was not needed, + // so attempt to position the new page just as the current page is + const previousPageDelegate = tableView.itemAtCell(0, previousPage) + const currentYOffset = previousPageDelegate + ? tableView.contentY - previousPageDelegate.y + : 0 + tableView.positionViewAtRow(current.page, Qt.AlignTop, currentYOffset) + console.log(lcMPV, "going from page", previousPage, "to", current.page, "offset", currentYOffset, + "ended up @", tableView.contentX.toFixed(1) + ", " + tableView.contentY.toFixed(1)) + } else if (current.rectangles.length > 0) { + // jump to a search result and position the covered area within the viewport + pageSize.width *= root.renderScale + pageSize.height *= root.renderScale + const rectPts = current.rectangles[0] + const rectPx = Qt.rect(rectPts.x * root.renderScale - tableView.jumpLocationMargin.x, + rectPts.y * root.renderScale - tableView.jumpLocationMargin.y, + rectPts.width * root.renderScale + tableView.jumpLocationMargin.x * 2, + rectPts.height * root.renderScale + tableView.jumpLocationMargin.y * 2) + tableView.positionViewAtCell(0, current.page, TableView.Contain, Qt.point(0, 0), rectPx) + console.log(lcMPV, "going to zoom", root.renderScale, "rect", rectPx, "on page", current.page, + "ended up @", tableView.contentX.toFixed(1) + ", " + tableView.contentY.toFixed(1)) + } else { + // jump to a page and position the given location relative to the top-left corner of the viewport + pageSize.width *= root.renderScale + pageSize.height *= root.renderScale + const rectPx = Qt.rect(current.location.x * root.renderScale - tableView.jumpLocationMargin.x, + current.location.y * root.renderScale - tableView.jumpLocationMargin.y, + tableView.jumpLocationMargin.x * 2, tableView.jumpLocationMargin.y * 2) + tableView.positionViewAtCell(0, current.page, TableView.AlignLeft | TableView.AlignTop, Qt.point(0, 0), rectPx) + console.log(lcMPV, "going to zoom", root.renderScale, "loc", current.location, "on page", current.page, + "ended up @", tableView.contentX.toFixed(1) + ", " + tableView.contentY.toFixed(1)) + } + jumping = false + previousPage = current.page + } + + property url documentSource: root.document.source + onDocumentSourceChanged: { + pageNavigator.clear() + root.resetScale() + tableView.contentX = 0 + tableView.contentY = 0 + } + } + PdfSearchModel { + id: searchModel + document: root.document === undefined ? null : root.document + onCurrentResultChanged: pageNavigator.jump(currentResultLink) + } +} 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 + } +} 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 + } + } +} diff --git a/src/pdfquick/PdfStyle.qml b/src/pdfquick/PdfStyle.qml new file mode 100644 index 000000000..a22276143 --- /dev/null +++ b/src/pdfquick/PdfStyle.qml @@ -0,0 +1,71 @@ +// 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 +import QtQuick + +/*! + \qmltype PdfStyle + \inqmlmodule QtQuick.Pdf + \brief A styling interface for the PDF viewer components. + + PdfStyle provides properties to modify the appearance of PdfMultiPageView, + PdfScrollablePageView, and PdfPageView. + + Default styles are provided to match the + \l {Styling Qt Quick Controls}{styles in Qt Quick Controls}. + \l {Using File Selectors with Qt Quick Controls}{File selectors} + are used to load the PDF style corresponding to the Controls style in use. + Custom styles are possible, using different \l {QFileSelector}{file selectors}. +*/ +QtObject { + /*! \internal + \qmlproperty SystemPalette PdfStyle::palette + */ + property SystemPalette palette: SystemPalette { } + + /*! \internal + \qmlmethod color PdfStyle::withAlpha() + */ + function withAlpha(color, alpha) { + return Qt.hsla(color.hslHue, color.hslSaturation, color.hslLightness, alpha) + } + + /*! + \qmlproperty color PdfStyle::selectionColor + + The color of translucent rectangles that are overlaid on + \l {PdfMultiPageView::selectedText}{selected text}. + + \sa PdfSelection + */ + property color selectionColor: withAlpha(palette.highlight, 0.5) + + /*! + \qmlproperty color PdfStyle::pageSearchResultsColor + + The color of translucent rectangles that are overlaid on text that + matches the \l {PdfMultiPageView::searchString}{search string}. + + \sa PdfSearchModel + */ + property color pageSearchResultsColor: "#80B0C4DE" + + /*! + \qmlproperty color PdfStyle::currentSearchResultStrokeColor + + The color of the box outline around the + \l {PdfSearchModel::currentResult}{current search result}. + + \sa PdfMultiPageView::searchBack(), PdfMultiPageView::searchForward(), PdfSearchModel::currentResult + */ + property color currentSearchResultStrokeColor: "cyan" + + /*! + \qmlproperty real PdfStyle::currentSearchResultStrokeWidth + + The line width of the box outline around the + \l {PdfSearchModel::currentResult}{current search result}. + + \sa PdfMultiPageView::searchBack(), PdfMultiPageView::searchForward(), PdfSearchModel::currentResult + */ + property real currentSearchResultStrokeWidth: 2 +} diff --git a/src/pdfquick/doc/src/qtquickpdf-module.qdoc b/src/pdfquick/doc/src/qtquickpdf-module.qdoc new file mode 100644 index 000000000..a4ca0d9e8 --- /dev/null +++ b/src/pdfquick/doc/src/qtquickpdf-module.qdoc @@ -0,0 +1,18 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! + \qmlmodule QtQuick.Pdf + \title Qt Quick PDF QML Types + \ingroup qmlmodules + \brief Provides QML types for handling PDF documents. + \since 5.14 + + This QML module contains types for handling PDF documents. + + To use the types in this module, import the module with the following line: + + \qml + import QtQuick.Pdf + \endqml +*/ diff --git a/src/pdfquick/qquickpdfbookmarkmodel.cpp b/src/pdfquick/qquickpdfbookmarkmodel.cpp new file mode 100644 index 000000000..81f8547ae --- /dev/null +++ b/src/pdfquick/qquickpdfbookmarkmodel.cpp @@ -0,0 +1,55 @@ +// 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 + +#include "qquickpdfbookmarkmodel_p.h" +#include <QLoggingCategory> + +QT_BEGIN_NAMESPACE + +/*! + \qmltype PdfBookmarkModel +//! \instantiates QQuickPdfBookmarkModel + \inqmlmodule QtQuick.Pdf + \ingroup pdf + \brief A tree of links (anchors) within a PDF document, such as the table of contents. + \since 6.4 + + A PDF document can contain a hierarchy of link destinations, usually + representing the table of contents, to be shown in a sidebar in a PDF + viewer, so that the user can quickly jump to those locations in the + document. This QAbstractItemModel holds the information in a form + suitable for display with TreeView, ListView, QTreeView or QListView. +*/ + +QQuickPdfBookmarkModel::QQuickPdfBookmarkModel(QObject *parent) + : QPdfBookmarkModel(parent) +{ +} + +/*! + \internal +*/ +QQuickPdfBookmarkModel::~QQuickPdfBookmarkModel() = default; + +/*! + \qmlproperty PdfDocument PdfBookmarkModel::document + + This property holds the PDF document in which bookmarks are to be found. +*/ +QQuickPdfDocument *QQuickPdfBookmarkModel::document() const +{ + return m_quickDocument; +} + +void QQuickPdfBookmarkModel::setDocument(QQuickPdfDocument *document) +{ + if (document == m_quickDocument) + return; + m_quickDocument = document; + QPdfBookmarkModel::setDocument(document->document()); + emit documentChanged(); +} + +QT_END_NAMESPACE + +#include "moc_qquickpdfbookmarkmodel_p.cpp" diff --git a/src/pdfquick/qquickpdfbookmarkmodel_p.h b/src/pdfquick/qquickpdfbookmarkmodel_p.h new file mode 100644 index 000000000..1276be058 --- /dev/null +++ b/src/pdfquick/qquickpdfbookmarkmodel_p.h @@ -0,0 +1,53 @@ +// 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 + +#ifndef QQUICKPDFBOOKMARKMODEL_P_H +#define QQUICKPDFBOOKMARKMODEL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtPdfQuick/private/qtpdfquickglobal_p.h> +#include <QtPdfQuick/private/qquickpdfdocument_p.h> +#include <QtPdf/qpdfbookmarkmodel.h> + +#include <QQmlEngine> + +QT_BEGIN_NAMESPACE + +class Q_PDFQUICK_EXPORT QQuickPdfBookmarkModel : public QPdfBookmarkModel +{ + Q_OBJECT + Q_PROPERTY(QQuickPdfDocument* document READ document WRITE setDocument NOTIFY documentChanged) + QML_NAMED_ELEMENT(PdfBookmarkModel) + QML_ADDED_IN_VERSION(6, 4) + +public: + explicit QQuickPdfBookmarkModel(QObject *parent = nullptr); + ~QQuickPdfBookmarkModel() override; + + QQuickPdfDocument *document() const; + void setDocument(QQuickPdfDocument *document); + +Q_SIGNALS: + void documentChanged(); + +private: + QQuickPdfDocument *m_quickDocument; + + Q_DISABLE_COPY(QQuickPdfBookmarkModel) +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickPdfBookmarkModel) + +#endif // QQUICKPDFBOOKMARKMODEL_P_H diff --git a/src/pdfquick/qquickpdfdocument.cpp b/src/pdfquick/qquickpdfdocument.cpp new file mode 100644 index 000000000..9770900db --- /dev/null +++ b/src/pdfquick/qquickpdfdocument.cpp @@ -0,0 +1,271 @@ +// 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 + +#include "qquickpdfdocument_p.h" +#include <private/qpdffile_p.h> +#include <QtCore/qmetatype.h> +#include <QtCore/qstandardpaths.h> +#include <QtQml/qqmlcontext.h> +#include <QtQml/qqmlengine.h> +#include <QtQuick/qquickitem.h> +#include <QtQml/qqmlfile.h> + +QT_BEGIN_NAMESPACE + +/*! + \qmltype PdfDocument +//! \instantiates QQuickPdfDocument + \inqmlmodule QtQuick.Pdf + \ingroup pdf + \brief A representation of a PDF document. + \since 5.15 + + PdfDocument provides access to PDF document meta-information. + It is not necessary for rendering, as it is enough to use an + \l Image with source set to the URL of the PDF. +*/ + +/* + Constructs a PDF document. +*/ +QQuickPdfDocument::QQuickPdfDocument(QObject *parent) + : QObject(parent) +{ +} + +/*! + \internal +*/ +QQuickPdfDocument::~QQuickPdfDocument() +{ + delete m_carrierFile; +}; + +void QQuickPdfDocument::classBegin() +{ + m_doc = static_cast<QPdfDocument *>(qmlExtendedObject(this)); + Q_ASSERT(m_doc); + connect(m_doc, &QPdfDocument::passwordChanged, this, [this]() -> void { + if (resolvedSource().isValid()) + m_doc->load(QQmlFile::urlToLocalFileOrQrc(resolvedSource())); + }); + connect(m_doc, &QPdfDocument::statusChanged, this, [this] (QPdfDocument::Status status) { + emit errorChanged(); + if (status == QPdfDocument::Status::Ready) + emit metaDataChanged(); + }); + if (m_doc->error() == QPdfDocument::Error::IncorrectPassword) + emit m_doc->passwordRequired(); +} + +/*! + \qmlproperty url PdfDocument::source + + This property holds a URL pointing to the PDF file to be loaded. + + \note At this time, only local filesystem URLs are supported. +*/ +void QQuickPdfDocument::setSource(QUrl source) +{ + if (m_source == source) + return; + + m_source = source; + m_maxPageWidthHeight = QSizeF(); + if (m_carrierFile) + m_carrierFile->deleteLater(); + m_carrierFile = nullptr; + emit sourceChanged(); + const QQmlContext *context = qmlContext(this); + m_resolvedSource = context ? context->resolvedUrl(source) : source; + if (m_resolvedSource.isValid()) + m_doc->load(QQmlFile::urlToLocalFileOrQrc(m_resolvedSource)); +} + +/*! + \qmlproperty string PdfDocument::error + + This property holds a translated string representation of the current + error, if any. + + \sa status +*/ +QString QQuickPdfDocument::error() const +{ + switch (m_doc->error()) { + case QPdfDocument::Error::None: + return tr("no error"); + break; + case QPdfDocument::Error::Unknown: + break; + case QPdfDocument::Error::DataNotYetAvailable: + return tr("data not yet available"); + break; + case QPdfDocument::Error::FileNotFound: + return tr("file not found"); + break; + case QPdfDocument::Error::InvalidFileFormat: + return tr("invalid file format"); + break; + case QPdfDocument::Error::IncorrectPassword: + return tr("incorrect password"); + break; + case QPdfDocument::Error::UnsupportedSecurityScheme: + return tr("unsupported security scheme"); + break; + } + return tr("unknown error"); +} + +/*! + \qmlproperty string PdfDocument::password + + This property holds the document password. If the passwordRequired() + signal is emitted, the UI should prompt the user and then set this + property so that document opening can continue. +*/ + +/*! + \qmlproperty int PdfDocument::pageCount + + This property holds the number of pages the PDF contains. +*/ + +/*! + \qmlsignal PdfDocument::passwordRequired() + + This signal is emitted when the PDF requires a password in order to open. + The UI in a typical PDF viewer should prompt the user for the password + and then set the password property when the user has provided it. +*/ + +/*! + \qmlmethod size PdfDocument::pagePointSize(int page) + + Returns the size of the given \a page in points. +*/ + +qreal QQuickPdfDocument::maxPageWidth() const +{ + updateMaxPageSize(); + return m_maxPageWidthHeight.width(); +} + +qreal QQuickPdfDocument::maxPageHeight() const +{ + updateMaxPageSize(); + return m_maxPageWidthHeight.height(); +} + +QPdfDocument *QQuickPdfDocument::document() const +{ + return m_doc; +} + +/*! + \internal + Returns a QPdfFile instance that can carry this document down into + QPdfIOHandler::load(QIODevice *). It should not be used for other purposes. +*/ +QPdfFile *QQuickPdfDocument::carrierFile() +{ + if (!m_carrierFile) + m_carrierFile = new QPdfFile(m_doc); + return m_carrierFile; +} + +void QQuickPdfDocument::updateMaxPageSize() const +{ + if (m_maxPageWidthHeight.isValid()) + return; + qreal w = 0; + qreal h = 0; + const int count = m_doc->pageCount(); + for (int i = 0; i < count; ++i) { + auto size = m_doc->pagePointSize(i); + w = qMax(w, size.width()); + h = qMax(w, size.height()); + } + m_maxPageWidthHeight = QSizeF(w, h); +} + +/*! + \qmlproperty real PdfDocument::maxPageWidth + + This property holds the width of the widest page in the document, in points. +*/ + +/*! + \qmlproperty real PdfDocument::maxPageHeight + + This property holds the height of the tallest page in the document, in points. +*/ + +/*! + \qmlproperty string PdfDocument::title + + This property holds the document's title. A typical viewer UI can bind this + to the \c Window.title property. +*/ + +/*! + \qmlproperty string PdfDocument::author + + This property holds the name of the person who created the document. +*/ + +/*! + \qmlproperty string PdfDocument::subject + + This property holds the subject of the document. +*/ + +/*! + \qmlproperty string PdfDocument::keywords + + This property holds the keywords associated with the document. +*/ + +/*! + \qmlproperty string PdfDocument::creator + + If the document was converted to PDF from another format, this property + holds the name of the software that created the original document. +*/ + +/*! + \qmlproperty string PdfDocument::producer + + If the document was converted to PDF from another format, this property + holds the name of the software that converted it to PDF. +*/ + +/*! + \qmlproperty date PdfDocument::creationDate + + This property holds the date and time the document was created. +*/ + +/*! + \qmlproperty date PdfDocument::modificationDate + + This property holds the date and time the document was most recently + modified. +*/ + +/*! + \qmlproperty enum PdfDocument::status + + This property tells the current status of the document. The possible values are: + + \value PdfDocument.Null The initial status after the document has been created or after it has been closed. + \value PdfDocument.Loading The status after load() has been called and before the document is fully loaded. + \value PdfDocument.Ready The status when the document is fully loaded and its data can be accessed. + \value PdfDocument.Unloading The status after close() has been called on an open document. + At this point the document is still valid and all its data can be accessed. + \value PdfDocument.Error The status after Loading, if loading has failed. +*/ + +QT_END_NAMESPACE + +#include "moc_qquickpdfdocument_p.cpp" diff --git a/src/pdfquick/qquickpdfdocument_p.h b/src/pdfquick/qquickpdfdocument_p.h new file mode 100644 index 000000000..95ded7f8b --- /dev/null +++ b/src/pdfquick/qquickpdfdocument_p.h @@ -0,0 +1,105 @@ +// 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 + +#ifndef QQUICKPDFDOCUMENT_P_H +#define QQUICKPDFDOCUMENT_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtPdfQuick/private/qtpdfquickglobal_p.h> +#include <QtPdf/QPdfDocument> + +#include <QtQml/QQmlEngine> +#include <QtQml/QQmlParserStatus> +#include <QtCore/QDateTime> +#include <QtCore/QUrl> + +QT_BEGIN_NAMESPACE + +class QPdfFile; + +class Q_PDFQUICK_EXPORT QQuickPdfDocument : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged FINAL) + Q_PROPERTY(qreal maxPageWidth READ maxPageWidth NOTIFY metaDataChanged FINAL) + Q_PROPERTY(qreal maxPageHeight READ maxPageHeight NOTIFY metaDataChanged FINAL) + Q_PROPERTY(QString error READ error NOTIFY errorChanged FINAL) + + Q_PROPERTY(QString title READ title NOTIFY metaDataChanged FINAL) + Q_PROPERTY(QString subject READ subject NOTIFY metaDataChanged FINAL) + Q_PROPERTY(QString author READ author NOTIFY metaDataChanged FINAL) + Q_PROPERTY(QString keywords READ keywords NOTIFY metaDataChanged FINAL) + Q_PROPERTY(QString producer READ producer NOTIFY metaDataChanged FINAL) + Q_PROPERTY(QString creator READ creator NOTIFY metaDataChanged FINAL) + Q_PROPERTY(QDateTime creationDate READ creationDate NOTIFY metaDataChanged FINAL) + Q_PROPERTY(QDateTime modificationDate READ modificationDate NOTIFY metaDataChanged FINAL) + QML_NAMED_ELEMENT(PdfDocument) + QML_EXTENDED(QPdfDocument) + QML_ADDED_IN_VERSION(5, 15) + +public: + explicit QQuickPdfDocument(QObject *parent = nullptr); + ~QQuickPdfDocument() override; + + void classBegin() override; + void componentComplete() override {} + + QUrl source() const { return m_source; } + void setSource(QUrl source); + QUrl resolvedSource() const { return m_resolvedSource; } + + QString error() const; + + QString title() { return m_doc->metaData(QPdfDocument::MetaDataField::Title).toString(); } + QString author() { return m_doc->metaData(QPdfDocument::MetaDataField::Author).toString(); } + QString subject() { return m_doc->metaData(QPdfDocument::MetaDataField::Subject).toString(); } + QString keywords() { return m_doc->metaData(QPdfDocument::MetaDataField::Keywords).toString(); } + QString producer() { return m_doc->metaData(QPdfDocument::MetaDataField::Producer).toString(); } + QString creator() { return m_doc->metaData(QPdfDocument::MetaDataField::Creator).toString(); } + QDateTime creationDate() { return m_doc->metaData(QPdfDocument::MetaDataField::CreationDate).toDateTime(); } + QDateTime modificationDate() { return m_doc->metaData(QPdfDocument::MetaDataField::ModificationDate).toDateTime(); } + + qreal maxPageWidth() const; + qreal maxPageHeight() const; + +Q_SIGNALS: + void sourceChanged(); + void errorChanged(); + void metaDataChanged(); + +private: + QPdfDocument *document() const; + QPdfFile *carrierFile(); + void updateMaxPageSize() const; + +private: + QUrl m_source; + QUrl m_resolvedSource; + QPdfDocument *m_doc = nullptr; + QPdfFile *m_carrierFile = nullptr; + mutable QSizeF m_maxPageWidthHeight; + + friend class QQuickPdfBookmarkModel; + friend class QQuickPdfLinkModel; + friend class QQuickPdfPageImage; + friend class QQuickPdfSearchModel; + friend class QQuickPdfSelection; + + Q_DISABLE_COPY(QQuickPdfDocument) +}; + +QT_END_NAMESPACE + +#endif // QQUICKPDFDOCUMENT_P_H diff --git a/src/pdfquick/qquickpdflinkmodel.cpp b/src/pdfquick/qquickpdflinkmodel.cpp new file mode 100644 index 000000000..469d13faf --- /dev/null +++ b/src/pdfquick/qquickpdflinkmodel.cpp @@ -0,0 +1,109 @@ +// Copyright (C) 2020 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 + +#include "qquickpdflinkmodel_p.h" +#include <QQuickItem> +#include <QQmlEngine> +#include <QStandardPaths> + +QT_BEGIN_NAMESPACE + +/*! + \qmltype PdfLinkModel +//! \instantiates QQuickPdfLinkModel + \inqmlmodule QtQuick.Pdf + \ingroup pdf + \brief A representation of links within a PDF document. + \since 5.15 + + PdfLinkModel provides the geometry and the destination for each link + that the specified \l page contains. + + The available model roles are: + + \value rectangle + Bounding rectangle around the link. + \value url + If the link is a web link, the URL for that; otherwise an empty URL. + \value page + If the link is an internal link, the page number to which the link should jump; otherwise \c {-1}. + \value location + If the link is an internal link, the location on the page to which the link should jump. + \value zoom + If the link is an internal link, the intended zoom level on the destination page. + + Normally it will be used with \l {QtQuick::Repeater}{Repeater} to visualize + the links and provide the ability to click them: + + \qml + Repeater { + model: PdfLinkModel { + document: root.document + page: image.currentFrame + } + delegate: Rectangle { + required property rect rectangle + required property url url + required property int page + color: "transparent" + border.color: "lightgrey" + x: rectangle.x + y: rectangle.y + width: rectangle.width + height: rectangle.height + HoverHandler { cursorShape: Qt.PointingHandCursor } + TapHandler { + onTapped: { + if (page >= 0) + image.currentFrame = page + else + Qt.openUrlExternally(url) + } + } + } + } + \endqml + + \note General-purpose PDF viewing capabilities are provided by + \c PdfScrollablePageView and \c PdfMultiPageView. PdfLinkModel is only needed + when building PDF view components from scratch. +*/ + +QQuickPdfLinkModel::QQuickPdfLinkModel(QObject *parent) + : QPdfLinkModel(parent) +{ +} + +/*! + \internal +*/ +QQuickPdfLinkModel::~QQuickPdfLinkModel() = default; + +/*! + \qmlproperty PdfDocument PdfLinkModel::document + + This property holds the PDF document in which links are to be found. +*/ +QQuickPdfDocument *QQuickPdfLinkModel::document() const +{ + return m_quickDocument; +} + +void QQuickPdfLinkModel::setDocument(QQuickPdfDocument *document) +{ + if (document == m_quickDocument) + return; + m_quickDocument = document; + if (document) + QPdfLinkModel::setDocument(document->document()); +} + +/*! + \qmlproperty int PdfLinkModel::page + + This property holds the page number on which links are to be found. +*/ + +QT_END_NAMESPACE + +#include "moc_qquickpdflinkmodel_p.cpp" diff --git a/src/pdfquick/qquickpdflinkmodel_p.h b/src/pdfquick/qquickpdflinkmodel_p.h new file mode 100644 index 000000000..44314b2b1 --- /dev/null +++ b/src/pdfquick/qquickpdflinkmodel_p.h @@ -0,0 +1,49 @@ +// 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 + +#ifndef QQUICKPDFLINKMODEL_P_H +#define QQUICKPDFLINKMODEL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtPdfQuick/private/qtpdfquickglobal_p.h> +#include <QtPdfQuick/private/qquickpdfdocument_p.h> +#include <QtPdf/private/qpdflinkmodel_p.h> + +#include <QtQml/QQmlEngine> + +QT_BEGIN_NAMESPACE + +class Q_PDFQUICK_EXPORT QQuickPdfLinkModel : public QPdfLinkModel +{ + Q_OBJECT + Q_PROPERTY(QQuickPdfDocument *document READ document WRITE setDocument NOTIFY documentChanged) + QML_NAMED_ELEMENT(PdfLinkModel) + QML_ADDED_IN_VERSION(5, 15) + +public: + explicit QQuickPdfLinkModel(QObject *parent = nullptr); + ~QQuickPdfLinkModel() override; + + QQuickPdfDocument *document() const; + void setDocument(QQuickPdfDocument *document); + +private: + QQuickPdfDocument *m_quickDocument; + Q_DISABLE_COPY(QQuickPdfLinkModel) +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickPdfLinkModel) + +#endif // QQUICKPDFLINKMODEL_P_H diff --git a/src/pdfquick/qquickpdfpageimage.cpp b/src/pdfquick/qquickpdfpageimage.cpp new file mode 100644 index 000000000..9ff0337a5 --- /dev/null +++ b/src/pdfquick/qquickpdfpageimage.cpp @@ -0,0 +1,143 @@ +// 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 + +#include "qquickpdfpageimage_p.h" +#include "qquickpdfdocument_p.h" +#include <private/qpdffile_p.h> +#include <QtQuick/private/qquickimage_p_p.h> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(qLcImg, "qt.pdf.image") + +/*! + \qmltype PdfPageImage +//! \instantiates QQuickPdfPageImage + \inqmlmodule QtQuick.Pdf + \ingroup pdf + \inherits Image + \brief Displays one page from a PDF document. + \since 6.4 + + The PdfPageImage type is an Image specialized to render a page from a PDF document. +*/ + +class QQuickPdfPageImagePrivate: public QQuickImagePrivate +{ +public: + QQuickPdfPageImagePrivate() : QQuickImagePrivate() {} + + QQuickPdfDocument *doc = nullptr; +}; + +QQuickPdfPageImage::QQuickPdfPageImage(QQuickItem *parent) + : QQuickImage(*(new QQuickPdfPageImagePrivate), parent) +{ +} + +/*! + \internal +*/ +QQuickPdfPageImage::~QQuickPdfPageImage() +{ + Q_D(QQuickPdfPageImage); + // cancel any async rendering job that is running on my behalf + d->pendingPix->clear(); +} + +/*! + \qmlproperty PdfDocument PdfPageImage::document + + This property holds the PDF document from which to render an image. +*/ +void QQuickPdfPageImage::setDocument(QQuickPdfDocument *document) +{ + Q_D(QQuickPdfPageImage); + if (d->doc == document) + return; + + if (d->doc) + disconnect(d->doc->document(), &QPdfDocument::statusChanged, this, &QQuickPdfPageImage::documentStatusChanged); + d->doc = document; + if (document) { + connect(document->document(), &QPdfDocument::statusChanged, this, &QQuickPdfPageImage::documentStatusChanged); + if (document->document()->status() == QPdfDocument::Status::Ready) + setSource(document->resolvedSource()); // calls load() + } + emit documentChanged(); +} + +QQuickPdfDocument *QQuickPdfPageImage::document() const +{ + Q_D(const QQuickPdfPageImage); + return d->doc; +} + +void QQuickPdfPageImage::load() +{ + Q_D(QQuickPdfPageImage); + QUrl url = source(); + if (!d->doc || !d->doc->carrierFile()) { + if (!url.isEmpty()) { + qmlWarning(this) << "document property not set: falling back to inefficient loading of " << url; + QQuickImageBase::load(); + } + return; + } + if (url != d->doc->resolvedSource()) { + url = d->doc->resolvedSource(); + qmlWarning(this) << "document and source properties in conflict: preferring document source " << url; + } + auto carrierFile = d->doc->carrierFile(); + static int thisRequestProgress = -1; + static int thisRequestFinished = -1; + if (thisRequestProgress == -1) { + thisRequestProgress = + QQuickImageBase::staticMetaObject.indexOfSlot("requestProgress(qint64,qint64)"); + thisRequestFinished = + QQuickImageBase::staticMetaObject.indexOfSlot("requestFinished()"); + } + static QMetaMethod requestFinishedSlot = staticMetaObject.method(thisRequestFinished); + + d->pendingPix->loadImageFromDevice(qmlEngine(this), carrierFile, url, + d->sourceClipRect.toRect(), d->sourcesize * d->devicePixelRatio, + QQuickImageProviderOptions(), d->currentFrame, d->frameCount); + + qCDebug(qLcImg) << "loading page" << d->currentFrame << "of" << d->frameCount + << "from" << carrierFile->fileName() << "status" << d->pendingPix->status(); + + switch (d->pendingPix->status()) { + case QQuickPixmap::Ready: + requestFinishedSlot.invoke(this); + pixmapChange(); + break; + case QQuickPixmap::Loading: + d->pendingPix->connectFinished(this, thisRequestFinished); + d->pendingPix->connectDownloadProgress(this, thisRequestProgress); + if (d->progress != 0.0) { + d->progress = 0.0; + emit progressChanged(d->progress); + } + if (d->status != Loading) { + d->status = Loading; + emit statusChanged(d->status); + } + break; + default: + qCDebug(qLcImg) << "unexpected status" << d->pendingPix->status(); + break; + } +} + +void QQuickPdfPageImage::documentStatusChanged() +{ + Q_D(QQuickPdfPageImage); + const auto status = d->doc->document()->status(); + qCDebug(qLcImg) << "document status" << status; + if (status == QPdfDocument::Status::Ready) + setSource(d->doc->resolvedSource()); // calls load() +} + +QT_END_NAMESPACE + +#include "moc_qquickpdfpageimage_p.cpp" diff --git a/src/pdfquick/qquickpdfpageimage_p.h b/src/pdfquick/qquickpdfpageimage_p.h new file mode 100644 index 000000000..daff052ac --- /dev/null +++ b/src/pdfquick/qquickpdfpageimage_p.h @@ -0,0 +1,52 @@ +// 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 + +#ifndef QQUICKPDFPAGEIMAGE_P_H +#define QQUICKPDFPAGEIMAGE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtPdfQuick/private/qtpdfquickglobal_p.h> +#include <QtQuick/private/qquickimage_p.h> + +QT_BEGIN_NAMESPACE + +class QQuickPdfDocument; +class QQuickPdfPageImagePrivate; +class Q_PDFQUICK_EXPORT QQuickPdfPageImage : public QQuickImage +{ + Q_OBJECT + Q_PROPERTY(QQuickPdfDocument* document READ document WRITE setDocument NOTIFY documentChanged FINAL) + QML_NAMED_ELEMENT(PdfPageImage) + QML_ADDED_IN_VERSION(6, 4) + +public: + QQuickPdfPageImage(QQuickItem *parent = nullptr); + ~QQuickPdfPageImage() override; + + void setDocument(QQuickPdfDocument *document); + QQuickPdfDocument *document() const; + +signals: + void documentChanged(); + +protected: + void load() override; + void documentStatusChanged(); + +private: + Q_DECLARE_PRIVATE(QQuickPdfPageImage) +}; + +QT_END_NAMESPACE + +#endif // QQUICKPDFPAGEIMAGE_P_H diff --git a/src/pdfquick/qquickpdfpagenavigator.cpp b/src/pdfquick/qquickpdfpagenavigator.cpp new file mode 100644 index 000000000..939d928e9 --- /dev/null +++ b/src/pdfquick/qquickpdfpagenavigator.cpp @@ -0,0 +1,130 @@ +// 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 + +#include "qquickpdfpagenavigator_p.h" +#include <QLoggingCategory> + +QT_BEGIN_NAMESPACE + +/*! + \qmltype PdfPageNavigator +//! \instantiates QQuickPdfPageNavigator + \inqmlmodule QtQuick.Pdf + \ingroup pdf + \brief History of the destinations visited within a PDF Document. + \since 5.15 + + PdfPageNavigator remembers which destinations the user has visited in a PDF + document, and provides the ability to traverse backward and forward. +*/ + +QQuickPdfPageNavigator::QQuickPdfPageNavigator(QObject *parent) + : QObject(parent) +{ +} + +/*! + \internal +*/ +QQuickPdfPageNavigator::~QQuickPdfPageNavigator() = default; + +/*! + \internal +*/ +QPdfPageNavigator *QQuickPdfPageNavigator::navStack() +{ + return static_cast<QPdfPageNavigator *>(qmlExtendedObject(this)); +} + +/*! + \qmlmethod void PdfPageNavigator::forward() + + Goes back to the page, location and zoom level that was being viewed before + back() was called, and then emits the \l jumped() signal. + + If a new destination was pushed since the last time \l back() was called, + the forward() function does nothing, because there is a branch in the + timeline which causes the "future" to be lost. +*/ + +/*! + \qmlmethod void PdfPageNavigator::back() + + Pops the stack, updates the \l currentPage, \l currentLocation and + \l currentZoom properties to the most-recently-viewed destination, and then + emits the \l jumped() signal. +*/ + +/*! + \qmlproperty int PdfPageNavigator::currentPage + + This property holds the current page that is being viewed. + If there is no current page, it holds \c -1. +*/ + +/*! + \qmlproperty point PdfPageNavigator::currentLocation + + This property holds the current location on the page that is being viewed. +*/ + +/*! + \qmlproperty real PdfPageNavigator::currentZoom + + This property holds the magnification scale on the page that is being viewed. +*/ + +/*! + \qmlmethod void PdfPageNavigator::jump(int page, point location, qreal zoom, bool emitJumped) + + Adds the given destination, consisting of \a page, \a location, and \a zoom, + to the history of visited locations. If \a emitJumped is \c false, the + \l jumped() signal will not be emitted. + + If forwardAvailable is \c true, calling this function represents a branch + in the timeline which causes the "future" to be lost, and therefore + forwardAvailable will change to \c false. +*/ + +/*! + \qmlmethod void PdfPageNavigator::update(int page, point location, qreal zoom) + + Modifies the current destination, consisting of \a page, \a location and \a zoom. + + This can be called periodically while the user is manually moving around + the document, so that after back() is called, forward() will jump back to + the most-recently-viewed destination rather than the destination that was + last specified by jump(). + + The \c currentZoomChanged, \c currentPageChanged and \c currentLocationChanged + signals will be emitted if the respective properties are actually changed. + The \l jumped signal is not emitted, because this operation + represents smooth movement rather than a navigational jump. +*/ + +/*! + \qmlproperty bool PdfPageNavigator::backAvailable + \readonly + + Holds \c true if a \e back destination is available in the history. +*/ + +/*! + \qmlproperty bool PdfPageNavigator::forwardAvailable + \readonly + + Holds \c true if a \e forward destination is available in the history. +*/ + +/*! + \qmlsignal PdfPageNavigator::jumped(int page, point location, qreal zoom) + + This signal is emitted when an abrupt jump occurs, to the specified \a page + index, \a location on the page, and \a zoom level; but \e not when simply + scrolling through the document one page at a time. That is, forward(), + back() and jump() always emit this signal; update() does not. +*/ + +QT_END_NAMESPACE + +#include "moc_qquickpdfpagenavigator_p.cpp" diff --git a/src/pdfquick/qquickpdfpagenavigator_p.h b/src/pdfquick/qquickpdfpagenavigator_p.h new file mode 100644 index 000000000..2a2d92d6b --- /dev/null +++ b/src/pdfquick/qquickpdfpagenavigator_p.h @@ -0,0 +1,55 @@ +// 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 + +#ifndef QQUICKPDFPAGENAVIGATOR_P_H +#define QQUICKPDFPAGENAVIGATOR_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtPdfQuick/private/qtpdfquickglobal_p.h> +#include <QtPdf/qpdfpagenavigator.h> +#include <QtPdf/private/qpdflink_p.h> + +#include <QQmlEngine> + +QT_BEGIN_NAMESPACE + +struct Q_PDFQUICK_EXPORT QPdfLinkForeign +{ + Q_GADGET + QML_FOREIGN(QPdfLink) + QML_VALUE_TYPE(pdfLink) + QML_ADDED_IN_VERSION(6, 4) +}; + +class Q_PDFQUICK_EXPORT QQuickPdfPageNavigator : public QObject +{ + Q_OBJECT + QML_EXTENDED(QPdfPageNavigator) + QML_NAMED_ELEMENT(PdfPageNavigator) + QML_ADDED_IN_VERSION(5, 15) + +public: + explicit QQuickPdfPageNavigator(QObject *parent = nullptr); + ~QQuickPdfPageNavigator() override; + +private: + QPdfPageNavigator *navStack(); + + Q_DISABLE_COPY(QQuickPdfPageNavigator) +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickPdfPageNavigator) + +#endif // QQUICKPDFPAGENAVIGATOR_P_H diff --git a/src/pdfquick/qquickpdfsearchmodel.cpp b/src/pdfquick/qquickpdfsearchmodel.cpp new file mode 100644 index 000000000..896584ad7 --- /dev/null +++ b/src/pdfquick/qquickpdfsearchmodel.cpp @@ -0,0 +1,282 @@ +// Copyright (C) 2020 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 + +#include "qquickpdfsearchmodel_p.h" +#include <QtCore/qloggingcategory.h> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(qLcSearch, "qt.pdf.search") + +/*! + \qmltype PdfSearchModel +//! \instantiates QQuickPdfSearchModel + \inqmlmodule QtQuick.Pdf + \ingroup pdf + \brief A representation of text search results within a PDF Document. + \since 5.15 + + PdfSearchModel provides the ability to search for text strings within a + document and get the geometric locations of matches on each page. +*/ + +QQuickPdfSearchModel::QQuickPdfSearchModel(QObject *parent) + : QPdfSearchModel(parent) +{ + connect(this, &QPdfSearchModel::searchStringChanged, + this, &QQuickPdfSearchModel::onResultsChanged); +} + +/*! + \internal +*/ +QQuickPdfSearchModel::~QQuickPdfSearchModel() = default; + +QQuickPdfDocument *QQuickPdfSearchModel::document() const +{ + return m_quickDocument; +} + +void QQuickPdfSearchModel::setDocument(QQuickPdfDocument *document) +{ + if (document == m_quickDocument || !document) + return; + + m_quickDocument = document; + QPdfSearchModel::setDocument(document->document()); +} + +/*! + \qmlproperty list<list<point>> PdfSearchModel::currentResultBoundingPolygons + + A set of paths in a form that can be bound to the \c paths property of a + \l {QtQuick::PathMultiline}{PathMultiline} instance to render a batch of + rectangles around the regions comprising the search result \l currentResult + on \l currentPage. This is normally used to highlight one search result + at a time, in a UI that allows stepping through the results: + + \qml + PdfDocument { + id: doc + } + PdfSearchModel { + id: searchModel + document: doc + currentPage: view.currentPage + currentResult: ... + } + Shape { + ShapePath { + PathMultiline { + paths: searchModel.currentResultBoundingPolygons + } + } + } + \endqml + + It becomes empty whenever \c {currentPage != currentResultLink.page}. + + \sa PathMultiline +*/ +QList<QPolygonF> QQuickPdfSearchModel::currentResultBoundingPolygons() const +{ + QList<QPolygonF> ret; + const auto result = currentResultLink(); + if (result.page() != m_currentPage) + return ret; + for (auto rect : result.rectangles()) + ret << QPolygonF(rect); + return ret; +} + +/*! + \qmlproperty point PdfSearchModel::currentResultBoundingRect + + The bounding box containing all \l currentResultBoundingPolygons, + if \c {currentPage == currentResultLink.page}; otherwise, an invalid rectangle. +*/ +QRectF QQuickPdfSearchModel::currentResultBoundingRect() const +{ + QRectF ret; + const auto result = currentResultLink(); + if (result.page() != m_currentPage) + return ret; + auto rects = result.rectangles(); + if (!rects.isEmpty()) { + ret = rects.takeFirst(); + for (auto rect : rects) + ret = ret.united(rect); + } + return ret; +} + +void QQuickPdfSearchModel::onResultsChanged() +{ + emit currentPageBoundingPolygonsChanged(); + emit currentResultBoundingPolygonsChanged(); +} + +/*! + \qmlproperty list<list<point>> PdfSearchModel::currentPageBoundingPolygons + + A set of paths in a form that can be bound to the \c paths property of a + \l {QtQuick::PathMultiline}{PathMultiline} instance to render a batch of + rectangles around all the regions where search results are found on + \l currentPage: + + \qml + PdfDocument { + id: doc + } + PdfSearchModel { + id: searchModel + document: doc + } + Shape { + ShapePath { + PathMultiline { + paths: searchModel.matchGeometry(view.currentPage) + } + } + } + \endqml + + \sa PathMultiline +*/ +QList<QPolygonF> QQuickPdfSearchModel::currentPageBoundingPolygons() const +{ + return const_cast<QQuickPdfSearchModel *>(this)->boundingPolygonsOnPage(m_currentPage); +} + +/*! + \qmlmethod list<list<point>> PdfSearchModel::boundingPolygonsOnPage(int page) + + Returns a set of paths in a form that can be bound to the \c paths property of a + \l {QtQuick::PathMultiline}{PathMultiline} instance, which is used to render a + batch of rectangles around all the matching locations on the \a page: + + \qml + PdfDocument { + id: doc + } + PdfSearchModel { + id: searchModel + document: doc + } + Shape { + ShapePath { + PathMultiline { + paths: searchModel.matchGeometry(view.currentPage) + } + } + } + \endqml + + \sa PathMultiline +*/ +QList<QPolygonF> QQuickPdfSearchModel::boundingPolygonsOnPage(int page) +{ + if (!document() || searchString().isEmpty() || page < 0 || page > document()->document()->pageCount()) + return {}; + + updatePage(page); + + QList<QPolygonF> ret; + const auto m = QPdfSearchModel::resultsOnPage(page); + for (const auto &result : m) { + for (const auto &rect : result.rectangles()) + ret << QPolygonF(rect); + } + + return ret; +} + +/*! + \qmlproperty int PdfSearchModel::currentPage + + The page on which \l currentResultBoundingPolygons should provide filtered + search results. +*/ +void QQuickPdfSearchModel::setCurrentPage(int currentPage) +{ + if (m_currentPage == currentPage || !document()) + return; + + const auto pageCount = document()->document()->pageCount(); + if (currentPage < 0) + currentPage = pageCount - 1; + else if (currentPage >= pageCount) + currentPage = 0; + + m_currentPage = currentPage; + if (!m_suspendSignals) { + emit currentPageChanged(); + onResultsChanged(); + } +} + +/*! + \qmlproperty int PdfSearchModel::currentResult + + The result index within the whole set of search results, for which + \l currentResultBoundingPolygons should provide the regions to highlight + if currentPage matches \c currentResultLink.page. +*/ +void QQuickPdfSearchModel::setCurrentResult(int currentResult) +{ + if (m_currentResult == currentResult) + return; + + const int currentResultWas = m_currentResult; + const int currentPageWas = m_currentPage; + const int resultCount = rowCount({}); + + // wrap around at the ends + if (currentResult >= resultCount) { + currentResult = 0; + } else if (currentResult < 0) { + currentResult = resultCount - 1; + } + + const QPdfLink link = resultAtIndex(currentResult); + if (link.isValid()) { + setCurrentPage(link.page()); + m_currentResult = currentResult; + emit currentResultChanged(); + emit currentResultLinkChanged(); + emit currentResultBoundingPolygonsChanged(); + emit currentResultBoundingRectChanged(); + qCDebug(qLcSearch) << "currentResult was" << currentResultWas + << "requested" << currentResult << "on page" << currentPageWas + << "->" << m_currentResult << "on page" << m_currentPage; + } else { + qWarning() << "failed to find result" << currentResult << "in range 0 ->" << resultCount; + } +} + +/*! + \qmlproperty QPdfLink PdfSearchModel::currentResultLink + + The result at index \l currentResult. +*/ +QPdfLink QQuickPdfSearchModel::currentResultLink() const +{ + return resultAtIndex(m_currentResult); +} + +/*! + \qmlproperty string PdfSearchModel::searchString + + The string to search for. +*/ + +/*! + \since 6.8 + \qmlproperty int PdfSearchModel::count + + The number of search results found. +*/ + +QT_END_NAMESPACE + +#include "moc_qquickpdfsearchmodel_p.cpp" diff --git a/src/pdfquick/qquickpdfsearchmodel_p.h b/src/pdfquick/qquickpdfsearchmodel_p.h new file mode 100644 index 000000000..699cd719f --- /dev/null +++ b/src/pdfquick/qquickpdfsearchmodel_p.h @@ -0,0 +1,85 @@ +// Copyright (C) 2020 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 + +#ifndef QQUICKPDFSEARCHMODEL_P_H +#define QQUICKPDFSEARCHMODEL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtPdfQuick/private/qtpdfquickglobal_p.h> +#include <QtPdfQuick/private/qquickpdfdocument_p.h> + +#include <QtPdf/qpdfsearchmodel.h> +#include <QtQml/QQmlEngine> + +QT_BEGIN_NAMESPACE + +class Q_PDFQUICK_EXPORT QQuickPdfSearchModel : public QPdfSearchModel +{ + Q_OBJECT + Q_PROPERTY(QQuickPdfDocument *document READ document WRITE setDocument NOTIFY documentChanged) + Q_PROPERTY(int currentPage READ currentPage WRITE setCurrentPage NOTIFY currentPageChanged) + Q_PROPERTY(int currentResult READ currentResult WRITE setCurrentResult NOTIFY currentResultChanged) + Q_PROPERTY(QPdfLink currentResultLink READ currentResultLink NOTIFY currentResultLinkChanged) + Q_PROPERTY(QList<QPolygonF> currentPageBoundingPolygons READ currentPageBoundingPolygons NOTIFY currentPageBoundingPolygonsChanged) + Q_PROPERTY(QList<QPolygonF> currentResultBoundingPolygons READ currentResultBoundingPolygons NOTIFY currentResultBoundingPolygonsChanged) + Q_PROPERTY(QRectF currentResultBoundingRect READ currentResultBoundingRect NOTIFY currentResultBoundingRectChanged) + QML_NAMED_ELEMENT(PdfSearchModel) + QML_ADDED_IN_VERSION(5, 15) + +public: + explicit QQuickPdfSearchModel(QObject *parent = nullptr); + ~QQuickPdfSearchModel() override; + + QQuickPdfDocument *document() const; + void setDocument(QQuickPdfDocument * document); + + Q_INVOKABLE QList<QPolygonF> boundingPolygonsOnPage(int page); + + int currentPage() const { return m_currentPage; } + void setCurrentPage(int currentPage); + + int currentResult() const { return m_currentResult; } + void setCurrentResult(int currentResult); + + QPdfLink currentResultLink() const; + QList<QPolygonF> currentPageBoundingPolygons() const; + QList<QPolygonF> currentResultBoundingPolygons() const; + QRectF currentResultBoundingRect() const; + +signals: + void currentPageChanged(); + void currentResultChanged(); + void currentResultLinkChanged(); + void currentPageBoundingPolygonsChanged(); + void currentResultBoundingPolygonsChanged(); + void currentResultBoundingRectChanged(); + +private: + void updateResults(); + void onResultsChanged(); + +private: + QQuickPdfDocument *m_quickDocument = nullptr; + int m_currentPage = 0; + int m_currentResult = 0; + bool m_suspendSignals = false; + + Q_DISABLE_COPY(QQuickPdfSearchModel) +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickPdfSearchModel) +QML_DECLARE_TYPE(QPdfSelection) + +#endif // QQUICKPDFSEARCHMODEL_P_H diff --git a/src/pdfquick/qquickpdfselection.cpp b/src/pdfquick/qquickpdfselection.cpp new file mode 100644 index 000000000..4776cb8b4 --- /dev/null +++ b/src/pdfquick/qquickpdfselection.cpp @@ -0,0 +1,546 @@ +// Copyright (C) 2020 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 + +#include "qquickpdfselection_p.h" +#include "qquickpdfdocument_p.h" +#include <QClipboard> +#include <QGuiApplication> +#include <QLoggingCategory> +#include <QQuickItem> +#include <QQmlEngine> +#include <QRegularExpression> +#include <QStandardPaths> +#include <QtPdf/private/qpdfdocument_p.h> + +Q_LOGGING_CATEGORY(qLcIm, "qt.pdf.im") + +QT_BEGIN_NAMESPACE + +static const QRegularExpression WordDelimiter(QStringLiteral("\\s")); + +/*! + \qmltype PdfSelection +//! \instantiates QQuickPdfSelection + \inqmlmodule QtQuick.Pdf + \ingroup pdf + \inherits Item + \brief A representation of a text selection within a PDF Document. + \since 5.15 + + PdfSelection provides the text string and its geometry within a bounding box + from one point to another. + + To modify the selection using the mouse, bind \l from and \l to + to the suitable properties of an input handler so that they will be set to + the positions where the drag gesture begins and ends, respectively; and + bind the \l hold property so that it will be set to \c true during the drag + gesture and \c false when the gesture ends. + + PdfSelection also directly handles Input Method queries so that text + selection handles can be used on platforms such as iOS. For this purpose, + it must have keyboard focus. +*/ + +QQuickPdfSelection::QQuickPdfSelection(QQuickItem *parent) + : QQuickItem(parent) +{ +#if QT_CONFIG(im) + setFlags(ItemIsFocusScope | ItemAcceptsInputMethod); +#endif +} + +/*! + \internal +*/ +QQuickPdfSelection::~QQuickPdfSelection() = default; + +/*! + \qmlproperty PdfDocument PdfSelection::document + + This property holds the PDF document in which to select text. +*/ +QQuickPdfDocument *QQuickPdfSelection::document() const +{ + return m_document; +} + +void QQuickPdfSelection::setDocument(QQuickPdfDocument *document) +{ + if (m_document == document) + return; + + if (m_document) { + disconnect(m_document, &QQuickPdfDocument::sourceChanged, + this, &QQuickPdfSelection::resetPoints); + } + m_document = document; + emit documentChanged(); + resetPoints(); + connect(m_document, &QQuickPdfDocument::sourceChanged, + this, &QQuickPdfSelection::resetPoints); +} + +/*! + \qmlproperty list<list<point>> PdfSelection::geometry + + A set of paths in a form that can be bound to the \c paths property of a + \l {QtQuick::PathMultiline}{PathMultiline} instance to render a batch of + rectangles around the text regions that are included in the selection: + + \qml + PdfDocument { + id: doc + } + PdfSelection { + id: selection + document: doc + from: textSelectionDrag.centroid.pressPosition + to: textSelectionDrag.centroid.position + hold: !textSelectionDrag.active + } + Shape { + ShapePath { + PathMultiline { + paths: selection.geometry + } + } + } + DragHandler { + id: textSelectionDrag + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + target: null + } + \endqml + + \sa PathMultiline +*/ +QList<QPolygonF> QQuickPdfSelection::geometry() const +{ + return m_geometry; +} + +/*! + \qmlmethod void PdfSelection::clear() + + Clears the current selection. +*/ +void QQuickPdfSelection::clear() +{ + m_hitPoint = QPointF(); + m_from = QPointF(); + m_to = QPointF(); + m_heightAtAnchor = 0; + m_heightAtCursor = 0; + m_fromCharIndex = -1; + m_toCharIndex = -1; + m_text.clear(); + m_geometry.clear(); + emit fromChanged(); + emit toChanged(); + emit textChanged(); + emit selectedAreaChanged(); + QGuiApplication::inputMethod()->update(Qt::ImQueryInput); +} + +/*! + \qmlmethod void PdfSelection::selectAll() + + Selects all text on the current \l page. +*/ +void QQuickPdfSelection::selectAll() +{ + if (!m_document) + return; + QPdfSelection sel = m_document->document()->getAllText(m_page); + if (sel.text() != m_text) { + m_text = sel.text(); + if (QGuiApplication::clipboard()->supportsSelection()) + sel.copyToClipboard(QClipboard::Selection); + emit textChanged(); + } + + if (sel.bounds() != m_geometry) { + m_geometry = sel.bounds(); + emit selectedAreaChanged(); + } +#if QT_CONFIG(im) + m_fromCharIndex = sel.startIndex(); + m_toCharIndex = sel.endIndex(); + if (sel.bounds().isEmpty()) { + m_from = QPointF(); + m_to = QPointF(); + } else { + m_from = sel.bounds().first().boundingRect().topLeft() * m_renderScale; + m_to = sel.bounds().last().boundingRect().bottomRight() * m_renderScale - QPointF(0, m_heightAtCursor); + } + + QGuiApplication::inputMethod()->update(Qt::ImCursorRectangle | Qt::ImAnchorRectangle); +#endif +} + +#if QT_CONFIG(im) +void QQuickPdfSelection::keyReleaseEvent(QKeyEvent *ev) +{ + qCDebug(qLcIm) << "release" << ev; + const auto &allText = pageText(); + if (ev == QKeySequence::MoveToPreviousWord) { + if (!m_document) + return; + // iOS sends MoveToPreviousWord first to get to the beginning of the word, + // and then SelectNextWord to select the whole word. + int i = allText.lastIndexOf(WordDelimiter, m_fromCharIndex - allText.size()); + if (i < 0) + i = 0; + else + i += 1; // don't select the space before the word + auto sel = m_document->document()->getSelectionAtIndex(m_page, i, m_text.size() + m_fromCharIndex - i); + update(sel); + QGuiApplication::inputMethod()->update(Qt::ImAnchorRectangle); + } else if (ev == QKeySequence::SelectNextWord) { + if (!m_document) + return; + int i = allText.indexOf(WordDelimiter, m_toCharIndex); + if (i < 0) + i = allText.size(); // go to the end of m_textAfter + auto sel = m_document->document()->getSelectionAtIndex(m_page, m_fromCharIndex, m_text.size() + i - m_toCharIndex); + update(sel); + QGuiApplication::inputMethod()->update(Qt::ImCursorRectangle); + } else if (ev == QKeySequence::Copy) { + copyToClipboard(); + } +} + +void QQuickPdfSelection::inputMethodEvent(QInputMethodEvent *event) +{ + for (auto attr : event->attributes()) { + switch (attr.type) { + case QInputMethodEvent::Cursor: + qCDebug(qLcIm) << "QInputMethodEvent::Cursor: moved to" << attr.start << "len" << attr.length; + break; + case QInputMethodEvent::Selection: { + if (!m_document) + return; + auto sel = m_document->document()->getSelectionAtIndex(m_page, attr.start, attr.length); + update(sel); + qCDebug(qLcIm) << "QInputMethodEvent::Selection: from" << attr.start << "len" << attr.length + << "result:" << m_fromCharIndex << "->" << m_toCharIndex << sel.boundingRectangle(); + // the iOS plugin decided that it wanted to change the selection, but still has to be told to move the handles (!?) + QGuiApplication::inputMethod()->update(Qt::ImCursorRectangle | Qt::ImAnchorRectangle); + break; + } + case QInputMethodEvent::Language: + case QInputMethodEvent::Ruby: + case QInputMethodEvent::TextFormat: + break; + } + } +} + +QVariant QQuickPdfSelection::inputMethodQuery(Qt::InputMethodQuery query, const QVariant &argument) const +{ + if (!argument.isNull()) { + qCDebug(qLcIm) << "IM query" << query << "with arg" << argument; + if (query == Qt::ImCursorPosition) { + if (!m_document) + return {}; + // If it didn't move since last time, return the same result. + if (m_hitPoint == argument.toPointF()) + return inputMethodQuery(query); + m_hitPoint = argument.toPointF(); + auto tp = m_document->document()->d->hitTest(m_page, m_hitPoint / m_renderScale); + qCDebug(qLcIm) << "ImCursorPosition hit testing in px" << m_hitPoint << "pt" << (m_hitPoint / m_renderScale) + << "got char index" << tp.charIndex << "@" << tp.position << "pt," << tp.position * m_renderScale << "px"; + if (tp.charIndex >= 0) { + m_toCharIndex = tp.charIndex; + m_to = tp.position * m_renderScale - QPointF(0, m_heightAtCursor); + m_heightAtCursor = tp.height * m_renderScale; + if (qFuzzyIsNull(m_heightAtAnchor)) + m_heightAtAnchor = m_heightAtCursor; + } + } + } + return inputMethodQuery(query); +} + +QVariant QQuickPdfSelection::inputMethodQuery(Qt::InputMethodQuery query) const +{ + QVariant ret; + switch (query) { + case Qt::ImEnabled: + ret = true; + break; + case Qt::ImHints: + ret = QVariant(Qt::ImhMultiLine | Qt::ImhNoPredictiveText); + break; + case Qt::ImInputItemClipRectangle: + ret = boundingRect(); + break; + case Qt::ImAnchorPosition: + ret = m_fromCharIndex; + break; + case Qt::ImAbsolutePosition: + ret = m_toCharIndex; + break; + case Qt::ImCursorPosition: + ret = m_toCharIndex; + break; + case Qt::ImAnchorRectangle: + ret = QRectF(m_from, QSizeF(1, m_heightAtAnchor)); + break; + case Qt::ImCursorRectangle: + ret = QRectF(m_to, QSizeF(1, m_heightAtCursor)); + break; + case Qt::ImSurroundingText: + ret = QVariant(pageText()); + break; + case Qt::ImTextBeforeCursor: + ret = QVariant(pageText().mid(0, m_toCharIndex)); + break; + case Qt::ImTextAfterCursor: + ret = QVariant(pageText().mid(m_toCharIndex)); + break; + case Qt::ImCurrentSelection: + ret = QVariant(m_text); + break; + case Qt::ImEnterKeyType: + break; + case Qt::ImFont: { + QFont font = QGuiApplication::font(); + font.setPointSizeF(m_heightAtCursor); + ret = font; + break; + } + case Qt::ImMaximumTextLength: + break; + case Qt::ImPreferredLanguage: + break; + case Qt::ImPlatformData: + break; + case Qt::ImReadOnly: + ret = true; + break; + case Qt::ImQueryInput: + case Qt::ImQueryAll: + qWarning() << "unexpected composite query"; + break; + } + qCDebug(qLcIm) << "IM query" << query << "returns" << ret; + return ret; +} +#endif // QT_CONFIG(im) + +const QString &QQuickPdfSelection::pageText() const +{ + if (m_pageTextDirty) { + if (!m_document) + return m_pageText; + m_pageText = m_document->document()->getAllText(m_page).text(); + m_pageTextDirty = false; + } + return m_pageText; +} + +void QQuickPdfSelection::resetPoints() +{ + bool wasHolding = m_hold; + m_hold = false; + setFrom(QPointF()); + setTo(QPointF()); + m_hold = wasHolding; +} + +/*! + \qmlproperty int PdfSelection::page + + The page number on which to search. + + \sa QtQuick::Image::currentFrame +*/ +int QQuickPdfSelection::page() const +{ + return m_page; +} + +void QQuickPdfSelection::setPage(int page) +{ + if (m_page == page) + return; + + m_page = page; + m_pageTextDirty = true; + emit pageChanged(); + resetPoints(); +} + +/*! + \qmlproperty real PdfSelection::renderScale + \brief The ratio from points to pixels at which the page is rendered. + + This is used to scale \l from and \l to to find ranges of + selected characters in the document, because positions within the document + are always given in points. +*/ +qreal QQuickPdfSelection::renderScale() const +{ + return m_renderScale; +} + +void QQuickPdfSelection::setRenderScale(qreal scale) +{ + if (qFuzzyIsNull(scale)) { + qWarning() << "PdfSelection.renderScale cannot be set to 0."; + return; + } + + if (qFuzzyCompare(scale, m_renderScale)) + return; + + m_renderScale = scale; + emit renderScaleChanged(); + updateResults(); +} + +/*! + \qmlproperty point PdfSelection::from + + The beginning location, in pixels from the upper-left corner of the page, + from which to find selected text. This can be bound to the + \c centroid.pressPosition of a \l DragHandler to begin selecting text from + the position where the user presses the mouse button and begins dragging, + for example. +*/ +QPointF QQuickPdfSelection::from() const +{ + return m_from; +} + +void QQuickPdfSelection::setFrom(QPointF from) +{ + if (m_hold || m_from == from) + return; + + m_from = from; + emit fromChanged(); + updateResults(); +} + +/*! + \qmlproperty point PdfSelection::to + + The ending location, in pixels from the upper-left corner of the page, + from which to find selected text. This can be bound to the + \c centroid.position of a \l DragHandler to end selection of text at the + position where the user is currently dragging the mouse, for example. +*/ +QPointF QQuickPdfSelection::to() const +{ + return m_to; +} + +void QQuickPdfSelection::setTo(QPointF to) +{ + if (m_hold || m_to == to) + return; + + m_to = to; + emit toChanged(); + updateResults(); +} + +/*! + \qmlproperty bool PdfSelection::hold + + Controls whether to hold the existing selection regardless of changes to + \l from and \l to. This property can be set to \c true when the mouse + or touchpoint is released, so that the selection is not lost due to the + point bindings changing. +*/ +bool QQuickPdfSelection::hold() const +{ + return m_hold; +} + +void QQuickPdfSelection::setHold(bool hold) +{ + if (m_hold == hold) + return; + + m_hold = hold; + emit holdChanged(); +} + +/*! + \qmlproperty string PdfSelection::string + + The string found. +*/ +QString QQuickPdfSelection::text() const +{ + return m_text; +} + +#if QT_CONFIG(clipboard) +/*! + \qmlmethod void PdfSelection::copyToClipboard() + + Copies plain text from the \l string property to the system clipboard. +*/ +void QQuickPdfSelection::copyToClipboard() const +{ + QGuiApplication::clipboard()->setText(m_text); +} +#endif + +void QQuickPdfSelection::updateResults() +{ + if (!m_document) + return; + QPdfSelection sel = m_document->document()->getSelection(m_page, + m_from / m_renderScale, m_to / m_renderScale); + update(sel, true); +} + +void QQuickPdfSelection::update(const QPdfSelection &sel, bool textAndGeometryOnly) +{ + if (sel.text() != m_text) { + m_text = sel.text(); + if (QGuiApplication::clipboard()->supportsSelection()) + sel.copyToClipboard(QClipboard::Selection); + emit textChanged(); + } + + if (sel.bounds() != m_geometry) { + m_geometry = sel.bounds(); + emit selectedAreaChanged(); + } + + if (textAndGeometryOnly) + return; + + m_fromCharIndex = sel.startIndex(); + m_toCharIndex = sel.endIndex(); + if (sel.bounds().isEmpty()) { + m_from = sel.boundingRectangle().topLeft() * m_renderScale; + m_to = m_from; + } else { + Qt::InputMethodQueries toUpdate = {}; + QRectF firstLineBounds = sel.bounds().first().boundingRect(); + m_from = firstLineBounds.topLeft() * m_renderScale; + if (!qFuzzyCompare(m_heightAtAnchor, firstLineBounds.height())) { + m_heightAtAnchor = firstLineBounds.height() * m_renderScale; + toUpdate.setFlag(Qt::ImAnchorRectangle); + } + QRectF lastLineBounds = sel.bounds().last().boundingRect(); + if (!qFuzzyCompare(m_heightAtCursor, lastLineBounds.height())) { + m_heightAtCursor = lastLineBounds.height() * m_renderScale; + toUpdate.setFlag(Qt::ImCursorRectangle); + } + m_to = lastLineBounds.topRight() * m_renderScale; + if (toUpdate) + QGuiApplication::inputMethod()->update(toUpdate); + } +} + +QT_END_NAMESPACE + +#include "moc_qquickpdfselection_p.cpp" diff --git a/src/pdfquick/qquickpdfselection_p.h b/src/pdfquick/qquickpdfselection_p.h new file mode 100644 index 000000000..4f633a467 --- /dev/null +++ b/src/pdfquick/qquickpdfselection_p.h @@ -0,0 +1,119 @@ +// Copyright (C) 2020 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 + +#ifndef QQUICKPDFSELECTION_P_H +#define QQUICKPDFSELECTION_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtPdfQuick/private/qtpdfquickglobal_p.h> +#include <QtPdfQuick/private/qquickpdfdocument_p.h> + +#include <QtCore/QPointF> +#include <QtCore/QVariant> +#include <QtGui/QPolygonF> +#include <QtQml/QQmlEngine> +#include <QtQuick/QQuickItem> + +QT_BEGIN_NAMESPACE +class QPdfSelection; + +class Q_PDFQUICK_EXPORT QQuickPdfSelection : public QQuickItem +{ + Q_OBJECT + Q_PROPERTY(QQuickPdfDocument *document READ document WRITE setDocument NOTIFY documentChanged) + Q_PROPERTY(int page READ page WRITE setPage NOTIFY pageChanged) + Q_PROPERTY(qreal renderScale READ renderScale WRITE setRenderScale NOTIFY renderScaleChanged) + Q_PROPERTY(QPointF from READ from WRITE setFrom NOTIFY fromChanged) + Q_PROPERTY(QPointF to READ to WRITE setTo NOTIFY toChanged) + Q_PROPERTY(bool hold READ hold WRITE setHold NOTIFY holdChanged) + + Q_PROPERTY(QString text READ text NOTIFY textChanged) + Q_PROPERTY(QList<QPolygonF> geometry READ geometry NOTIFY selectedAreaChanged) + QML_NAMED_ELEMENT(PdfSelection) + QML_ADDED_IN_VERSION(5, 15) + +public: + explicit QQuickPdfSelection(QQuickItem *parent = nullptr); + ~QQuickPdfSelection() override; + + QQuickPdfDocument *document() const; + void setDocument(QQuickPdfDocument * document); + int page() const; + void setPage(int page); + qreal renderScale() const; + void setRenderScale(qreal scale); + QPointF from() const; + void setFrom(QPointF from); + QPointF to() const; + void setTo(QPointF to); + bool hold() const; + void setHold(bool hold); + + QString text() const; + QList<QPolygonF> geometry() const; + + Q_INVOKABLE void clear(); + Q_INVOKABLE void selectAll(); +#if QT_CONFIG(clipboard) + Q_INVOKABLE void copyToClipboard() const; +#endif + +signals: + void documentChanged(); + void pageChanged(); + void renderScaleChanged(); + void fromChanged(); + void toChanged(); + void holdChanged(); + void textChanged(); + void selectedAreaChanged(); + +protected: +#if QT_CONFIG(im) + void keyReleaseEvent(QKeyEvent *ev) override; + void inputMethodEvent(QInputMethodEvent *event) override; + Q_INVOKABLE QVariant inputMethodQuery(Qt::InputMethodQuery query, const QVariant &argument) const; + QVariant inputMethodQuery(Qt::InputMethodQuery query) const override; +#endif + +private: + void resetPoints(); + void updateResults(); + void update(const QPdfSelection &sel, bool textAndGeometryOnly = false); + const QString &pageText() const; + +private: + QQuickPdfDocument *m_document = nullptr; + mutable QPointF m_hitPoint; + QPointF m_from; + mutable QPointF m_to; + qreal m_renderScale = 1; + mutable qreal m_heightAtAnchor = 0; + mutable qreal m_heightAtCursor = 0; + QString m_text; // selected text + mutable QString m_pageText; // all text on the page + QList<QPolygonF> m_geometry; + int m_page = 0; + int m_fromCharIndex = -1; // same as anchor position + mutable int m_toCharIndex = -1; // same as cursor position + bool m_hold = false; + mutable bool m_pageTextDirty = true; + + Q_DISABLE_COPY(QQuickPdfSelection) +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickPdfSelection) + +#endif // QQUICKPDFSELECTION_P_H diff --git a/src/pdfquick/qtpdfquickglobal_p.h b/src/pdfquick/qtpdfquickglobal_p.h new file mode 100644 index 000000000..7b5d629ed --- /dev/null +++ b/src/pdfquick/qtpdfquickglobal_p.h @@ -0,0 +1,34 @@ +// Copyright (C) 2021 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 + +#ifndef QTPDFQUICKGLOBAL_H +#define QTPDFQUICKGLOBAL_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qglobal.h> + +QT_BEGIN_NAMESPACE + +#ifndef QT_STATIC +# if defined(QT_BUILD_PDFQUICK_LIB) +# define Q_PDFQUICK_EXPORT Q_DECL_EXPORT +# else +# define Q_PDFQUICK_EXPORT Q_DECL_IMPORT +# endif +#else +# define Q_PDFQUICK_EXPORT +#endif + +QT_END_NAMESPACE + +#endif // QTPDFQUICKGLOBAL_H |