1 /*
2     SPDX-FileCopyrightText: 2012 Marco Martin <mart@kde.org>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "pageitem.h"
8 #include "documentitem.h"
9 
10 #include <QPainter>
11 #include <QQuickWindow>
12 #include <QSGSimpleTextureNode>
13 #include <QStyleOptionGraphicsItem>
14 #include <QTimer>
15 
16 #include <core/bookmarkmanager.h>
17 #include <core/generator.h>
18 #include <core/page.h>
19 
20 #include "part/pagepainter.h"
21 #include "part/priorities.h"
22 #include "settings.h"
23 
24 #define REDRAW_TIMEOUT 250
25 
PageItem(QQuickItem * parent)26 PageItem::PageItem(QQuickItem *parent)
27     : QQuickItem(parent)
28     , Okular::View(QStringLiteral("PageView"))
29     , m_page(nullptr)
30     , m_smooth(false)
31     , m_bookmarked(false)
32     , m_isThumbnail(false)
33 {
34     setFlag(QQuickItem::ItemHasContents, true);
35 
36     m_viewPort.rePos.enabled = true;
37 
38     m_redrawTimer = new QTimer(this);
39     m_redrawTimer->setInterval(REDRAW_TIMEOUT);
40     m_redrawTimer->setSingleShot(true);
41     connect(m_redrawTimer, &QTimer::timeout, this, &PageItem::requestPixmap);
42     connect(this, &QQuickItem::windowChanged, m_redrawTimer, [this]() { m_redrawTimer->start(); });
43 }
44 
~PageItem()45 PageItem::~PageItem()
46 {
47 }
48 
setFlickable(QQuickItem * flickable)49 void PageItem::setFlickable(QQuickItem *flickable)
50 {
51     if (m_flickable.data() == flickable) {
52         return;
53     }
54 
55     // check the object can act as a flickable
56     if (!flickable->property("contentX").isValid() || !flickable->property("contentY").isValid()) {
57         return;
58     }
59 
60     if (m_flickable) {
61         disconnect(m_flickable.data(), nullptr, this, nullptr);
62     }
63 
64     // check the object can act as a flickable
65     if (!flickable->property("contentX").isValid() || !flickable->property("contentY").isValid()) {
66         m_flickable.clear();
67         return;
68     }
69 
70     m_flickable = flickable;
71 
72     if (flickable) {
73         connect(flickable, SIGNAL(contentXChanged()), this, SLOT(contentXChanged()));
74         connect(flickable, SIGNAL(contentYChanged()), this, SLOT(contentYChanged()));
75     }
76 
77     emit flickableChanged();
78 }
79 
flickable() const80 QQuickItem *PageItem::flickable() const
81 {
82     return m_flickable.data();
83 }
84 
document() const85 DocumentItem *PageItem::document() const
86 {
87     return m_documentItem.data();
88 }
89 
setDocument(DocumentItem * doc)90 void PageItem::setDocument(DocumentItem *doc)
91 {
92     if (doc == m_documentItem.data() || !doc) {
93         return;
94     }
95 
96     m_page = nullptr;
97     disconnect(doc, nullptr, this, nullptr);
98     m_documentItem = doc;
99     Observer *observer = m_isThumbnail ? m_documentItem.data()->thumbnailObserver() : m_documentItem.data()->pageviewObserver();
100     connect(observer, &Observer::pageChanged, this, &PageItem::pageHasChanged);
101     connect(doc->document()->bookmarkManager(), &Okular::BookmarkManager::bookmarksChanged, this, &PageItem::checkBookmarksChanged);
102     setPageNumber(0);
103     emit documentChanged();
104     m_redrawTimer->start();
105 
106     connect(doc, &DocumentItem::urlChanged, this, &PageItem::refreshPage);
107 }
108 
pageNumber() const109 int PageItem::pageNumber() const
110 {
111     return m_viewPort.pageNumber;
112 }
113 
setPageNumber(int number)114 void PageItem::setPageNumber(int number)
115 {
116     if ((m_page && m_viewPort.pageNumber == number) || !m_documentItem || !m_documentItem.data()->isOpened() || number < 0) {
117         return;
118     }
119 
120     m_viewPort.pageNumber = number;
121     refreshPage();
122     emit pageNumberChanged();
123     checkBookmarksChanged();
124 }
125 
refreshPage()126 void PageItem::refreshPage()
127 {
128     if (uint(m_viewPort.pageNumber) < m_documentItem.data()->document()->pages()) {
129         m_page = m_documentItem.data()->document()->page(m_viewPort.pageNumber);
130     } else {
131         m_page = nullptr;
132     }
133 
134     emit implicitWidthChanged();
135     emit implicitHeightChanged();
136 
137     m_redrawTimer->start();
138 }
139 
implicitWidth() const140 int PageItem::implicitWidth() const
141 {
142     if (m_page) {
143         return m_page->width();
144     }
145     return 0;
146 }
147 
implicitHeight() const148 int PageItem::implicitHeight() const
149 {
150     if (m_page) {
151         return m_page->height();
152     }
153     return 0;
154 }
155 
setSmooth(const bool smooth)156 void PageItem::setSmooth(const bool smooth)
157 {
158     if (smooth == m_smooth) {
159         return;
160     }
161     m_smooth = smooth;
162     update();
163 }
164 
smooth() const165 bool PageItem::smooth() const
166 {
167     return m_smooth;
168 }
169 
isBookmarked()170 bool PageItem::isBookmarked()
171 {
172     return m_bookmarked;
173 }
174 
setBookmarked(bool bookmarked)175 void PageItem::setBookmarked(bool bookmarked)
176 {
177     if (!m_documentItem) {
178         return;
179     }
180 
181     if (bookmarked == m_bookmarked) {
182         return;
183     }
184 
185     if (bookmarked) {
186         m_documentItem.data()->document()->bookmarkManager()->addBookmark(m_viewPort);
187     } else {
188         m_documentItem.data()->document()->bookmarkManager()->removeBookmark(m_viewPort.pageNumber);
189     }
190     m_bookmarked = bookmarked;
191     emit bookmarkedChanged();
192 }
193 
bookmarks() const194 QStringList PageItem::bookmarks() const
195 {
196     QStringList list;
197     const KBookmark::List pageMarks = m_documentItem.data()->document()->bookmarkManager()->bookmarks(m_viewPort.pageNumber);
198     for (const KBookmark &bookmark : pageMarks) {
199         list << bookmark.url().toString();
200     }
201     return list;
202 }
203 
goToBookmark(const QString & bookmark)204 void PageItem::goToBookmark(const QString &bookmark)
205 {
206     Okular::DocumentViewport viewPort(QUrl::fromUserInput(bookmark).fragment(QUrl::FullyDecoded));
207     setPageNumber(viewPort.pageNumber);
208 
209     // Are we in a flickable?
210     if (m_flickable) {
211         // normalizedX is a proportion, so contentX will be the difference between document and viewport times normalizedX
212         m_flickable.data()->setProperty("contentX", qMax((qreal)0, width() - m_flickable.data()->width()) * viewPort.rePos.normalizedX);
213 
214         m_flickable.data()->setProperty("contentY", qMax((qreal)0, height() - m_flickable.data()->height()) * viewPort.rePos.normalizedY);
215     }
216 }
217 
bookmarkPosition(const QString & bookmark) const218 QPointF PageItem::bookmarkPosition(const QString &bookmark) const
219 {
220     Okular::DocumentViewport viewPort(QUrl::fromUserInput(bookmark).fragment(QUrl::FullyDecoded));
221 
222     if (viewPort.pageNumber != m_viewPort.pageNumber) {
223         return QPointF(-1, -1);
224     }
225 
226     return QPointF(qMax((qreal)0, width() - m_flickable.data()->width()) * viewPort.rePos.normalizedX, qMax((qreal)0, height() - m_flickable.data()->height()) * viewPort.rePos.normalizedY);
227 }
228 
setBookmarkAtPos(qreal x,qreal y)229 void PageItem::setBookmarkAtPos(qreal x, qreal y)
230 {
231     Okular::DocumentViewport viewPort(m_viewPort);
232     viewPort.rePos.normalizedX = x;
233     viewPort.rePos.normalizedY = y;
234 
235     m_documentItem.data()->document()->bookmarkManager()->addBookmark(viewPort);
236 
237     if (!m_bookmarked) {
238         m_bookmarked = true;
239         emit bookmarkedChanged();
240     }
241 
242     emit bookmarksChanged();
243 }
244 
removeBookmarkAtPos(qreal x,qreal y)245 void PageItem::removeBookmarkAtPos(qreal x, qreal y)
246 {
247     Okular::DocumentViewport viewPort(m_viewPort);
248     viewPort.rePos.enabled = true;
249     viewPort.rePos.normalizedX = x;
250     viewPort.rePos.normalizedY = y;
251 
252     m_documentItem.data()->document()->bookmarkManager()->addBookmark(viewPort);
253 
254     if (m_bookmarked && m_documentItem.data()->document()->bookmarkManager()->bookmarks(m_viewPort.pageNumber).count() == 0) {
255         m_bookmarked = false;
256         emit bookmarkedChanged();
257     }
258 
259     emit bookmarksChanged();
260 }
261 
removeBookmark(const QString & bookmark)262 void PageItem::removeBookmark(const QString &bookmark)
263 {
264     m_documentItem.data()->document()->bookmarkManager()->removeBookmark(Okular::DocumentViewport(bookmark));
265     emit bookmarksChanged();
266 }
267 
268 // Reimplemented
geometryChanged(const QRectF & newGeometry,const QRectF & oldGeometry)269 void PageItem::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
270 {
271     if (newGeometry.size().isEmpty()) {
272         return;
273     }
274 
275     bool changed = false;
276     if (newGeometry.size() != oldGeometry.size()) {
277         changed = true;
278         m_redrawTimer->start();
279     }
280 
281     QQuickItem::geometryChanged(newGeometry, oldGeometry);
282 
283     if (changed) {
284         // Why aren't they automatically emitted?
285         emit widthChanged();
286         emit heightChanged();
287     }
288 }
289 
updatePaintNode(QSGNode * node,QQuickItem::UpdatePaintNodeData *)290 QSGNode *PageItem::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData * /*data*/)
291 {
292     if (!window() || m_buffer.isNull()) {
293         delete node;
294         return nullptr;
295     }
296     QSGSimpleTextureNode *n = static_cast<QSGSimpleTextureNode *>(node);
297     if (!n) {
298         n = new QSGSimpleTextureNode();
299         n->setOwnsTexture(true);
300     }
301 
302     n->setTexture(window()->createTextureFromImage(m_buffer));
303     n->setRect(boundingRect());
304     return n;
305 }
306 
requestPixmap()307 void PageItem::requestPixmap()
308 {
309     if (!m_documentItem || !m_page || !window() || width() <= 0 || height() < 0) {
310         if (!m_buffer.isNull()) {
311             m_buffer = QImage();
312             update();
313         }
314         return;
315     }
316 
317     Observer *observer = m_isThumbnail ? m_documentItem.data()->thumbnailObserver() : m_documentItem.data()->pageviewObserver();
318     const int priority = m_isThumbnail ? THUMBNAILS_PRIO : PAGEVIEW_PRIO;
319 
320     const qreal dpr = window()->devicePixelRatio();
321 
322     // Here we want to request the pixmap for the page, but it may happen that the page
323     // already has the pixmap, thus requestPixmaps would not trigger pageHasChanged
324     // and we would not call paint. Always call paint, if we don't have a pixmap
325     // it's a noop. Requesting a page that already has a pixmap is also
326     // almost a noop.
327     // Ideally we would do one or the other but for now this is good enough
328     paint();
329     {
330         auto request = new Okular::PixmapRequest(observer, m_viewPort.pageNumber, width() * dpr, height() * dpr, priority, Okular::PixmapRequest::Asynchronous);
331         request->setNormalizedRect(Okular::NormalizedRect(0, 0, 1, 1));
332         const Okular::Document::PixmapRequestFlag prf = Okular::Document::NoOption;
333         m_documentItem.data()->document()->requestPixmaps({request}, prf);
334     }
335 }
336 
paint()337 void PageItem::paint()
338 {
339     Observer *observer = m_isThumbnail ? m_documentItem.data()->thumbnailObserver() : m_documentItem.data()->pageviewObserver();
340     const int flags = PagePainter::Accessibility | PagePainter::Highlights | PagePainter::Annotations;
341 
342     const qreal dpr = window()->devicePixelRatio();
343     const QRect limits(QPoint(0, 0), QSize(width() * dpr, height() * dpr));
344     QPixmap pix(limits.size());
345     pix.setDevicePixelRatio(dpr);
346     QPainter p(&pix);
347     p.setRenderHint(QPainter::Antialiasing, m_smooth);
348     PagePainter::paintPageOnPainter(&p, m_page, observer, flags, width(), height(), limits);
349     p.end();
350 
351     m_buffer = pix.toImage();
352 
353     update();
354 }
355 
356 // Protected slots
pageHasChanged(int page,int flags)357 void PageItem::pageHasChanged(int page, int flags)
358 {
359     if (m_viewPort.pageNumber == page) {
360         if (flags == Okular::DocumentObserver::BoundingBox) {
361             // skip bounding box updates
362             // kDebug() << "32" << m_page->boundingBox();
363         } else if (flags == Okular::DocumentObserver::Pixmap) {
364             // if pixmaps have updated, just repaint .. don't bother updating pixmaps AGAIN
365             paint();
366         } else {
367             m_redrawTimer->start();
368         }
369     }
370 }
371 
checkBookmarksChanged()372 void PageItem::checkBookmarksChanged()
373 {
374     if (!m_documentItem) {
375         return;
376     }
377 
378     bool newBookmarked = m_documentItem.data()->document()->bookmarkManager()->isBookmarked(m_viewPort.pageNumber);
379     if (m_bookmarked != newBookmarked) {
380         m_bookmarked = newBookmarked;
381         emit bookmarkedChanged();
382     }
383 
384     // TODO: check the page
385     emit bookmarksChanged();
386 }
387 
contentXChanged()388 void PageItem::contentXChanged()
389 {
390     if (!m_flickable || !m_flickable.data()->property("contentX").isValid()) {
391         return;
392     }
393 
394     m_viewPort.rePos.normalizedX = m_flickable.data()->property("contentX").toReal() / (width() - m_flickable.data()->width());
395 }
396 
contentYChanged()397 void PageItem::contentYChanged()
398 {
399     if (!m_flickable || !m_flickable.data()->property("contentY").isValid()) {
400         return;
401     }
402 
403     m_viewPort.rePos.normalizedY = m_flickable.data()->property("contentY").toReal() / (height() - m_flickable.data()->height());
404 }
405 
setIsThumbnail(bool thumbnail)406 void PageItem::setIsThumbnail(bool thumbnail)
407 {
408     if (thumbnail == m_isThumbnail) {
409         return;
410     }
411 
412     m_isThumbnail = thumbnail;
413 
414     if (thumbnail) {
415         m_smooth = false;
416     }
417 
418     /*
419     m_redrawTimer->setInterval(thumbnail ? 0 : REDRAW_TIMEOUT);
420     m_redrawTimer->setSingleShot(true);
421     */
422 }
423