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