diff options
Diffstat (limited to 'src/pdfwidgets')
-rw-r--r-- | src/pdfwidgets/CMakeLists.txt | 5 | ||||
-rw-r--r-- | src/pdfwidgets/qpdfpageselector.cpp | 171 | ||||
-rw-r--r-- | src/pdfwidgets/qpdfpageselector.h | 51 | ||||
-rw-r--r-- | src/pdfwidgets/qpdfpageselector_p.h | 60 | ||||
-rw-r--r-- | src/pdfwidgets/qpdfview.cpp | 397 | ||||
-rw-r--r-- | src/pdfwidgets/qpdfview.h | 74 | ||||
-rw-r--r-- | src/pdfwidgets/qpdfview_p.h | 49 | ||||
-rw-r--r-- | src/pdfwidgets/qtpdfwidgetsglobal.h | 37 |
8 files changed, 641 insertions, 203 deletions
diff --git a/src/pdfwidgets/CMakeLists.txt b/src/pdfwidgets/CMakeLists.txt index 2d2616556..41af017c0 100644 --- a/src/pdfwidgets/CMakeLists.txt +++ b/src/pdfwidgets/CMakeLists.txt @@ -1,7 +1,11 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + find_package(Qt6 ${PROJECT_VERSION} CONFIG REQUIRED COMPONENTS Core Gui Widgets) qt_internal_add_module(PdfWidgets SOURCES + qpdfpageselector.cpp qpdfpageselector.h qpdfpageselector_p.h qpdfview.cpp qpdfview.h qpdfview_p.h qtpdfwidgetsglobal.h LIBRARIES @@ -11,4 +15,5 @@ qt_internal_add_module(PdfWidgets Qt::Gui Qt::Widgets Qt::Pdf + NO_GENERATE_CPP_EXPORTS ) diff --git a/src/pdfwidgets/qpdfpageselector.cpp b/src/pdfwidgets/qpdfpageselector.cpp new file mode 100644 index 000000000..66b781ed3 --- /dev/null +++ b/src/pdfwidgets/qpdfpageselector.cpp @@ -0,0 +1,171 @@ +// Copyright (C) 2023 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 "qpdfpageselector.h" +#include "qpdfpageselector_p.h" + +#include <QPdfDocument> + +#include <QtWidgets/qboxlayout.h> + +using namespace Qt::StringLiterals; + +QT_BEGIN_NAMESPACE + +/*! + \class QPdfPageSelector + \inmodule QtPdf + \since 6.6 + \brief A widget for selecting a PDF page. + + QPdfPageSelector is a widget for selecting a page label from a + QPdfDocument. + + \sa QPdfDocument::pageLabel() +*/ + +/*! + Constructs a PDF page selector with parent widget \a parent. +*/ +QPdfPageSelector::QPdfPageSelector(QWidget *parent) + : QWidget(parent), d_ptr(new QPdfPageSelectorPrivate) +{ + Q_D(QPdfPageSelector); + d->spinBox = new QPdfPageSelectorSpinBox(this); + d->spinBox->setObjectName(u"_q_spinBox"_s); + auto vlay = new QVBoxLayout(this); + vlay->setContentsMargins({}); + vlay->addWidget(d->spinBox); + + connect(d->spinBox, &QPdfPageSelectorSpinBox::_q_documentChanged, + this, &QPdfPageSelector::documentChanged); + connect(d->spinBox, &QSpinBox::valueChanged, this, &QPdfPageSelector::currentPageChanged); + connect(d->spinBox, &QSpinBox::textChanged, this, &QPdfPageSelector::currentPageLabelChanged); +} + +/*! + Destroys the page selector. +*/ +QPdfPageSelector::~QPdfPageSelector() + = default; + +/*! + \property QPdfPageSelector::document + + This property holds the document to be viewed. +*/ + +void QPdfPageSelector::setDocument(QPdfDocument *doc) +{ + Q_D(QPdfPageSelector); + d->spinBox->setDocument(doc); +} + +QPdfDocument *QPdfPageSelector::document() const +{ + Q_D(const QPdfPageSelector); + return d->spinBox->document(); +} + +/*! + \property QPdfPageSelector::currentPage + + This property holds the index (\c{0}-based) of the current page in the + document. +*/ + +int QPdfPageSelector::currentPage() const +{ + Q_D(const QPdfPageSelector); + return d->spinBox->value(); +} + +void QPdfPageSelector::setCurrentPage(int index) +{ + Q_D(QPdfPageSelector); + d->spinBox->setValue(index); +} + +/*! + \property QPdfPageSelector::currentPageLabel + + This property holds the page label corresponding to the current page + in the document. + + This is the text presented to the user. + + \sa QPdfDocument::pageLabel() +*/ + +QString QPdfPageSelector::currentPageLabel() const +{ + Q_D(const QPdfPageSelector); + return d->spinBox->text(); +} + +// +// QPdfPageSelectorSpinBox: +// + +void QPdfPageSelectorSpinBox::documentStatusChanged() +{ + if (m_document && m_document->status() == QPdfDocument::Status::Ready) { + setMaximum(m_document->pageCount()); + setValue(0); + } +} + +void QPdfPageSelectorSpinBox::setDocument(QPdfDocument *document) +{ + if (m_document == document) + return; + + if (m_document) + disconnect(m_documentStatusChangedConnection); + + m_document = document; + emit _q_documentChanged(document); + + if (m_document) { + m_documentStatusChangedConnection = + connect(m_document.get(), &QPdfDocument::statusChanged, + this, &QPdfPageSelectorSpinBox::documentStatusChanged); + } + + documentStatusChanged(); +} + +QPdfPageSelectorSpinBox::QPdfPageSelectorSpinBox(QWidget *parent) + : QSpinBox(parent) +{ +} + +QPdfPageSelectorSpinBox::~QPdfPageSelectorSpinBox() + = default; + +int QPdfPageSelectorSpinBox::valueFromText(const QString &text) const +{ + if (!m_document) + return 0; + + return m_document->pageIndexForLabel(text.trimmed()); +} + +QString QPdfPageSelectorSpinBox::textFromValue(int value) const +{ + if (!m_document) + return {}; + + return m_document->pageLabel(value); +} + +QValidator::State QPdfPageSelectorSpinBox::validate(QString &text, int &pos) const +{ + Q_UNUSED(pos); + return valueFromText(text) >= 0 ? QValidator::Acceptable : QValidator::Intermediate; +} + +QT_END_NAMESPACE + +#include "moc_qpdfpageselector_p.cpp" +#include "moc_qpdfpageselector.cpp" diff --git a/src/pdfwidgets/qpdfpageselector.h b/src/pdfwidgets/qpdfpageselector.h new file mode 100644 index 000000000..d779f54cd --- /dev/null +++ b/src/pdfwidgets/qpdfpageselector.h @@ -0,0 +1,51 @@ +// Copyright (C) 2023 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 QPDFPAGESELECTOR_H +#define QPDFPAGESELECTOR_H + +#include <QtPdfWidgets/qtpdfwidgetsglobal.h> + +#include <QtWidgets/qwidget.h> + +#include <memory> + +QT_BEGIN_NAMESPACE + +class QPdfDocument; +class QPdfPageSelectorPrivate; + +class Q_PDF_WIDGETS_EXPORT QPdfPageSelector : public QWidget +{ + Q_OBJECT + + Q_PROPERTY(QPdfDocument* document READ document WRITE setDocument NOTIFY documentChanged) + Q_PROPERTY(int currentPage READ currentPage WRITE setCurrentPage NOTIFY currentPageChanged USER true) + Q_PROPERTY(QString currentPageLabel READ currentPageLabel NOTIFY currentPageLabelChanged) +public: + QPdfPageSelector() : QPdfPageSelector(nullptr) {} + explicit QPdfPageSelector(QWidget *parent); + ~QPdfPageSelector() override; + + void setDocument(QPdfDocument *document); + QPdfDocument *document() const; + + int currentPage() const; + QString currentPageLabel() const; + +public Q_SLOTS: + void setCurrentPage(int index); + +Q_SIGNALS: + void documentChanged(QPdfDocument *document); + void currentPageChanged(int index); + void currentPageLabelChanged(const QString &label); + +private: + Q_DECLARE_PRIVATE(QPdfPageSelector) + const std::unique_ptr<QPdfPageSelectorPrivate> d_ptr; +}; + +QT_END_NAMESPACE + +#endif // QPDFPAGESELECTOR_H diff --git a/src/pdfwidgets/qpdfpageselector_p.h b/src/pdfwidgets/qpdfpageselector_p.h new file mode 100644 index 000000000..8e961f1d2 --- /dev/null +++ b/src/pdfwidgets/qpdfpageselector_p.h @@ -0,0 +1,60 @@ +// Copyright (C) 2023 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 QPDFPAGESELECTOR_P_H +#define QPDFPAGESELECTOR_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 "qpdfpageselector.h" + +#include <QtWidgets/qspinbox.h> + +#include <QPointer> + +QT_BEGIN_NAMESPACE + +class QPdfPageSelectorSpinBox : public QSpinBox +{ + Q_OBJECT +public: + QPdfPageSelectorSpinBox() : QPdfPageSelectorSpinBox(nullptr) {} + explicit QPdfPageSelectorSpinBox(QWidget *parent); + ~QPdfPageSelectorSpinBox(); + + void setDocument(QPdfDocument *document); + QPdfDocument *document() const { return m_document.get(); } + +Q_SIGNALS: + void _q_documentChanged(QPdfDocument *document); + +protected: + int valueFromText(const QString &text) const override; + QString textFromValue(int value) const override; + QValidator::State validate(QString &text, int &pos) const override; + +private: + void documentStatusChanged(); +private: + QPointer<QPdfDocument> m_document; + QMetaObject::Connection m_documentStatusChangedConnection; +}; + +class QPdfPageSelectorPrivate +{ +public: + QPdfPageSelectorSpinBox *spinBox; +}; + +QT_END_NAMESPACE + +#endif // QPDFPAGESELECTOR_P_H diff --git a/src/pdfwidgets/qpdfview.cpp b/src/pdfwidgets/qpdfview.cpp index cd2a8ef82..a67667fed 100644 --- a/src/pdfwidgets/qpdfview.cpp +++ b/src/pdfwidgets/qpdfview.cpp @@ -1,38 +1,6 @@ -/**************************************************************************** -** -** Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Tobias König <tobias.koenig@kdab.com> -** Contact: http://www.qt.io/licensing/ -** -** This file is part of the QtPDF module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL3$ -** 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 http://www.qt.io/terms-conditions. For further -** information use the contact form at http://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.LGPLv3 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.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 later as published by the Free -** Software Foundation and appearing in the file LICENSE.GPL included in -** the packaging of this file. Please review the following information to -** ensure the GNU General Public License version 2.0 requirements will be -** met: http://www.gnu.org/licenses/gpl-2.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" @@ -40,23 +8,31 @@ #include "qpdfpagerenderer.h" #include <QGuiApplication> +#include <QLoggingCategory> #include <QPainter> #include <QPaintEvent> #include <QPdfDocument> -#include <QPdfPageNavigation> +#include <QPdfPageNavigator> +#include <QPdfSearchModel> #include <QScreen> #include <QScrollBar> -#include <QScroller> QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(qLcLink, "qt.pdf.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) @@ -70,7 +46,7 @@ void QPdfViewPrivate::init() { Q_Q(QPdfView); - m_pageNavigation = new QPdfPageNavigation(q); + m_pageNavigator = new QPdfPageNavigator(q); m_pageRenderer = new QPdfPageRenderer(q); m_pageRenderer->setRenderMode(QPdfPageRenderer::RenderMode::MultiThreaded); } @@ -90,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(); } @@ -118,29 +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 QPdfPageNavigation 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; - m_pageNavigation->setCurrentPage(currentPage); + // ΤODO give location on the page + m_pageNavigator->jump(currentPage, {}, m_zoomFactor); m_blockPageScrolling = false; } } @@ -167,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); @@ -202,37 +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) { - 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); + 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->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(); @@ -241,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); @@ -263,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() @@ -277,7 +276,21 @@ void QPdfViewPrivate::updateDocumentLayout() updateScrollBars(); } - +/*! + \class QPdfView + \inmodule QtPdf + \brief A PDF viewer widget. + + QPdfView is a PDF viewer widget that offers a user experience similar to + many common PDF viewer applications, with two \l {pageMode}{modes}. + In the \c MultiPage mode, it supports flicking through the pages in the + entire document, with narrow gaps between the page images. + In the \c SinglePage mode, it shows one page at a time. +*/ + +/*! + Constructs a PDF viewer with parent widget \a parent. +*/ QPdfView::QPdfView(QWidget *parent) : QAbstractScrollArea(parent) , d_ptr(new QPdfViewPrivate(this)) @@ -286,23 +299,32 @@ QPdfView::QPdfView(QWidget *parent) d->init(); - connect(d->m_pageNavigation, &QPdfPageNavigation::currentPageChanged, this, [d](int page){ d->currentPageChanged(page); }); + connect(d->m_pageNavigator, &QPdfPageNavigator::currentPageChanged, this, + [d](int page){ d->currentPageChanged(page); }); - connect(d->m_pageRenderer, &QPdfPageRenderer::pageRendered, - this, [d](int pageNumber, QSize imageSize, const QImage &image, QPdfDocumentRenderOptions, quint64 requestId){ d->pageRendered(pageNumber, imageSize, image, requestId); }); + connect(d->m_pageRenderer, &QPdfPageRenderer::pageRendered, this, + [d](int pageNumber, QSize imageSize, const QImage &image, QPdfDocumentRenderOptions, quint64 requestId) { + d->pageRendered(pageNumber, imageSize, image, requestId); }); verticalScrollBar()->setSingleStep(20); horizontalScrollBar()->setSingleStep(20); - QScroller::grabGesture(this); - + setMouseTracking(true); d->calculateViewport(); } +/*! + Destroys the PDF viewer. +*/ QPdfView::~QPdfView() { } +/*! + \property QPdfView::document + + This property holds the document to be viewed. +*/ void QPdfView::setDocument(QPdfDocument *document) { Q_D(QPdfView); @@ -317,10 +339,12 @@ void QPdfView::setDocument(QPdfDocument *document) emit documentChanged(d->m_document); if (d->m_document) - d->m_documentStatusChangedConnection = connect(d->m_document.data(), &QPdfDocument::statusChanged, this, [d](){ d->documentStatusChanged(); }); + d->m_documentStatusChangedConnection = + connect(d->m_document.data(), &QPdfDocument::statusChanged, this, + [d](){ d->documentStatusChanged(); }); - d->m_pageNavigation->setDocument(d->m_document); d->m_pageRenderer->setDocument(d->m_document); + d->m_linkModel.setDocument(d->m_document); d->documentStatusChanged(); } @@ -332,13 +356,94 @@ QPdfDocument *QPdfView::document() const return d->m_document; } -QPdfPageNavigation *QPdfView::pageNavigation() 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. +*/ +QPdfPageNavigator *QPdfView::pageNavigator() const { Q_D(const QPdfView); - return d->m_pageNavigation; + return d->m_pageNavigator; } +/*! + \enum QPdfView::PageMode + + This enum describes the overall behavior of the PDF viewer: + + \value SinglePage Show one page at a time. + \value MultiPage Allow scrolling through all pages in the document. +*/ + +/*! + \property QPdfView::pageMode + + This property holds whether to show one page at a time, or all pages in the + document. The default is \c SinglePage. +*/ QPdfView::PageMode QPdfView::pageMode() const { Q_D(const QPdfView); @@ -359,6 +464,24 @@ void QPdfView::setPageMode(PageMode mode) emit pageModeChanged(d->m_pageMode); } +/*! + \enum QPdfView::ZoomMode + + This enum describes the magnification behavior of the PDF viewer: + + \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 + the entire page fits in the view. +*/ + +/*! + \property QPdfView::zoomMode + + This property indicates whether to use a custom size for the page(s), + or zoom them to fit to the view. The default is \c CustomZoom. +*/ QPdfView::ZoomMode QPdfView::zoomMode() const { Q_D(const QPdfView); @@ -379,6 +502,12 @@ void QPdfView::setZoomMode(ZoomMode mode) emit zoomModeChanged(d->m_zoomMode); } +/*! + \property QPdfView::zoomFactor + + 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. +*/ qreal QPdfView::zoomFactor() const { Q_D(const QPdfView); @@ -399,6 +528,12 @@ void QPdfView::setZoomFactor(qreal factor) emit zoomFactorChanged(d->m_zoomFactor); } +/*! + \property QPdfView::pageSpacing + + This property holds the size of the padding between pages in the \l MultiPage + \l {pageMode}{mode}. +*/ int QPdfView::pageSpacing() const { Q_D(const QPdfView); @@ -419,6 +554,11 @@ void QPdfView::setPageSpacing(int spacing) emit pageSpacingChanged(d->m_pageSpacing); } +/*! + \property QPdfView::documentMargins + + This property holds the margins around the page view. +*/ QMargins QPdfView::documentMargins() const { Q_D(const QPdfView); @@ -447,8 +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); @@ -456,9 +597,47 @@ void QPdfView::paintEvent(QPaintEvent *event) const auto pageIt = d->m_pageCache.constFind(page); if (pageIt != d->m_pageCache.cend()) { const QImage &img = pageIt.value(); - painter.drawImage(pageGeometry.topLeft(), img); + painter.drawImage(pageGeometry, img); } else { - d->m_pageRenderer->requestPage(page, pageGeometry.size()); + 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())); + } + } } } } @@ -483,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(qLcLink) << 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(qLcLink) << 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" diff --git a/src/pdfwidgets/qpdfview.h b/src/pdfwidgets/qpdfview.h index 25c0e7f69..7f1d014de 100644 --- a/src/pdfwidgets/qpdfview.h +++ b/src/pdfwidgets/qpdfview.h @@ -1,38 +1,6 @@ -/**************************************************************************** -** -** Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Tobias König <tobias.koenig@kdab.com> -** Contact: http://www.qt.io/licensing/ -** -** This file is part of the QtPDF module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL3$ -** 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 http://www.qt.io/terms-conditions. For further -** information use the contact form at http://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.LGPLv3 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.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 later as published by the Free -** Software Foundation and appearing in the file LICENSE.GPL included in -** the packaging of this file. Please review the following information to -** ensure the GNU General Public License version 2.0 requirements will be -** met: http://www.gnu.org/licenses/gpl-2.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) 2023 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 QPDFVIEW_H #define QPDFVIEW_H @@ -43,7 +11,8 @@ QT_BEGIN_NAMESPACE class QPdfDocument; -class QPdfPageNavigation; +class QPdfPageNavigator; +class QPdfSearchModel; class QPdfViewPrivate; class Q_PDF_WIDGETS_EXPORT QPdfView : public QAbstractScrollArea @@ -59,29 +28,38 @@ class Q_PDF_WIDGETS_EXPORT QPdfView : public QAbstractScrollArea Q_PROPERTY(int pageSpacing READ pageSpacing WRITE setPageSpacing NOTIFY pageSpacingChanged) Q_PROPERTY(QMargins documentMargins READ documentMargins WRITE setDocumentMargins NOTIFY documentMarginsChanged) + Q_PROPERTY(QPdfSearchModel* searchModel READ searchModel WRITE setSearchModel NOTIFY searchModelChanged) + Q_PROPERTY(int currentSearchResultIndex READ currentSearchResultIndex WRITE setCurrentSearchResultIndex NOTIFY currentSearchResultIndexChanged) + public: - enum PageMode + enum class PageMode { SinglePage, MultiPage }; Q_ENUM(PageMode) - enum ZoomMode + enum class ZoomMode { - CustomZoom, + Custom, FitToWidth, FitInView }; Q_ENUM(ZoomMode) - explicit QPdfView(QWidget *parent = nullptr); + QPdfView() : QPdfView(nullptr) {} + explicit QPdfView(QWidget *parent); ~QPdfView(); void setDocument(QPdfDocument *document); QPdfDocument *document() const; - QPdfPageNavigation *pageNavigation() const; + QPdfSearchModel *searchModel() const; + void setSearchModel(QPdfSearchModel *searchModel); + + int currentSearchResultIndex() const; + + QPdfPageNavigator *pageNavigator() const; PageMode pageMode() const; ZoomMode zoomMode() const; @@ -94,22 +72,28 @@ public: void setDocumentMargins(QMargins margins); public Q_SLOTS: - void setPageMode(PageMode mode); - void setZoomMode(ZoomMode mode); + void setPageMode(QPdfView::PageMode mode); + void setZoomMode(QPdfView::ZoomMode mode); void setZoomFactor(qreal factor); + void setCurrentSearchResultIndex(int currentResult); Q_SIGNALS: void documentChanged(QPdfDocument *document); - void pageModeChanged(PageMode pageMode); - void zoomModeChanged(ZoomMode zoomMode); + void pageModeChanged(QPdfView::PageMode pageMode); + void zoomModeChanged(QPdfView::ZoomMode zoomMode); void zoomFactorChanged(qreal zoomFactor); void pageSpacingChanged(int pageSpacing); void documentMarginsChanged(QMargins documentMargins); + void searchModelChanged(QPdfSearchModel *searchModel); + void currentSearchResultIndexChanged(int currentResult); protected: void paintEvent(QPaintEvent *event) override; void resizeEvent(QResizeEvent *event) override; void scrollContentsBy(int dx, int dy) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; private: Q_DECLARE_PRIVATE(QPdfView) diff --git a/src/pdfwidgets/qpdfview_p.h b/src/pdfwidgets/qpdfview_p.h index 60f67ec4e..d349cc2ee 100644 --- a/src/pdfwidgets/qpdfview_p.h +++ b/src/pdfwidgets/qpdfview_p.h @@ -1,38 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Tobias König <tobias.koenig@kdab.com> -** Contact: http://www.qt.io/licensing/ -** -** This file is part of the QtPDF module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL3$ -** 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 http://www.qt.io/terms-conditions. For further -** information use the contact form at http://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.LGPLv3 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.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 later as published by the Free -** Software Foundation and appearing in the file LICENSE.GPL included in -** the packaging of this file. Please review the following information to -** ensure the GNU General Public License version 2.0 requirements will be -** met: http://www.gnu.org/licenses/gpl-2.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> +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #ifndef QPDFVIEW_P_H #define QPDFVIEW_P_H @@ -49,6 +16,8 @@ // #include "qpdfview.h" +#include "qpdfdocument.h" +#include "qpdflinkmodel.h" #include <QHash> #include <QPointer> @@ -77,10 +46,12 @@ public: qreal yPositionForPage(int page) const; + QTransform screenScaleTransform(int page) const; // points to pixels + struct DocumentLayout { QSize documentSize; - QHash<int, QRect> pageGeometries; + QHash<int, QPair<QRect, qreal>> pageGeometryAndScale; }; DocumentLayout calculateDocumentLayout() const; @@ -88,13 +59,17 @@ public: QPdfView *q_ptr; QPointer<QPdfDocument> m_document; - QPdfPageNavigation* m_pageNavigation; + QPointer<QPdfSearchModel> m_searchModel; + QPdfPageNavigator* m_pageNavigator; QPdfPageRenderer *m_pageRenderer; + QPdfLinkModel m_linkModel; QPdfView::PageMode m_pageMode; QPdfView::ZoomMode m_zoomMode; qreal m_zoomFactor; + int m_currentSearchResultIndex = -1; + int m_pageSpacing; QMargins m_documentMargins; diff --git a/src/pdfwidgets/qtpdfwidgetsglobal.h b/src/pdfwidgets/qtpdfwidgetsglobal.h index 6c73a34f6..83133e38c 100644 --- a/src/pdfwidgets/qtpdfwidgetsglobal.h +++ b/src/pdfwidgets/qtpdfwidgetsglobal.h @@ -1,38 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2017 The Qt Company Ltd. -** Contact: http://www.qt.io/licensing/ -** -** This file is part of the QtPDF module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL3$ -** 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 http://www.qt.io/terms-conditions. For further -** information use the contact form at http://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.LGPLv3 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.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 later as published by the Free -** Software Foundation and appearing in the file LICENSE.GPL included in -** the packaging of this file. Please review the following information to -** ensure the GNU General Public License version 2.0 requirements will be -** met: http://www.gnu.org/licenses/gpl-2.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2017 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 QTPDFWIDGETSGLOBAL_H #define QTPDFWIDGETSGLOBAL_H |