File indexing completed on 2024-05-12 16:06:54

0001 /*
0002     SPDX-FileCopyrightText: 2012 Marco Martin <mart@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "pageitem.h"
0008 #include "documentitem.h"
0009 
0010 #include <QPainter>
0011 #include <QQuickWindow>
0012 #include <QSGSimpleTextureNode>
0013 #include <QStyleOptionGraphicsItem>
0014 #include <QTimer>
0015 
0016 #include <core/bookmarkmanager.h>
0017 #include <core/generator.h>
0018 #include <core/page.h>
0019 
0020 #include "gui/pagepainter.h"
0021 #include "gui/priorities.h"
0022 #include "settings.h"
0023 
0024 #define REDRAW_TIMEOUT 250
0025 
0026 PageItem::PageItem(QQuickItem *parent)
0027     : QQuickItem(parent)
0028     , Okular::View(QStringLiteral("PageView"))
0029     , m_page(nullptr)
0030     , m_bookmarked(false)
0031     , m_isThumbnail(false)
0032 {
0033     setFlag(QQuickItem::ItemHasContents, true);
0034 
0035     m_viewPort.rePos.enabled = true;
0036 
0037     m_redrawTimer = new QTimer(this);
0038     m_redrawTimer->setInterval(REDRAW_TIMEOUT);
0039     m_redrawTimer->setSingleShot(true);
0040     connect(m_redrawTimer, &QTimer::timeout, this, &PageItem::requestPixmap);
0041     connect(this, &QQuickItem::windowChanged, m_redrawTimer, [this]() { m_redrawTimer->start(); });
0042 }
0043 
0044 PageItem::~PageItem()
0045 {
0046 }
0047 
0048 void PageItem::setFlickable(QQuickItem *flickable)
0049 {
0050     if (m_flickable.data() == flickable) {
0051         return;
0052     }
0053 
0054     // check the object can act as a flickable
0055     if (!flickable->property("contentX").isValid() || !flickable->property("contentY").isValid()) {
0056         return;
0057     }
0058 
0059     if (m_flickable) {
0060         disconnect(m_flickable.data(), nullptr, this, nullptr);
0061     }
0062 
0063     // check the object can act as a flickable
0064     if (!flickable->property("contentX").isValid() || !flickable->property("contentY").isValid()) {
0065         m_flickable.clear();
0066         return;
0067     }
0068 
0069     m_flickable = flickable;
0070 
0071     if (flickable) {
0072         // QQuickFlickable is not exported so we need the old-style connects here
0073         connect(flickable, SIGNAL(contentXChanged()), this, SLOT(contentXChanged())); // clazy:exclude=old-style-connect
0074         connect(flickable, SIGNAL(contentYChanged()), this, SLOT(contentYChanged())); // clazy:exclude=old-style-connect
0075     }
0076 
0077     Q_EMIT flickableChanged();
0078 }
0079 
0080 QQuickItem *PageItem::flickable() const
0081 {
0082     return m_flickable.data();
0083 }
0084 
0085 DocumentItem *PageItem::document() const
0086 {
0087     return m_documentItem.data();
0088 }
0089 
0090 void PageItem::setDocument(DocumentItem *doc)
0091 {
0092     if (doc == m_documentItem.data() || !doc) {
0093         return;
0094     }
0095 
0096     m_page = nullptr;
0097     disconnect(doc, nullptr, this, nullptr);
0098     m_documentItem = doc;
0099     Observer *observer = m_isThumbnail ? m_documentItem.data()->thumbnailObserver() : m_documentItem.data()->pageviewObserver();
0100     connect(observer, &Observer::pageChanged, this, &PageItem::pageHasChanged);
0101     connect(doc->document()->bookmarkManager(), &Okular::BookmarkManager::bookmarksChanged, this, &PageItem::checkBookmarksChanged);
0102     setPageNumber(0);
0103     Q_EMIT documentChanged();
0104     m_redrawTimer->start();
0105 
0106     connect(doc, &DocumentItem::urlChanged, this, &PageItem::refreshPage);
0107 }
0108 
0109 int PageItem::pageNumber() const
0110 {
0111     return m_viewPort.pageNumber;
0112 }
0113 
0114 void PageItem::setPageNumber(int number)
0115 {
0116     if ((m_page && m_viewPort.pageNumber == number) || !m_documentItem || !m_documentItem.data()->isOpened() || number < 0) {
0117         return;
0118     }
0119 
0120     m_viewPort.pageNumber = number;
0121     refreshPage();
0122     Q_EMIT pageNumberChanged();
0123     checkBookmarksChanged();
0124 }
0125 
0126 void PageItem::refreshPage()
0127 {
0128     if (uint(m_viewPort.pageNumber) < m_documentItem.data()->document()->pages()) {
0129         m_page = m_documentItem.data()->document()->page(m_viewPort.pageNumber);
0130     } else {
0131         m_page = nullptr;
0132     }
0133 
0134     Q_EMIT implicitWidthChanged();
0135     Q_EMIT implicitHeightChanged();
0136 
0137     m_redrawTimer->start();
0138 }
0139 
0140 int PageItem::implicitWidth() const
0141 {
0142     if (m_page) {
0143         return m_page->width();
0144     }
0145     return 0;
0146 }
0147 
0148 int PageItem::implicitHeight() const
0149 {
0150     if (m_page) {
0151         return m_page->height();
0152     }
0153     return 0;
0154 }
0155 
0156 bool PageItem::isBookmarked()
0157 {
0158     return m_bookmarked;
0159 }
0160 
0161 void PageItem::setBookmarked(bool bookmarked)
0162 {
0163     if (!m_documentItem) {
0164         return;
0165     }
0166 
0167     if (bookmarked == m_bookmarked) {
0168         return;
0169     }
0170 
0171     if (bookmarked) {
0172         m_documentItem.data()->document()->bookmarkManager()->addBookmark(m_viewPort);
0173     } else {
0174         m_documentItem.data()->document()->bookmarkManager()->removeBookmark(m_viewPort.pageNumber);
0175     }
0176     m_bookmarked = bookmarked;
0177     Q_EMIT bookmarkedChanged();
0178 }
0179 
0180 QStringList PageItem::bookmarks() const
0181 {
0182     QStringList list;
0183     const KBookmark::List pageMarks = m_documentItem.data()->document()->bookmarkManager()->bookmarks(m_viewPort.pageNumber);
0184     for (const KBookmark &bookmark : pageMarks) {
0185         list << bookmark.url().toString();
0186     }
0187     return list;
0188 }
0189 
0190 void PageItem::goToBookmark(const QString &bookmark)
0191 {
0192     Okular::DocumentViewport viewPort(QUrl::fromUserInput(bookmark).fragment(QUrl::FullyDecoded));
0193     setPageNumber(viewPort.pageNumber);
0194 
0195     // Are we in a flickable?
0196     if (m_flickable) {
0197         // normalizedX is a proportion, so contentX will be the difference between document and viewport times normalizedX
0198         m_flickable.data()->setProperty("contentX", qMax((qreal)0, width() - m_flickable.data()->width()) * viewPort.rePos.normalizedX);
0199 
0200         m_flickable.data()->setProperty("contentY", qMax((qreal)0, height() - m_flickable.data()->height()) * viewPort.rePos.normalizedY);
0201     }
0202 }
0203 
0204 QPointF PageItem::bookmarkPosition(const QString &bookmark) const
0205 {
0206     Okular::DocumentViewport viewPort(QUrl::fromUserInput(bookmark).fragment(QUrl::FullyDecoded));
0207 
0208     if (viewPort.pageNumber != m_viewPort.pageNumber) {
0209         return QPointF(-1, -1);
0210     }
0211 
0212     return QPointF(qMax((qreal)0, width() - m_flickable.data()->width()) * viewPort.rePos.normalizedX, qMax((qreal)0, height() - m_flickable.data()->height()) * viewPort.rePos.normalizedY);
0213 }
0214 
0215 void PageItem::setBookmarkAtPos(qreal x, qreal y)
0216 {
0217     Okular::DocumentViewport viewPort(m_viewPort);
0218     viewPort.rePos.normalizedX = x;
0219     viewPort.rePos.normalizedY = y;
0220 
0221     m_documentItem.data()->document()->bookmarkManager()->addBookmark(viewPort);
0222 
0223     if (!m_bookmarked) {
0224         m_bookmarked = true;
0225         Q_EMIT bookmarkedChanged();
0226     }
0227 
0228     Q_EMIT bookmarksChanged();
0229 }
0230 
0231 void PageItem::removeBookmarkAtPos(qreal x, qreal y)
0232 {
0233     Okular::DocumentViewport viewPort(m_viewPort);
0234     viewPort.rePos.enabled = true;
0235     viewPort.rePos.normalizedX = x;
0236     viewPort.rePos.normalizedY = y;
0237 
0238     m_documentItem.data()->document()->bookmarkManager()->addBookmark(viewPort);
0239 
0240     if (m_bookmarked && m_documentItem.data()->document()->bookmarkManager()->bookmarks(m_viewPort.pageNumber).count() == 0) {
0241         m_bookmarked = false;
0242         Q_EMIT bookmarkedChanged();
0243     }
0244 
0245     Q_EMIT bookmarksChanged();
0246 }
0247 
0248 void PageItem::removeBookmark(const QString &bookmark)
0249 {
0250     m_documentItem.data()->document()->bookmarkManager()->removeBookmark(Okular::DocumentViewport(bookmark));
0251     Q_EMIT bookmarksChanged();
0252 }
0253 
0254 // Reimplemented
0255 void PageItem::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
0256 {
0257     if (newGeometry.size().isEmpty()) {
0258         return;
0259     }
0260 
0261     bool changed = false;
0262     if (newGeometry.size() != oldGeometry.size()) {
0263         changed = true;
0264         m_redrawTimer->start();
0265     }
0266 
0267     QQuickItem::geometryChange(newGeometry, oldGeometry);
0268 
0269     if (changed) {
0270         // Why aren't they automatically emitted?
0271         Q_EMIT widthChanged();
0272         Q_EMIT heightChanged();
0273     }
0274 }
0275 
0276 QSGNode *PageItem::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData * /*data*/)
0277 {
0278     if (!window() || m_buffer.isNull()) {
0279         delete node;
0280         return nullptr;
0281     }
0282     QSGSimpleTextureNode *n = static_cast<QSGSimpleTextureNode *>(node);
0283     if (!n) {
0284         n = new QSGSimpleTextureNode();
0285         n->setOwnsTexture(true);
0286     }
0287 
0288     n->setTexture(window()->createTextureFromImage(m_buffer));
0289     n->setRect(boundingRect());
0290     return n;
0291 }
0292 
0293 void PageItem::requestPixmap()
0294 {
0295     if (!m_documentItem || !m_page || !window() || width() <= 0 || height() < 0) {
0296         if (!m_buffer.isNull()) {
0297             m_buffer = QImage();
0298             update();
0299         }
0300         return;
0301     }
0302 
0303     Observer *observer = m_isThumbnail ? m_documentItem.data()->thumbnailObserver() : m_documentItem.data()->pageviewObserver();
0304     const int priority = m_isThumbnail ? THUMBNAILS_PRIO : PAGEVIEW_PRIO;
0305 
0306     const qreal dpr = window()->devicePixelRatio();
0307 
0308     // Here we want to request the pixmap for the page, but it may happen that the page
0309     // already has the pixmap, thus requestPixmaps would not trigger pageHasChanged
0310     // and we would not call paint. Always call paint, if we don't have a pixmap
0311     // it's a noop. Requesting a page that already has a pixmap is also
0312     // almost a noop.
0313     // Ideally we would do one or the other but for now this is good enough
0314     paint();
0315     {
0316         auto request = new Okular::PixmapRequest(observer, m_viewPort.pageNumber, width(), height(), dpr, priority, Okular::PixmapRequest::Asynchronous);
0317         request->setNormalizedRect(Okular::NormalizedRect(0, 0, 1, 1));
0318         const Okular::Document::PixmapRequestFlag prf = Okular::Document::NoOption;
0319         m_documentItem.data()->document()->requestPixmaps({request}, prf);
0320     }
0321 }
0322 
0323 void PageItem::paint()
0324 {
0325     Observer *observer = m_isThumbnail ? m_documentItem.data()->thumbnailObserver() : m_documentItem.data()->pageviewObserver();
0326     const int flags = PagePainter::Accessibility | PagePainter::Highlights | PagePainter::Annotations;
0327 
0328     const qreal dpr = window()->devicePixelRatio();
0329     const QRect limits(QPoint(0, 0), QSize(width() * dpr, height() * dpr));
0330     QPixmap pix(limits.size());
0331     pix.setDevicePixelRatio(dpr);
0332     QPainter p(&pix);
0333     p.setRenderHint(QPainter::Antialiasing, false);
0334     PagePainter::paintPageOnPainter(&p, m_page, observer, flags, width(), height(), limits);
0335     p.end();
0336 
0337     m_buffer = pix.toImage();
0338 
0339     update();
0340 }
0341 
0342 // Protected slots
0343 void PageItem::pageHasChanged(int page, int flags)
0344 {
0345     if (m_viewPort.pageNumber == page) {
0346         if (flags == Okular::DocumentObserver::BoundingBox) {
0347             // skip bounding box updates
0348             // kDebug() << "32" << m_page->boundingBox();
0349         } else if (flags == Okular::DocumentObserver::Pixmap) {
0350             // if pixmaps have updated, just repaint .. don't bother updating pixmaps AGAIN
0351             paint();
0352         } else {
0353             m_redrawTimer->start();
0354         }
0355     }
0356 }
0357 
0358 void PageItem::checkBookmarksChanged()
0359 {
0360     if (!m_documentItem) {
0361         return;
0362     }
0363 
0364     bool newBookmarked = m_documentItem.data()->document()->bookmarkManager()->isBookmarked(m_viewPort.pageNumber);
0365     if (m_bookmarked != newBookmarked) {
0366         m_bookmarked = newBookmarked;
0367         Q_EMIT bookmarkedChanged();
0368     }
0369 
0370     // TODO: check the page
0371     Q_EMIT bookmarksChanged();
0372 }
0373 
0374 void PageItem::contentXChanged()
0375 {
0376     if (!m_flickable || !m_flickable.data()->property("contentX").isValid()) {
0377         return;
0378     }
0379 
0380     m_viewPort.rePos.normalizedX = m_flickable.data()->property("contentX").toReal() / (width() - m_flickable.data()->width());
0381 }
0382 
0383 void PageItem::contentYChanged()
0384 {
0385     if (!m_flickable || !m_flickable.data()->property("contentY").isValid()) {
0386         return;
0387     }
0388 
0389     m_viewPort.rePos.normalizedY = m_flickable.data()->property("contentY").toReal() / (height() - m_flickable.data()->height());
0390 }
0391 
0392 void PageItem::setIsThumbnail(bool thumbnail)
0393 {
0394     if (thumbnail == m_isThumbnail) {
0395         return;
0396     }
0397 
0398     m_isThumbnail = thumbnail;
0399 
0400     /*
0401     m_redrawTimer->setInterval(thumbnail ? 0 : REDRAW_TIMEOUT);
0402     m_redrawTimer->setSingleShot(true);
0403     */
0404 }