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