diff options
Diffstat (limited to 'src/pdfwidgets/qpdfview.cpp')
-rw-r--r-- | src/pdfwidgets/qpdfview.cpp | 307 |
1 files changed, 223 insertions, 84 deletions
diff --git a/src/pdfwidgets/qpdfview.cpp b/src/pdfwidgets/qpdfview.cpp index 395b350e7..a19b77a7f 100644 --- a/src/pdfwidgets/qpdfview.cpp +++ b/src/pdfwidgets/qpdfview.cpp @@ -1,42 +1,6 @@ -/**************************************************************************** -** -** Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Tobias König <tobias.koenig@kdab.com> -** Copyright (C) 2022 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtPDF module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Tobias König <tobias.koenig@kdab.com> +// 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 "qpdfview.h" #include "qpdfview_p.h" @@ -44,23 +8,31 @@ #include "qpdfpagerenderer.h" #include <QGuiApplication> +#include <QLoggingCategory> #include <QPainter> #include <QPaintEvent> #include <QPdfDocument> -#include <QPdfNavigationStack> +#include <QPdfPageNavigator> +#include <QPdfSearchModel> #include <QScreen> #include <QScrollBar> -#include <QScroller> QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(qLcWLink, "qt.pdf.widgets.links") +//#define DEBUG_LINKS + +static const QColor SearchResultHighlight("#80B0C4DE"); +static const QColor CurrentSearchResultHighlight(Qt::cyan); +static const int CurrentSearchResultWidth(2); + QPdfViewPrivate::QPdfViewPrivate(QPdfView *q) : q_ptr(q) , m_document(nullptr) - , m_pageNavigation(nullptr) + , m_pageNavigator(nullptr) , m_pageRenderer(nullptr) - , m_pageMode(QPdfView::SinglePage) - , m_zoomMode(QPdfView::CustomZoom) + , m_pageMode(QPdfView::PageMode::SinglePage) + , m_zoomMode(QPdfView::ZoomMode::Custom) , m_zoomFactor(1.0) , m_pageSpacing(3) , m_documentMargins(6, 6, 6, 6) @@ -74,7 +46,7 @@ void QPdfViewPrivate::init() { Q_Q(QPdfView); - m_pageNavigation = new QPdfNavigationStack(q); + m_pageNavigator = new QPdfPageNavigator(q); m_pageRenderer = new QPdfPageRenderer(q); m_pageRenderer->setRenderMode(QPdfPageRenderer::RenderMode::MultiThreaded); } @@ -94,7 +66,7 @@ void QPdfViewPrivate::currentPageChanged(int currentPage) q->verticalScrollBar()->setValue(yPositionForPage(currentPage)); - if (m_pageMode == QPdfView::SinglePage) + if (m_pageMode == QPdfView::PageMode::SinglePage) invalidateDocumentLayout(); } @@ -122,30 +94,31 @@ void QPdfViewPrivate::setViewport(QRect viewport) if (oldSize != m_viewport.size()) { updateDocumentLayout(); - if (m_zoomMode != QPdfView::CustomZoom) { + if (m_zoomMode != QPdfView::ZoomMode::Custom) { invalidatePageCache(); } } - if (m_pageMode == QPdfView::MultiPage) { + if (m_pageMode == QPdfView::PageMode::MultiPage) { // An imaginary, 2px height line at the upper half of the viewport, which is used to // determine which page is currently located there -> we propagate that as 'current' page - // to the QPdfNavigationStack object + // to the QPdfPageNavigator object const QRect currentPageLine(m_viewport.x(), m_viewport.y() + m_viewport.height() * 0.4, m_viewport.width(), 2); int currentPage = 0; - for (auto it = m_documentLayout.pageGeometries.cbegin(); it != m_documentLayout.pageGeometries.cend(); ++it) { - const QRect pageGeometry = it.value(); + for (auto it = m_documentLayout.pageGeometryAndScale.cbegin(); + it != m_documentLayout.pageGeometryAndScale.cend(); ++it) { + const QRect pageGeometry = it.value().first; if (pageGeometry.intersects(currentPageLine)) { currentPage = it.key(); break; } } - if (currentPage != m_pageNavigation->currentPage()) { + if (currentPage != m_pageNavigator->currentPage()) { m_blockPageScrolling = true; // ΤODO give location on the page - m_pageNavigation->jump(currentPage, {}, m_zoomFactor); + m_pageNavigator->jump(currentPage, {}, m_zoomFactor); m_blockPageScrolling = false; } } @@ -172,7 +145,7 @@ void QPdfViewPrivate::pageRendered(int pageNumber, QSize imageSize, const QImage Q_UNUSED(requestId); if (!m_cachedPagesLRU.contains(pageNumber)) { - if (m_cachedPagesLRU.length() > m_pageCacheLimit) + if (m_cachedPagesLRU.size() > m_pageCacheLimit) m_pageCache.remove(m_cachedPagesLRU.takeFirst()); m_cachedPagesLRU.append(pageNumber); @@ -207,39 +180,43 @@ QPdfViewPrivate::DocumentLayout QPdfViewPrivate::calculateDocumentLayout() const DocumentLayout documentLayout; - if (!m_document || m_document->status() != QPdfDocument::Ready) + if (!m_document || m_document->status() != QPdfDocument::Status::Ready) return documentLayout; - QHash<int, QRect> pageGeometries; + QHash<int, QPair<QRect, qreal>> pageGeometryAndScale; const int pageCount = m_document->pageCount(); int totalWidth = 0; - const int startPage = (m_pageMode == QPdfView::SinglePage ? m_pageNavigation->currentPage() : 0); - const int endPage = (m_pageMode == QPdfView::SinglePage ? m_pageNavigation->currentPage() + 1 : pageCount); + const int startPage = (m_pageMode == QPdfView::PageMode::SinglePage ? m_pageNavigator->currentPage() : 0); + const int endPage = (m_pageMode == QPdfView::PageMode::SinglePage ? m_pageNavigator->currentPage() + 1 : pageCount); // calculate page sizes for (int page = startPage; page < endPage; ++page) { QSize pageSize; - if (m_zoomMode == QPdfView::CustomZoom) { - pageSize = QSizeF(m_document->pageSize(page) * m_screenResolution * m_zoomFactor).toSize(); - } else if (m_zoomMode == QPdfView::FitToWidth) { - pageSize = QSizeF(m_document->pageSize(page) * m_screenResolution).toSize(); - const qreal factor = (qreal(m_viewport.width() - m_documentMargins.left() - m_documentMargins.right()) / - qreal(pageSize.width())); - pageSize *= factor; - } else if (m_zoomMode == QPdfView::FitInView) { + qreal pageScale = m_zoomFactor; + if (m_zoomMode == QPdfView::ZoomMode::Custom) { + pageSize = QSizeF(m_document->pagePointSize(page) * m_screenResolution * m_zoomFactor).toSize(); + } else if (m_zoomMode == QPdfView::ZoomMode::FitToWidth) { + pageSize = QSizeF(m_document->pagePointSize(page) * m_screenResolution).toSize(); + pageScale = (qreal(m_viewport.width() - m_documentMargins.left() - m_documentMargins.right()) / + qreal(pageSize.width())); + pageSize *= pageScale; + } else if (m_zoomMode == QPdfView::ZoomMode::FitInView) { const QSize viewportSize(m_viewport.size() + QSize(-m_documentMargins.left() - m_documentMargins.right(), -m_pageSpacing)); - pageSize = QSizeF(m_document->pageSize(page) * m_screenResolution).toSize(); - pageSize = pageSize.scaled(viewportSize, Qt::KeepAspectRatio); + pageSize = QSizeF(m_document->pagePointSize(page) * m_screenResolution).toSize(); + QSize scaledSize = pageSize.scaled(viewportSize, Qt::KeepAspectRatio); + // because of KeepAspectRatio, the ratio of widths should be the same as the ratio of heights + pageScale = qreal(scaledSize.width()) / qreal(pageSize.width()); + pageSize = scaledSize; } totalWidth = qMax(totalWidth, pageSize.width()); - pageGeometries[page] = QRect(QPoint(0, 0), pageSize); + pageGeometryAndScale[page] = {QRect(QPoint(0, 0), pageSize), pageScale}; } totalWidth += m_documentMargins.left() + m_documentMargins.right(); @@ -248,19 +225,19 @@ QPdfViewPrivate::DocumentLayout QPdfViewPrivate::calculateDocumentLayout() const // calculate page positions for (int page = startPage; page < endPage; ++page) { - const QSize pageSize = pageGeometries[page].size(); + const QSize pageSize = pageGeometryAndScale[page].first.size(); // center horizontal inside the viewport const int pageX = (qMax(totalWidth, m_viewport.width()) - pageSize.width()) / 2; - pageGeometries[page].moveTopLeft(QPoint(pageX, pageY)); + pageGeometryAndScale[page].first.moveTopLeft(QPoint(pageX, pageY)); pageY += pageSize.height() + m_pageSpacing; } pageY += m_documentMargins.bottom(); - documentLayout.pageGeometries = pageGeometries; + documentLayout.pageGeometryAndScale = pageGeometryAndScale; // calculate overall document size documentLayout.documentSize = QSize(totalWidth, pageY); @@ -270,11 +247,26 @@ QPdfViewPrivate::DocumentLayout QPdfViewPrivate::calculateDocumentLayout() const qreal QPdfViewPrivate::yPositionForPage(int pageNumber) const { - const auto it = m_documentLayout.pageGeometries.constFind(pageNumber); - if (it == m_documentLayout.pageGeometries.cend()) + const auto it = m_documentLayout.pageGeometryAndScale.constFind(pageNumber); + if (it == m_documentLayout.pageGeometryAndScale.cend()) return 0.0; - return (*it).y(); + return (*it).first.y(); +} + +QTransform QPdfViewPrivate::screenScaleTransform(int page) const +{ + qreal scale = m_screenResolution * m_zoomFactor; + switch (m_zoomMode) { + case QPdfView::ZoomMode::FitToWidth: + case QPdfView::ZoomMode::FitInView: + scale = m_screenResolution * m_documentLayout.pageGeometryAndScale[page].second; + break; + default: + break; + } + + return QTransform::fromScale(scale, scale); } void QPdfViewPrivate::updateDocumentLayout() @@ -307,7 +299,7 @@ QPdfView::QPdfView(QWidget *parent) d->init(); - connect(d->m_pageNavigation, &QPdfNavigationStack::currentPageChanged, this, + connect(d->m_pageNavigator, &QPdfPageNavigator::currentPageChanged, this, [d](int page){ d->currentPageChanged(page); }); connect(d->m_pageRenderer, &QPdfPageRenderer::pageRendered, this, @@ -317,8 +309,7 @@ QPdfView::QPdfView(QWidget *parent) verticalScrollBar()->setSingleStep(20); horizontalScrollBar()->setSingleStep(20); - QScroller::grabGesture(this); - + setMouseTracking(true); d->calculateViewport(); } @@ -353,6 +344,7 @@ void QPdfView::setDocument(QPdfDocument *document) [d](){ d->documentStatusChanged(); }); d->m_pageRenderer->setDocument(d->m_document); + d->m_linkModel.setDocument(d->m_document); d->documentStatusChanged(); } @@ -365,13 +357,76 @@ QPdfDocument *QPdfView::document() const } /*! + \since 6.6 + \property QPdfView::searchModel + + If this property is set, QPdfView draws highlight rectangles over the + search results provided by \l QPdfSearchModel::resultsOnPage(). By default + it is \c nullptr. +*/ +void QPdfView::setSearchModel(QPdfSearchModel *searchModel) +{ + Q_D(QPdfView); + if (d->m_searchModel == searchModel) + return; + + if (d->m_searchModel) + d->m_searchModel->disconnect(this); + + d->m_searchModel = searchModel; + emit searchModelChanged(searchModel); + + if (searchModel) { + connect(searchModel, &QPdfSearchModel::dataChanged, this, + [this](const QModelIndex &, const QModelIndex &, const QList<int> &) { update(); }); + } + setCurrentSearchResultIndex(-1); +} + +QPdfSearchModel *QPdfView::searchModel() const +{ + Q_D(const QPdfView); + return d->m_searchModel; +} + +/*! + \since 6.6 + \property QPdfView::currentSearchResultIndex + + If this property is set to a positive number, and \l searchModel is set, + QPdfView draws a frame around the search result provided by + \l QPdfSearchModel at the given index. For example, if QPdfSearchModel is + used as the model for a QListView, you can keep this property updated by + connecting QItemSelectionModel::currentChanged() from + QListView::selectionModel() to a function that will in turn call this function. + + By default it is \c -1, so that no search results are framed. +*/ +void QPdfView::setCurrentSearchResultIndex(int currentResult) +{ + Q_D(QPdfView); + if (d->m_currentSearchResultIndex == currentResult) + return; + + d->m_currentSearchResultIndex = currentResult; + emit currentSearchResultIndexChanged(currentResult); + viewport()->update(); //update(); +} + +int QPdfView::currentSearchResultIndex() const +{ + Q_D(const QPdfView); + return d->m_currentSearchResultIndex; +} + +/*! This accessor returns the navigation stack that will handle back/forward navigation. */ -QPdfNavigationStack *QPdfView::pageNavigation() const +QPdfPageNavigator *QPdfView::pageNavigator() const { Q_D(const QPdfView); - return d->m_pageNavigation; + return d->m_pageNavigator; } /*! @@ -414,7 +469,7 @@ void QPdfView::setPageMode(PageMode mode) This enum describes the magnification behavior of the PDF viewer: - \value CustomZoom Use \l zoomFactor only. + \value Custom Use \l zoomFactor only. \value FitToWidth Automatically choose a zoom factor so that the width of the page fits in the view. \value FitInView Automatically choose a zoom factor so that @@ -532,9 +587,9 @@ void QPdfView::paintEvent(QPaintEvent *event) painter.fillRect(event->rect(), palette().brush(QPalette::Dark)); painter.translate(-d->m_viewport.x(), -d->m_viewport.y()); - for (auto it = d->m_documentLayout.pageGeometries.cbegin(); - it != d->m_documentLayout.pageGeometries.cend(); ++it) { - const QRect pageGeometry = it.value(); + for (auto it = d->m_documentLayout.pageGeometryAndScale.cbegin(); + it != d->m_documentLayout.pageGeometryAndScale.cend(); ++it) { + const QRect pageGeometry = it.value().first; if (pageGeometry.intersects(d->m_viewport)) { // page needs to be painted painter.fillRect(pageGeometry, Qt::white); @@ -546,6 +601,44 @@ void QPdfView::paintEvent(QPaintEvent *event) } else { d->m_pageRenderer->requestPage(page, pageGeometry.size() * devicePixelRatioF()); } + + const QTransform scaleTransform = d->screenScaleTransform(page); +#ifdef DEBUG_LINKS + const QString fmt = u"page %1 @ %2, %3"_s; + d->m_linkModel.setPage(page); + const int linkCount = d->m_linkModel.rowCount({}); + for (int i = 0; i < linkCount; ++i) { + const QRectF linkBounds = scaleTransform.mapRect( + d->m_linkModel.data(d->m_linkModel.index(i), + int(QPdfLinkModel::Role::Rect)).toRectF()) + .translated(pageGeometry.topLeft()); + painter.setPen(Qt::blue); + painter.drawRect(linkBounds); + painter.setPen(Qt::red); + const QPoint loc = d->m_linkModel.data(d->m_linkModel.index(i), + int(QPdfLinkModel::Role::Location)).toPoint(); + // TODO maybe draw destination URL if that's what it is + painter.drawText(linkBounds.bottomLeft() + QPoint(2, -2), + fmt.arg(d->m_linkModel.data(d->m_linkModel.index(i), + int(QPdfLinkModel::Role::Page)).toInt()) + .arg(loc.x()).arg(loc.y())); + } +#endif + if (d->m_searchModel) { + for (const QPdfLink &result : d->m_searchModel->resultsOnPage(page)) { + for (const QRectF &rect : result.rectangles()) + painter.fillRect(scaleTransform.mapRect(rect).translated(pageGeometry.topLeft()), SearchResultHighlight); + } + + if (d->m_currentSearchResultIndex >= 0 && d->m_currentSearchResultIndex < d->m_searchModel->rowCount({})) { + const QPdfLink &cur = d->m_searchModel->resultAtIndex(d->m_currentSearchResultIndex); + if (cur.page() == page) { + painter.setPen({CurrentSearchResultHighlight, CurrentSearchResultWidth}); + for (const auto &rect : cur.rectangles()) + painter.drawRect(scaleTransform.mapRect(rect).translated(pageGeometry.topLeft())); + } + } + } } } } @@ -569,6 +662,52 @@ void QPdfView::scrollContentsBy(int dx, int dy) d->calculateViewport(); } +void QPdfView::mousePressEvent(QMouseEvent *event) +{ + Q_ASSERT(event->isAccepted()); +} + +void QPdfView::mouseMoveEvent(QMouseEvent *event) +{ + Q_D(QPdfView); + for (auto it = d->m_documentLayout.pageGeometryAndScale.cbegin(); + it != d->m_documentLayout.pageGeometryAndScale.cend(); ++it) { + const int page = it.key(); + const QTransform screenInvTransform = d->screenScaleTransform(page).inverted(); + const QRect pageGeometry = it.value().first; + if (pageGeometry.contains(event->position().toPoint())) { + const QPointF posInPoints = screenInvTransform.map(event->position() - pageGeometry.topLeft()); + d->m_linkModel.setPage(page); + auto dest = d->m_linkModel.linkAt(posInPoints); + setCursor(dest.isValid() ? Qt::PointingHandCursor : Qt::ArrowCursor); + if (dest.isValid()) + qCDebug(qLcWLink) << event->position() << ":" << posInPoints << "pt ->" << dest; + } + } +} + +void QPdfView::mouseReleaseEvent(QMouseEvent *event) +{ + Q_D(QPdfView); + for (auto it = d->m_documentLayout.pageGeometryAndScale.cbegin(); + it != d->m_documentLayout.pageGeometryAndScale.cend(); ++it) { + const int page = it.key(); + const QTransform screenInvTransform = d->screenScaleTransform(page).inverted(); + const QRect pageGeometry = it.value().first; + if (pageGeometry.contains(event->position().toPoint())) { + const QPointF posInPoints = screenInvTransform.map(event->position() - pageGeometry.topLeft()); + d->m_linkModel.setPage(page); + auto dest = d->m_linkModel.linkAt(posInPoints); + if (dest.isValid()) { + qCDebug(qLcWLink) << event << ": jumping to" << dest; + d->m_pageNavigator->jump(dest.page(), dest.location(), dest.zoom()); + // TODO scroll and zoom to where the link tells us to + } + return; + } + } +} + QT_END_NAMESPACE #include "moc_qpdfview.cpp" |