1 /*
2 SPDX-FileCopyrightText: 2004-2006 Albert Astals Cid <aacid@kde.org>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include "thumbnaillist.h"
8
9 // qt/kde includes
10 #include <QAction>
11 #include <QApplication>
12 #include <QIcon>
13 #include <QPainter>
14 #include <QResizeEvent>
15 #include <QScrollBar>
16 #include <QSizePolicy>
17 #include <QStyle>
18 #include <QTimer>
19 #include <QVBoxLayout>
20
21 #include <KActionCollection>
22 #include <KLocalizedString>
23 #include <KTitleWidget>
24
25 #include <kwidgetsaddons_version.h>
26
27 // local includes
28 #include "core/area.h"
29 #include "core/bookmarkmanager.h"
30 #include "core/document.h"
31 #include "core/generator.h"
32 #include "core/page.h"
33 #include "cursorwraphelper.h"
34 #include "pagepainter.h"
35 #include "priorities.h"
36 #include "settings.h"
37
38 class ThumbnailWidget;
39
ThumbnailsBox(QWidget * parent)40 ThumbnailsBox::ThumbnailsBox(QWidget *parent)
41 : QWidget(parent)
42 {
43 QVBoxLayout *vbox = new QVBoxLayout(this);
44 vbox->setSpacing(0);
45
46 KTitleWidget *titleWidget = new KTitleWidget(this);
47 titleWidget->setLevel(2);
48 titleWidget->setText(i18n("Thumbnails"));
49 vbox->addWidget(titleWidget);
50 vbox->setAlignment(titleWidget, Qt::AlignHCenter);
51 }
52
sizeHint() const53 QSize ThumbnailsBox::sizeHint() const
54 {
55 return QSize();
56 }
57
58 class ThumbnailListPrivate : public QWidget
59 {
60 Q_OBJECT
61 public:
62 ThumbnailListPrivate(ThumbnailList *qq, Okular::Document *document);
63 ~ThumbnailListPrivate() override;
64
65 enum ChangePageDirection { Null, Left, Right, Up, Down };
66
67 ThumbnailList *q;
68 Okular::Document *m_document;
69 ThumbnailWidget *m_selected;
70 QTimer *m_delayTimer;
71 QPixmap *m_bookmarkOverlay;
72 QVector<ThumbnailWidget *> m_thumbnails;
73 QList<ThumbnailWidget *> m_visibleThumbnails;
74 int m_vectorIndex;
75 // Grabbing variables
76 QPoint m_mouseGrabPos;
77 ThumbnailWidget *m_mouseGrabItem;
78 int m_pageCurrentlyGrabbed;
79
80 // resize thumbnails to fit the width
81 void viewportResizeEvent(QResizeEvent *);
82 // called by ThumbnailWidgets to get the overlay bookmark pixmap
83 const QPixmap *getBookmarkOverlay() const;
84 // called by ThumbnailWidgets to send (forward) the mouse move signals
85 ChangePageDirection forwardTrack(const QPoint, const QSize);
86
87 ThumbnailWidget *itemFor(const QPoint p) const;
88 void delayedRequestVisiblePixmaps(int delayMs = 0);
89
90 // SLOTS:
91 // make requests for generating pixmaps for visible thumbnails
92 void slotRequestVisiblePixmaps();
93 // delay timeout: resize overlays and requests pixmaps
94 void slotDelayTimeout();
95 ThumbnailWidget *getPageByNumber(int page) const;
96 int getNewPageOffset(int n, ThumbnailListPrivate::ChangePageDirection dir) const;
97 ThumbnailWidget *getThumbnailbyOffset(int current, int offset) const;
98
99 protected:
100 void mousePressEvent(QMouseEvent *e) override;
101 void mouseReleaseEvent(QMouseEvent *e) override;
102 void mouseMoveEvent(QMouseEvent *e) override;
103 void wheelEvent(QWheelEvent *e) override;
104 void contextMenuEvent(QContextMenuEvent *e) override;
105 void paintEvent(QPaintEvent *e) override;
106 };
107
108 // ThumbnailWidget represents a single thumbnail in the ThumbnailList
109 class ThumbnailWidget
110 {
111 public:
112 ThumbnailWidget(ThumbnailListPrivate *parent, const Okular::Page *page);
113
114 // set internal parameters to fit the page in the given width
115 void resizeFitWidth(int width);
116 // set thumbnail's selected state
117 void setSelected(bool selected);
118 // set the visible rect of the current page
119 void setVisibleRect(const Okular::NormalizedRect &rect);
120
121 // query methods
heightHint() const122 int heightHint() const
123 {
124 return m_pixmapHeight + m_labelHeight + m_margin;
125 }
pixmapWidth() const126 int pixmapWidth() const
127 {
128 return m_pixmapWidth;
129 }
pixmapHeight() const130 int pixmapHeight() const
131 {
132 return m_pixmapHeight;
133 }
pageNumber() const134 int pageNumber() const
135 {
136 return m_page->number();
137 }
page() const138 const Okular::Page *page() const
139 {
140 return m_page;
141 }
visibleRect() const142 QRect visibleRect() const
143 {
144 return m_visibleRect.geometry(m_pixmapWidth, m_pixmapHeight);
145 }
146
147 void paint(QPainter &p, const QRect clipRect);
148
margin()149 static int margin()
150 {
151 return m_margin;
152 }
153
154 // simulating QWidget
rect() const155 QRect rect() const
156 {
157 return m_rect;
158 }
height() const159 int height() const
160 {
161 return m_rect.height();
162 }
width() const163 int width() const
164 {
165 return m_rect.width();
166 }
pos() const167 QPoint pos() const
168 {
169 return m_rect.topLeft();
170 }
move(int x,int y)171 void move(int x, int y)
172 {
173 m_rect.setTopLeft(QPoint(x, y));
174 }
update()175 void update()
176 {
177 m_parent->update(m_rect);
178 }
update(const QRect rect)179 void update(const QRect rect)
180 {
181 m_parent->update(rect.translated(m_rect.topLeft()));
182 }
183
184 private:
185 // the margin around the widget
186 static int const m_margin = 16;
187
188 ThumbnailListPrivate *m_parent;
189 const Okular::Page *m_page;
190 bool m_selected;
191 int m_pixmapWidth, m_pixmapHeight;
192 int m_labelHeight, m_labelNumber;
193 Okular::NormalizedRect m_visibleRect;
194 QRect m_rect;
195 };
196
ThumbnailListPrivate(ThumbnailList * qq,Okular::Document * document)197 ThumbnailListPrivate::ThumbnailListPrivate(ThumbnailList *qq, Okular::Document *document)
198 : QWidget()
199 , q(qq)
200 , m_document(document)
201 , m_selected(nullptr)
202 , m_delayTimer(nullptr)
203 , m_bookmarkOverlay(nullptr)
204 , m_vectorIndex(0)
205 {
206 setMouseTracking(true);
207 m_mouseGrabItem = nullptr;
208 }
209
getPageByNumber(int page) const210 ThumbnailWidget *ThumbnailListPrivate::getPageByNumber(int page) const
211 {
212 QVector<ThumbnailWidget *>::const_iterator tIt = m_thumbnails.constBegin(), tEnd = m_thumbnails.constEnd();
213 for (; tIt != tEnd; ++tIt) {
214 if ((*tIt)->pageNumber() == page)
215 return (*tIt);
216 }
217 return nullptr;
218 }
219
~ThumbnailListPrivate()220 ThumbnailListPrivate::~ThumbnailListPrivate()
221 {
222 }
223
itemFor(const QPoint p) const224 ThumbnailWidget *ThumbnailListPrivate::itemFor(const QPoint p) const
225 {
226 QVector<ThumbnailWidget *>::const_iterator tIt = m_thumbnails.constBegin(), tEnd = m_thumbnails.constEnd();
227 for (; tIt != tEnd; ++tIt) {
228 if ((*tIt)->rect().contains(p))
229 return (*tIt);
230 }
231 return nullptr;
232 }
233
paintEvent(QPaintEvent * e)234 void ThumbnailListPrivate::paintEvent(QPaintEvent *e)
235 {
236 QPainter painter(this);
237 QVector<ThumbnailWidget *>::const_iterator tIt = m_thumbnails.constBegin(), tEnd = m_thumbnails.constEnd();
238 for (; tIt != tEnd; ++tIt) {
239 QRect rect = e->rect().intersected((*tIt)->rect());
240 if (!rect.isNull()) {
241 rect.translate(-(*tIt)->pos());
242 painter.save();
243 painter.translate((*tIt)->pos());
244 (*tIt)->paint(painter, rect);
245 painter.restore();
246 }
247 }
248 }
249
250 /** ThumbnailList implementation **/
251
ThumbnailList(QWidget * parent,Okular::Document * document)252 ThumbnailList::ThumbnailList(QWidget *parent, Okular::Document *document)
253 : QScrollArea(parent)
254 , d(new ThumbnailListPrivate(this, document))
255 {
256 setObjectName(QStringLiteral("okular::Thumbnails"));
257 // set scrollbars
258 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
259 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
260 verticalScrollBar()->setEnabled(false);
261
262 setAttribute(Qt::WA_StaticContents);
263
264 viewport()->setBackgroundRole(QPalette::Base);
265
266 setWidget(d);
267 // widget setup: can be focused by mouse click (not wheel nor tab)
268 widget()->setFocusPolicy(Qt::ClickFocus);
269 widget()->show();
270 widget()->setBackgroundRole(QPalette::Base);
271
272 connect(verticalScrollBar(), &QScrollBar::valueChanged, d, &ThumbnailListPrivate::slotRequestVisiblePixmaps);
273 }
274
~ThumbnailList()275 ThumbnailList::~ThumbnailList()
276 {
277 d->m_document->removeObserver(this);
278 delete d->m_bookmarkOverlay;
279 }
280
281 // BEGIN DocumentObserver inherited methods
notifySetup(const QVector<Okular::Page * > & pages,int setupFlags)282 void ThumbnailList::notifySetup(const QVector<Okular::Page *> &pages, int setupFlags)
283 {
284 // if there was a widget selected, save its pagenumber to restore
285 // its selection (if available in the new set of pages)
286 int prevPage = -1;
287 if (!(setupFlags & Okular::DocumentObserver::DocumentChanged) && d->m_selected) {
288 prevPage = d->m_selected->page()->number();
289 } else
290 prevPage = d->m_document->viewport().pageNumber;
291
292 // delete all the Thumbnails
293 QVector<ThumbnailWidget *>::const_iterator tIt = d->m_thumbnails.constBegin(), tEnd = d->m_thumbnails.constEnd();
294 for (; tIt != tEnd; ++tIt)
295 delete *tIt;
296 d->m_thumbnails.clear();
297 d->m_visibleThumbnails.clear();
298 d->m_selected = nullptr;
299 d->m_mouseGrabItem = nullptr;
300
301 if (pages.count() < 1) {
302 widget()->resize(0, 0);
303 return;
304 }
305
306 // show pages containing highlighted text or bookmarked ones
307 // RESTORE THIS int flags = Okular::Settings::filterBookmarks() ? Okular::Page::Bookmark : Okular::Page::Highlight;
308
309 // if no page matches filter rule, then display all pages
310 QVector<Okular::Page *>::const_iterator pIt = pages.constBegin(), pEnd = pages.constEnd();
311 bool skipCheck = true;
312 for (; pIt != pEnd; ++pIt)
313 // if ( (*pIt)->attributes() & flags )
314 if ((*pIt)->hasHighlights(SW_SEARCH_ID))
315 skipCheck = false;
316
317 // generate Thumbnails for the given set of pages
318 const int width = viewport()->width();
319 int height = 0;
320 int centerHeight = 0;
321 for (pIt = pages.constBegin(); pIt != pEnd; ++pIt)
322 // if ( skipCheck || (*pIt)->attributes() & flags )
323 if (skipCheck || (*pIt)->hasHighlights(SW_SEARCH_ID)) {
324 ThumbnailWidget *t = new ThumbnailWidget(d, *pIt);
325 t->move(0, height);
326 // add to the internal queue
327 d->m_thumbnails.push_back(t);
328 // update total height (asking widget its own height)
329 t->resizeFitWidth(width);
330 // restoring the previous selected page, if any
331 if ((*pIt)->number() < prevPage) {
332 centerHeight = height + t->height() + this->style()->layoutSpacing(QSizePolicy::Frame, QSizePolicy::Frame, Qt::Vertical) / 2;
333 }
334 if ((*pIt)->number() == prevPage) {
335 d->m_selected = t;
336 d->m_selected->setSelected(true);
337 centerHeight = height + t->height() / 2;
338 }
339 height += t->height() + this->style()->layoutSpacing(QSizePolicy::Frame, QSizePolicy::Frame, Qt::Vertical);
340 }
341
342 // update scrollview's contents size (sets scrollbars limits)
343 height -= this->style()->layoutSpacing(QSizePolicy::Frame, QSizePolicy::Frame, Qt::Vertical);
344 widget()->resize(width, height);
345
346 // enable scrollbar when there's something to scroll
347 verticalScrollBar()->setEnabled(viewport()->height() < height);
348 verticalScrollBar()->setValue(centerHeight - viewport()->height() / 2);
349
350 // request for thumbnail generation
351 d->delayedRequestVisiblePixmaps(200);
352 }
353
notifyCurrentPageChanged(int previousPage,int currentPage)354 void ThumbnailList::notifyCurrentPageChanged(int previousPage, int currentPage)
355 {
356 Q_UNUSED(previousPage)
357
358 // skip notifies for the current page (already selected)
359 if (d->m_selected && d->m_selected->pageNumber() == currentPage)
360 return;
361
362 // deselect previous thumbnail
363 if (d->m_selected)
364 d->m_selected->setSelected(false);
365 d->m_selected = nullptr;
366
367 // select the page with viewport and ensure it's centered in the view
368 d->m_vectorIndex = 0;
369 QVector<ThumbnailWidget *>::const_iterator tIt = d->m_thumbnails.constBegin(), tEnd = d->m_thumbnails.constEnd();
370 for (; tIt != tEnd; ++tIt) {
371 if ((*tIt)->pageNumber() == currentPage) {
372 d->m_selected = *tIt;
373 d->m_selected->setSelected(true);
374 if (Okular::Settings::syncThumbnailsViewport()) {
375 int yOffset = qMax(viewport()->height() / 4, d->m_selected->height() / 2);
376 ensureVisible(0, d->m_selected->pos().y() + d->m_selected->height() / 2, 0, yOffset);
377 }
378 break;
379 }
380 d->m_vectorIndex++;
381 }
382 }
383
notifyPageChanged(int pageNumber,int changedFlags)384 void ThumbnailList::notifyPageChanged(int pageNumber, int changedFlags)
385 {
386 static const int interestingFlags = DocumentObserver::Pixmap | DocumentObserver::Bookmark | DocumentObserver::Highlights | DocumentObserver::Annotations;
387 // only handle change notifications we are interested in
388 if (!(changedFlags & interestingFlags))
389 return;
390
391 // iterate over visible items: if page(pageNumber) is one of them, repaint it
392 QList<ThumbnailWidget *>::const_iterator vIt = d->m_visibleThumbnails.constBegin(), vEnd = d->m_visibleThumbnails.constEnd();
393 for (; vIt != vEnd; ++vIt)
394 if ((*vIt)->pageNumber() == pageNumber) {
395 (*vIt)->update();
396 break;
397 }
398 }
399
notifyContentsCleared(int changedFlags)400 void ThumbnailList::notifyContentsCleared(int changedFlags)
401 {
402 // if pixmaps were cleared, re-ask them
403 if (changedFlags & DocumentObserver::Pixmap)
404 d->slotRequestVisiblePixmaps();
405 }
406
notifyVisibleRectsChanged()407 void ThumbnailList::notifyVisibleRectsChanged()
408 {
409 bool found = false;
410 const QVector<Okular::VisiblePageRect *> &visibleRects = d->m_document->visiblePageRects();
411 QVector<ThumbnailWidget *>::const_iterator tIt = d->m_thumbnails.constBegin(), tEnd = d->m_thumbnails.constEnd();
412 QVector<Okular::VisiblePageRect *>::const_iterator vEnd = visibleRects.end();
413 for (; tIt != tEnd; ++tIt) {
414 found = false;
415 QVector<Okular::VisiblePageRect *>::const_iterator vIt = visibleRects.begin();
416 for (; (vIt != vEnd) && !found; ++vIt) {
417 if ((*tIt)->pageNumber() == (*vIt)->pageNumber) {
418 (*tIt)->setVisibleRect((*vIt)->rect);
419 found = true;
420 }
421 }
422 if (!found) {
423 (*tIt)->setVisibleRect(Okular::NormalizedRect());
424 }
425 }
426 }
427
canUnloadPixmap(int pageNumber) const428 bool ThumbnailList::canUnloadPixmap(int pageNumber) const
429 {
430 // if the thumbnail 'pageNumber' is one of the visible ones, forbid unloading
431 QList<ThumbnailWidget *>::const_iterator vIt = d->m_visibleThumbnails.constBegin(), vEnd = d->m_visibleThumbnails.constEnd();
432 for (; vIt != vEnd; ++vIt)
433 if ((*vIt)->pageNumber() == pageNumber)
434 return false;
435 // if hidden permit unloading
436 return true;
437 }
438 // END DocumentObserver inherited methods
439
updateWidgets()440 void ThumbnailList::updateWidgets()
441 {
442 // Update all visible widgets
443 QList<ThumbnailWidget *>::const_iterator vIt = d->m_visibleThumbnails.constBegin(), vEnd = d->m_visibleThumbnails.constEnd();
444 for (; vIt != vEnd; ++vIt) {
445 ThumbnailWidget *t = *vIt;
446 t->update();
447 }
448 }
449
getNewPageOffset(int n,ThumbnailListPrivate::ChangePageDirection dir) const450 int ThumbnailListPrivate::getNewPageOffset(int n, ThumbnailListPrivate::ChangePageDirection dir) const
451 {
452 int reason = 1;
453 int facingFirst = 0; // facingFirstCentered cornercase
454 if (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing)
455 reason = 2;
456 else if (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered) {
457 facingFirst = 1;
458 reason = 2;
459 } else if (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Summary)
460 reason = 3;
461 if (dir == ThumbnailListPrivate::Up) {
462 if (facingFirst && n == 1)
463 return -1;
464 return -reason;
465 }
466 if (dir == ThumbnailListPrivate::Down)
467 return reason;
468 if (dir == ThumbnailListPrivate::Left && reason > 1 && (n + facingFirst) % reason)
469 return -1;
470 if (dir == ThumbnailListPrivate::Right && reason > 1 && (n + 1 + facingFirst) % reason)
471 return 1;
472 return 0;
473 }
474
getThumbnailbyOffset(int current,int offset) const475 ThumbnailWidget *ThumbnailListPrivate::getThumbnailbyOffset(int current, int offset) const
476 {
477 QVector<ThumbnailWidget *>::const_iterator it = m_thumbnails.begin();
478 QVector<ThumbnailWidget *>::const_iterator itE = m_thumbnails.end();
479 int idx = 0;
480 while (it != itE) {
481 if ((*it)->pageNumber() == current)
482 break;
483 ++idx;
484 ++it;
485 }
486 if (it == itE)
487 return nullptr;
488 idx += offset;
489 if (idx < 0 || idx >= m_thumbnails.size())
490 return nullptr;
491 return m_thumbnails[idx];
492 }
493
forwardTrack(const QPoint point,const QSize r)494 ThumbnailListPrivate::ChangePageDirection ThumbnailListPrivate::forwardTrack(const QPoint point, const QSize r)
495 {
496 Okular::DocumentViewport vp = m_document->viewport();
497 const double deltaX = (double)point.x() / r.width(), deltaY = (double)point.y() / r.height();
498 vp.rePos.normalizedX -= deltaX;
499 vp.rePos.normalizedY -= deltaY;
500 if (vp.rePos.normalizedY > 1.0)
501 return ThumbnailListPrivate::Down;
502 if (vp.rePos.normalizedY < 0.0)
503 return ThumbnailListPrivate::Up;
504 if (vp.rePos.normalizedX > 1.0)
505 return ThumbnailListPrivate::Right;
506 if (vp.rePos.normalizedX < 0.0)
507 return ThumbnailListPrivate::Left;
508 vp.rePos.enabled = true;
509 m_document->setViewport(vp);
510 return ThumbnailListPrivate::Null;
511 }
512
getBookmarkOverlay() const513 const QPixmap *ThumbnailListPrivate::getBookmarkOverlay() const
514 {
515 return m_bookmarkOverlay;
516 }
517
slotFilterBookmarks(bool filterOn)518 void ThumbnailList::slotFilterBookmarks(bool filterOn)
519 {
520 // save state
521 Okular::Settings::setFilterBookmarks(filterOn);
522 Okular::Settings::self()->save();
523 // ask for the 'notifySetup' with a little trick (on reinsertion the
524 // document sends the list again)
525 d->m_document->removeObserver(this);
526 d->m_document->addObserver(this);
527 }
528
529 // BEGIN widget events
keyPressEvent(QKeyEvent * keyEvent)530 void ThumbnailList::keyPressEvent(QKeyEvent *keyEvent)
531 {
532 if (d->m_thumbnails.count() < 1) {
533 keyEvent->ignore();
534 return;
535 }
536
537 int nextPage = -1;
538 if (keyEvent->key() == Qt::Key_Up) {
539 if (!d->m_selected)
540 nextPage = 0;
541 else if (d->m_vectorIndex > 0)
542 nextPage = d->m_thumbnails[d->m_vectorIndex - 1]->pageNumber();
543 } else if (keyEvent->key() == Qt::Key_Down) {
544 if (!d->m_selected)
545 nextPage = 0;
546 else if (d->m_vectorIndex < (int)d->m_thumbnails.count() - 1)
547 nextPage = d->m_thumbnails[d->m_vectorIndex + 1]->pageNumber();
548 } else if (keyEvent->key() == Qt::Key_PageUp)
549 verticalScrollBar()->triggerAction(QScrollBar::SliderPageStepSub);
550 else if (keyEvent->key() == Qt::Key_PageDown)
551 verticalScrollBar()->triggerAction(QScrollBar::SliderPageStepAdd);
552 else if (keyEvent->key() == Qt::Key_Home)
553 nextPage = d->m_thumbnails[0]->pageNumber();
554 else if (keyEvent->key() == Qt::Key_End)
555 nextPage = d->m_thumbnails[d->m_thumbnails.count() - 1]->pageNumber();
556
557 if (nextPage == -1) {
558 keyEvent->ignore();
559 return;
560 }
561
562 keyEvent->accept();
563 if (d->m_selected)
564 d->m_selected->setSelected(false);
565 d->m_selected = nullptr;
566 d->m_document->setViewportPage(nextPage);
567 }
568
viewportEvent(QEvent * e)569 bool ThumbnailList::viewportEvent(QEvent *e)
570 {
571 switch (e->type()) {
572 case QEvent::Resize: {
573 d->viewportResizeEvent((QResizeEvent *)e);
574 break;
575 }
576 default:;
577 }
578 return QScrollArea::viewportEvent(e);
579 }
580
viewportResizeEvent(QResizeEvent * e)581 void ThumbnailListPrivate::viewportResizeEvent(QResizeEvent *e)
582 {
583 if (m_thumbnails.count() < 1 || width() < 1)
584 return;
585
586 // if width changed resize all the Thumbnails, reposition them to the
587 // right place and recalculate the contents area
588 if (e->size().width() != e->oldSize().width()) {
589 // runs the timer avoiding a thumbnail regeneration by 'contentsMoving'
590 delayedRequestVisiblePixmaps(2000);
591
592 // resize and reposition items
593 const int newWidth = q->viewport()->width();
594 int newHeight = 0;
595 QVector<ThumbnailWidget *>::const_iterator tIt = m_thumbnails.constBegin(), tEnd = m_thumbnails.constEnd();
596 for (; tIt != tEnd; ++tIt) {
597 ThumbnailWidget *t = *tIt;
598 t->move(0, newHeight);
599 t->resizeFitWidth(newWidth);
600 newHeight += t->height() + this->style()->layoutSpacing(QSizePolicy::Frame, QSizePolicy::Frame, Qt::Vertical);
601 }
602
603 // update scrollview's contents size (sets scrollbars limits)
604 newHeight -= this->style()->layoutSpacing(QSizePolicy::Frame, QSizePolicy::Frame, Qt::Vertical);
605 const int oldHeight = q->widget()->height();
606 const int oldYCenter = q->verticalScrollBar()->value() + q->viewport()->height() / 2;
607 q->widget()->resize(newWidth, newHeight);
608
609 // enable scrollbar when there's something to scroll
610 q->verticalScrollBar()->setEnabled(q->viewport()->height() < newHeight);
611
612 // ensure that what was visible before remains visible now
613 q->ensureVisible(0, int((qreal)oldYCenter * q->widget()->height() / oldHeight), 0, q->viewport()->height() / 2);
614 } else if (e->size().height() <= e->oldSize().height())
615 return;
616
617 // invalidate the bookmark overlay
618 if (m_bookmarkOverlay) {
619 delete m_bookmarkOverlay;
620 m_bookmarkOverlay = nullptr;
621 }
622
623 // update Thumbnails since width has changed or height has increased
624 delayedRequestVisiblePixmaps(500);
625 }
626 // END widget events
627
628 // BEGIN internal SLOTS
slotRequestVisiblePixmaps()629 void ThumbnailListPrivate::slotRequestVisiblePixmaps()
630 {
631 // if an update is already scheduled or the widget is hidden, don't proceed
632 if ((m_delayTimer && m_delayTimer->isActive()) || q->isHidden())
633 return;
634
635 // scroll from the top to the last visible thumbnail
636 m_visibleThumbnails.clear();
637 QLinkedList<Okular::PixmapRequest *> requestedPixmaps;
638 QVector<ThumbnailWidget *>::const_iterator tIt = m_thumbnails.constBegin(), tEnd = m_thumbnails.constEnd();
639 const QRect viewportRect = q->viewport()->rect().translated(q->horizontalScrollBar()->value(), q->verticalScrollBar()->value());
640 for (; tIt != tEnd; ++tIt) {
641 ThumbnailWidget *t = *tIt;
642 const QRect thumbRect = t->rect();
643 if (!thumbRect.intersects(viewportRect))
644 continue;
645 // add ThumbnailWidget to visible list
646 m_visibleThumbnails.push_back(t);
647 // if pixmap not present add it to requests
648 if (!t->page()->hasPixmap(q, t->pixmapWidth(), t->pixmapHeight())) {
649 Okular::PixmapRequest *p = new Okular::PixmapRequest(q, t->pageNumber(), t->pixmapWidth(), t->pixmapHeight(), devicePixelRatioF(), THUMBNAILS_PRIO, Okular::PixmapRequest::Asynchronous);
650 requestedPixmaps.push_back(p);
651 }
652 }
653
654 // actually request pixmaps
655 if (!requestedPixmaps.isEmpty())
656 m_document->requestPixmaps(requestedPixmaps);
657 }
658
slotDelayTimeout()659 void ThumbnailListPrivate::slotDelayTimeout()
660 {
661 // resize the bookmark overlay
662 delete m_bookmarkOverlay;
663 const int expectedWidth = q->viewport()->width() / 4;
664 if (expectedWidth > 10)
665 m_bookmarkOverlay = new QPixmap(QIcon::fromTheme(QStringLiteral("bookmarks")).pixmap(expectedWidth));
666 else
667 m_bookmarkOverlay = nullptr;
668
669 // request pixmaps
670 slotRequestVisiblePixmaps();
671 }
672 // END internal SLOTS
673
delayedRequestVisiblePixmaps(int delayMs)674 void ThumbnailListPrivate::delayedRequestVisiblePixmaps(int delayMs)
675 {
676 if (!m_delayTimer) {
677 m_delayTimer = new QTimer(q);
678 m_delayTimer->setSingleShot(true);
679 connect(m_delayTimer, &QTimer::timeout, this, &ThumbnailListPrivate::slotDelayTimeout);
680 }
681 m_delayTimer->start(delayMs);
682 }
683
684 /** ThumbnailWidget implementation **/
685
ThumbnailWidget(ThumbnailListPrivate * parent,const Okular::Page * page)686 ThumbnailWidget::ThumbnailWidget(ThumbnailListPrivate *parent, const Okular::Page *page)
687 : m_parent(parent)
688 , m_page(page)
689 , m_selected(false)
690 , m_pixmapWidth(10)
691 , m_pixmapHeight(10)
692 {
693 m_labelNumber = m_page->number() + 1;
694 m_labelHeight = QFontMetrics(m_parent->font()).height();
695 }
696
resizeFitWidth(int width)697 void ThumbnailWidget::resizeFitWidth(int width)
698 {
699 m_pixmapWidth = width - m_margin;
700 m_pixmapHeight = qRound(m_page->ratio() * (double)m_pixmapWidth);
701 m_rect.setSize(QSize(width, heightHint()));
702 }
703
setSelected(bool selected)704 void ThumbnailWidget::setSelected(bool selected)
705 {
706 // update selected state
707 if (m_selected != selected) {
708 m_selected = selected;
709 update();
710 }
711 }
712
setVisibleRect(const Okular::NormalizedRect & rect)713 void ThumbnailWidget::setVisibleRect(const Okular::NormalizedRect &rect)
714 {
715 if (rect == m_visibleRect)
716 return;
717
718 m_visibleRect = rect;
719 update();
720 }
721
mousePressEvent(QMouseEvent * e)722 void ThumbnailListPrivate::mousePressEvent(QMouseEvent *e)
723 {
724 ThumbnailWidget *item = itemFor(e->pos());
725 if (!item) { // mouse on the spacing between items
726 e->ignore();
727 return;
728 }
729
730 const QRect r = item->visibleRect();
731 const int margin = ThumbnailWidget::margin();
732 const QPoint p = e->pos() - item->pos();
733
734 if (e->button() != Qt::RightButton && r.contains(p - QPoint(margin / 2, margin / 2))) {
735 m_mouseGrabPos.setX(0);
736 m_mouseGrabPos.setY(0);
737 m_mouseGrabItem = item;
738 m_pageCurrentlyGrabbed = item->pageNumber();
739 m_mouseGrabItem = item;
740 } else {
741 m_mouseGrabPos.setX(0);
742 m_mouseGrabPos.setY(0);
743 m_mouseGrabItem = nullptr;
744 }
745
746 CursorWrapHelper::startDrag();
747 }
748
mouseReleaseEvent(QMouseEvent * e)749 void ThumbnailListPrivate::mouseReleaseEvent(QMouseEvent *e)
750 {
751 ThumbnailWidget *item = itemFor(e->pos());
752 m_mouseGrabItem = item;
753 if (!item) { // mouse on the spacing between items
754 e->ignore();
755 return;
756 }
757
758 QRect r = item->visibleRect();
759 const QPoint p = e->pos() - item->pos();
760
761 // jump center of viewport to cursor if it wasn't dragged
762 if (m_mouseGrabPos.isNull()) {
763 r = item->visibleRect();
764 Okular::DocumentViewport vp = Okular::DocumentViewport(item->pageNumber());
765 vp.rePos.normalizedX = double(p.x()) / double(item->rect().width());
766 vp.rePos.normalizedY = double(p.y()) / double(item->rect().height());
767 vp.rePos.pos = Okular::DocumentViewport::Center;
768 vp.rePos.enabled = true;
769 m_document->setViewport(vp, nullptr, true);
770 }
771 setCursor(Qt::OpenHandCursor);
772 m_mouseGrabPos.setX(0);
773 m_mouseGrabPos.setY(0);
774 }
775
mouseMoveEvent(QMouseEvent * e)776 void ThumbnailListPrivate::mouseMoveEvent(QMouseEvent *e)
777 {
778 if (e->buttons() == Qt::NoButton) {
779 ThumbnailWidget *item = itemFor(e->pos());
780 if (!item) { // mouse on the spacing between items
781 e->ignore();
782 return;
783 }
784
785 QRect r = item->visibleRect();
786 const int margin = ThumbnailWidget::margin();
787 const QPoint p = e->pos() - item->pos();
788 if (r.contains(p - QPoint(margin / 2, margin / 2))) {
789 setCursor(Qt::OpenHandCursor);
790 } else {
791 setCursor(Qt::ArrowCursor);
792 }
793
794 e->ignore();
795 return;
796 }
797 // no item under the mouse or previously selected
798 if (!m_mouseGrabItem) {
799 e->ignore();
800 return;
801 }
802 const QRect r = m_mouseGrabItem->rect();
803 if (!m_mouseGrabPos.isNull()) {
804 const QPoint mousePos = e->pos();
805 const QPoint delta = m_mouseGrabPos - mousePos;
806 m_mouseGrabPos = e->pos();
807 // don't handle the mouse move, forward it to the thumbnail list
808 ThumbnailListPrivate::ChangePageDirection direction;
809 if ((direction = forwardTrack(delta, r.size())) != ThumbnailListPrivate::Null) {
810 // Changing the selected page
811 const int offset = getNewPageOffset(m_pageCurrentlyGrabbed, direction);
812 const ThumbnailWidget *newThumb = getThumbnailbyOffset(m_pageCurrentlyGrabbed, offset);
813 if (!newThumb)
814 return;
815 int newPageOn = newThumb->pageNumber();
816 if (newPageOn == m_pageCurrentlyGrabbed || newPageOn < 0 || newPageOn >= (int)m_document->pages()) {
817 return;
818 }
819 Okular::DocumentViewport vp = m_document->viewport();
820 const float origNormalX = vp.rePos.normalizedX;
821 const float origNormalY = vp.rePos.normalizedY;
822 vp = Okular::DocumentViewport(newPageOn);
823 vp.rePos.normalizedX = origNormalX;
824 vp.rePos.normalizedY = origNormalY;
825 if (direction == ThumbnailListPrivate::Up) {
826 vp.rePos.normalizedY = 1.0;
827 if (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered && !newPageOn) {
828 if (m_pageCurrentlyGrabbed == 1)
829 vp.rePos.normalizedX = origNormalX - 0.5;
830 else
831 vp.rePos.normalizedX = origNormalX + 0.5;
832 if (vp.rePos.normalizedX < 0.0)
833 vp.rePos.normalizedX = 0.0;
834 else if (vp.rePos.normalizedX > 1.0)
835 vp.rePos.normalizedX = 1.0;
836 }
837 } else if (direction == ThumbnailListPrivate::Down) {
838 vp.rePos.normalizedY = 0.0;
839 if (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered && !m_pageCurrentlyGrabbed) {
840 if (origNormalX < 0.5) {
841 vp = Okular::DocumentViewport(--newPageOn);
842 vp.rePos.normalizedX = origNormalX + 0.5;
843 } else
844 vp.rePos.normalizedX = origNormalX - 0.5;
845 if (vp.rePos.normalizedX < 0.0)
846 vp.rePos.normalizedX = 0.0;
847 else if (vp.rePos.normalizedX > 1.0)
848 vp.rePos.normalizedX = 1.0;
849 }
850 } else if (Okular::Settings::viewMode() != Okular::Settings::EnumViewMode::Single) {
851 if (direction == ThumbnailListPrivate::Left)
852 vp.rePos.normalizedX = 1.0;
853 else
854 vp.rePos.normalizedX = 0.0;
855 }
856 vp.rePos.pos = Okular::DocumentViewport::Center;
857 vp.rePos.enabled = true;
858 m_document->setViewport(vp);
859 m_mouseGrabPos.setX(0);
860 m_mouseGrabPos.setY(0);
861 m_pageCurrentlyGrabbed = newPageOn;
862 m_mouseGrabItem = getPageByNumber(m_pageCurrentlyGrabbed);
863 }
864
865 // Wrap mouse cursor
866 if (!CursorWrapHelper::wrapCursor(mousePos, Qt::TopEdge | Qt::BottomEdge).isNull()) {
867 m_mouseGrabPos.setX(0);
868 m_mouseGrabPos.setY(0);
869 }
870 } else {
871 setCursor(Qt::ClosedHandCursor);
872 m_mouseGrabPos = e->pos();
873 }
874 }
875
wheelEvent(QWheelEvent * e)876 void ThumbnailListPrivate::wheelEvent(QWheelEvent *e)
877 {
878 const ThumbnailWidget *item = itemFor(e->pos());
879 if (!item) { // wheeling on the spacing between items
880 e->ignore();
881 return;
882 }
883
884 const QRect r = item->visibleRect();
885 const int margin = ThumbnailWidget::margin();
886
887 if (r.contains(e->pos() - QPoint(margin / 2, margin / 2)) && e->orientation() == Qt::Vertical && e->modifiers() == Qt::ControlModifier) {
888 m_document->setZoom(e->angleDelta().y());
889 } else {
890 e->ignore();
891 }
892 }
893
contextMenuEvent(QContextMenuEvent * e)894 void ThumbnailListPrivate::contextMenuEvent(QContextMenuEvent *e)
895 {
896 const ThumbnailWidget *item = itemFor(e->pos());
897 if (item) {
898 emit q->rightClick(item->page(), e->globalPos());
899 }
900 }
901
paint(QPainter & p,const QRect _clipRect)902 void ThumbnailWidget::paint(QPainter &p, const QRect _clipRect)
903 {
904 const int width = m_pixmapWidth + m_margin;
905 QRect clipRect = _clipRect;
906 const QPalette pal = m_parent->palette();
907
908 // draw the bottom label + highlight mark
909 const QColor fillColor = m_selected ? pal.color(QPalette::Active, QPalette::Highlight) : pal.color(QPalette::Active, QPalette::Base);
910 p.fillRect(clipRect, fillColor);
911 p.setPen(m_selected ? pal.color(QPalette::Active, QPalette::HighlightedText) : pal.color(QPalette::Active, QPalette::Text));
912 p.drawText(0, m_pixmapHeight + (m_margin - 3), width, m_labelHeight, Qt::AlignCenter, QString::number(m_labelNumber));
913
914 // draw page outline and pixmap
915 if (clipRect.top() < m_pixmapHeight + m_margin) {
916 // if page is bookmarked draw a colored border
917 const bool isBookmarked = m_parent->m_document->bookmarkManager()->isBookmarked(pageNumber());
918 // draw the inner rect
919 p.setPen(isBookmarked ? QColor(0xFF8000) : Qt::black);
920 p.drawRect(m_margin / 2 - 1, m_margin / 2 - 1, m_pixmapWidth + 1, m_pixmapHeight + 1);
921 // draw the clear rect
922 p.setPen(isBookmarked ? QColor(0x804000) : pal.color(QPalette::Active, QPalette::Base));
923 // draw the bottom and right shadow edges
924 if (!isBookmarked) {
925 int left, right, bottom, top;
926 left = m_margin / 2 + 1;
927 right = m_margin / 2 + m_pixmapWidth + 1;
928 bottom = m_pixmapHeight + m_margin / 2 + 1;
929 top = m_margin / 2 + 1;
930 p.setPen(Qt::gray);
931 p.drawLine(left, bottom, right, bottom);
932 p.drawLine(right, top, right, bottom);
933 }
934
935 // draw the page using the shared PagePainter class
936 p.translate(m_margin / 2.0, m_margin / 2.0);
937 clipRect.translate(-m_margin / 2, -m_margin / 2);
938 clipRect = clipRect.intersected(QRect(0, 0, m_pixmapWidth, m_pixmapHeight));
939 if (clipRect.isValid()) {
940 int flags = PagePainter::Accessibility | PagePainter::Highlights | PagePainter::Annotations;
941 PagePainter::paintPageOnPainter(&p, m_page, m_parent->q, flags, m_pixmapWidth, m_pixmapHeight, clipRect);
942 }
943
944 if (!m_visibleRect.isNull()) {
945 p.save();
946 p.setPen(QColor(255, 255, 0, 200));
947 p.setBrush(QColor(0, 0, 0, 100));
948 p.drawRect(m_visibleRect.geometry(m_pixmapWidth, m_pixmapHeight).adjusted(0, 0, -1, -1));
949 p.restore();
950 }
951
952 // draw the bookmark overlay on the top-right corner
953 const QPixmap *bookmarkPixmap = m_parent->getBookmarkOverlay();
954 if (isBookmarked && bookmarkPixmap) {
955 int pixW = bookmarkPixmap->width(), pixH = bookmarkPixmap->height();
956 clipRect = clipRect.intersected(QRect(m_pixmapWidth - pixW, 0, pixW, pixH));
957 if (clipRect.isValid())
958 p.drawPixmap(m_pixmapWidth - pixW, -pixH / 8, *bookmarkPixmap);
959 }
960 }
961 }
962
963 /** ThumbnailsController implementation **/
964
965 #define FILTERB_ID 1
966
ThumbnailController(QWidget * parent,ThumbnailList * list)967 ThumbnailController::ThumbnailController(QWidget *parent, ThumbnailList *list)
968 : QToolBar(parent)
969 {
970 setObjectName(QStringLiteral("ThumbsControlBar"));
971 // change toolbar appearance
972 setIconSize(QSize(16, 16));
973 setMovable(false);
974 QSizePolicy sp = sizePolicy();
975 sp.setVerticalPolicy(QSizePolicy::Minimum);
976 setSizePolicy(sp);
977
978 // insert a togglebutton [show only bookmarked pages]
979 // insertSeparator();
980 QAction *showBoomarkOnlyAction = addAction(QIcon::fromTheme(QStringLiteral("bookmarks")), i18n("Show bookmarked pages only"));
981 showBoomarkOnlyAction->setCheckable(true);
982 connect(showBoomarkOnlyAction, &QAction::toggled, list, &ThumbnailList::slotFilterBookmarks);
983 showBoomarkOnlyAction->setChecked(Okular::Settings::filterBookmarks());
984 // insertLineSeparator();
985 }
986
987 #include "thumbnaillist.moc"
988
989 /* kate: replace-tabs on; indent-width 4; */
990