/*
    SPDX-FileCopyrightText: 2004-2006 Albert Astals Cid <aacid@kde.org>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "thumbnaillist.h"

// qt/kde includes
#include <QAction>
#include <QApplication>
#include <QIcon>
#include <QPainter>
#include <QResizeEvent>
#include <QScrollBar>
#include <QSizePolicy>
#include <QStyle>
#include <QTimer>
#include <QVBoxLayout>

#include <KActionCollection>
#include <KLocalizedString>
#include <KTitleWidget>

#include <kwidgetsaddons_version.h>

// local includes
#include "core/area.h"
#include "core/bookmarkmanager.h"
#include "core/document.h"
#include "core/generator.h"
#include "core/page.h"
#include "cursorwraphelper.h"
#include "pagepainter.h"
#include "priorities.h"
#include "settings.h"

class ThumbnailWidget;

ThumbnailsBox::ThumbnailsBox(QWidget *parent)
    : QWidget(parent)
{
    QVBoxLayout *vbox = new QVBoxLayout(this);
    vbox->setSpacing(0);

    KTitleWidget *titleWidget = new KTitleWidget(this);
    titleWidget->setLevel(2);
    titleWidget->setText(i18n("Thumbnails"));
    vbox->addWidget(titleWidget);
    vbox->setAlignment(titleWidget, Qt::AlignHCenter);
}

QSize ThumbnailsBox::sizeHint() const
{
    return QSize();
}

class ThumbnailListPrivate : public QWidget
{
    Q_OBJECT
public:
    ThumbnailListPrivate(ThumbnailList *qq, Okular::Document *document);
    ~ThumbnailListPrivate() override;

    enum ChangePageDirection { Null, Left, Right, Up, Down };

    ThumbnailList *q;
    Okular::Document *m_document;
    ThumbnailWidget *m_selected;
    QTimer *m_delayTimer;
    QPixmap *m_bookmarkOverlay;
    QVector<ThumbnailWidget *> m_thumbnails;
    QList<ThumbnailWidget *> m_visibleThumbnails;
    int m_vectorIndex;
    // Grabbing variables
    QPoint m_mouseGrabPos;
    ThumbnailWidget *m_mouseGrabItem;
    int m_pageCurrentlyGrabbed;

    // resize thumbnails to fit the width
    void viewportResizeEvent(QResizeEvent *);
    // called by ThumbnailWidgets to get the overlay bookmark pixmap
    const QPixmap *getBookmarkOverlay() const;
    // called by ThumbnailWidgets to send (forward) the mouse move signals
    ChangePageDirection forwardTrack(const QPoint, const QSize);

    ThumbnailWidget *itemFor(const QPoint p) const;
    void delayedRequestVisiblePixmaps(int delayMs = 0);

    // SLOTS:
    // make requests for generating pixmaps for visible thumbnails
    void slotRequestVisiblePixmaps();
    // delay timeout: resize overlays and requests pixmaps
    void slotDelayTimeout();
    ThumbnailWidget *getPageByNumber(int page) const;
    int getNewPageOffset(int n, ThumbnailListPrivate::ChangePageDirection dir) const;
    ThumbnailWidget *getThumbnailbyOffset(int current, int offset) const;

protected:
    void mousePressEvent(QMouseEvent *e) override;
    void mouseReleaseEvent(QMouseEvent *e) override;
    void mouseMoveEvent(QMouseEvent *e) override;
    void wheelEvent(QWheelEvent *e) override;
    void contextMenuEvent(QContextMenuEvent *e) override;
    void paintEvent(QPaintEvent *e) override;
};

// ThumbnailWidget represents a single thumbnail in the ThumbnailList
class ThumbnailWidget
{
public:
    ThumbnailWidget(ThumbnailListPrivate *parent, const Okular::Page *page);

    // set internal parameters to fit the page in the given width
    void resizeFitWidth(int width);
    // set thumbnail's selected state
    void setSelected(bool selected);
    // set the visible rect of the current page
    void setVisibleRect(const Okular::NormalizedRect &rect);

    // query methods
    int heightHint() const
    {
        return m_pixmapHeight + m_labelHeight + m_margin;
    }
    int pixmapWidth() const
    {
        return m_pixmapWidth;
    }
    int pixmapHeight() const
    {
        return m_pixmapHeight;
    }
    int pageNumber() const
    {
        return m_page->number();
    }
    const Okular::Page *page() const
    {
        return m_page;
    }
    QRect visibleRect() const
    {
        return m_visibleRect.geometry(m_pixmapWidth, m_pixmapHeight);
    }

    void paint(QPainter &p, const QRect clipRect);

    static int margin()
    {
        return m_margin;
    }

    // simulating QWidget
    QRect rect() const
    {
        return m_rect;
    }
    int height() const
    {
        return m_rect.height();
    }
    int width() const
    {
        return m_rect.width();
    }
    QPoint pos() const
    {
        return m_rect.topLeft();
    }
    void move(int x, int y)
    {
        m_rect.setTopLeft(QPoint(x, y));
    }
    void update()
    {
        m_parent->update(m_rect);
    }
    void update(const QRect rect)
    {
        m_parent->update(rect.translated(m_rect.topLeft()));
    }

private:
    // the margin around the widget
    static int const m_margin = 16;

    ThumbnailListPrivate *m_parent;
    const Okular::Page *m_page;
    bool m_selected;
    int m_pixmapWidth, m_pixmapHeight;
    int m_labelHeight, m_labelNumber;
    Okular::NormalizedRect m_visibleRect;
    QRect m_rect;
};

ThumbnailListPrivate::ThumbnailListPrivate(ThumbnailList *qq, Okular::Document *document)
    : QWidget()
    , q(qq)
    , m_document(document)
    , m_selected(nullptr)
    , m_delayTimer(nullptr)
    , m_bookmarkOverlay(nullptr)
    , m_vectorIndex(0)
{
    setMouseTracking(true);
    m_mouseGrabItem = nullptr;
}

ThumbnailWidget *ThumbnailListPrivate::getPageByNumber(int page) const
{
    QVector<ThumbnailWidget *>::const_iterator tIt = m_thumbnails.constBegin(), tEnd = m_thumbnails.constEnd();
    for (; tIt != tEnd; ++tIt) {
        if ((*tIt)->pageNumber() == page)
            return (*tIt);
    }
    return nullptr;
}

ThumbnailListPrivate::~ThumbnailListPrivate()
{
}

ThumbnailWidget *ThumbnailListPrivate::itemFor(const QPoint p) const
{
    QVector<ThumbnailWidget *>::const_iterator tIt = m_thumbnails.constBegin(), tEnd = m_thumbnails.constEnd();
    for (; tIt != tEnd; ++tIt) {
        if ((*tIt)->rect().contains(p))
            return (*tIt);
    }
    return nullptr;
}

void ThumbnailListPrivate::paintEvent(QPaintEvent *e)
{
    QPainter painter(this);
    QVector<ThumbnailWidget *>::const_iterator tIt = m_thumbnails.constBegin(), tEnd = m_thumbnails.constEnd();
    for (; tIt != tEnd; ++tIt) {
        QRect rect = e->rect().intersected((*tIt)->rect());
        if (!rect.isNull()) {
            rect.translate(-(*tIt)->pos());
            painter.save();
            painter.translate((*tIt)->pos());
            (*tIt)->paint(painter, rect);
            painter.restore();
        }
    }
}

/** ThumbnailList implementation **/

ThumbnailList::ThumbnailList(QWidget *parent, Okular::Document *document)
    : QScrollArea(parent)
    , d(new ThumbnailListPrivate(this, document))
{
    setObjectName(QStringLiteral("okular::Thumbnails"));
    // set scrollbars
    setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
    verticalScrollBar()->setEnabled(false);

    setAttribute(Qt::WA_StaticContents);

    viewport()->setBackgroundRole(QPalette::Base);

    setWidget(d);
    // widget setup: can be focused by mouse click (not wheel nor tab)
    widget()->setFocusPolicy(Qt::ClickFocus);
    widget()->show();
    widget()->setBackgroundRole(QPalette::Base);

    connect(verticalScrollBar(), &QScrollBar::valueChanged, d, &ThumbnailListPrivate::slotRequestVisiblePixmaps);
}

ThumbnailList::~ThumbnailList()
{
    d->m_document->removeObserver(this);
    delete d->m_bookmarkOverlay;
}

// BEGIN DocumentObserver inherited methods
void ThumbnailList::notifySetup(const QVector<Okular::Page *> &pages, int setupFlags)
{
    // if there was a widget selected, save its pagenumber to restore
    // its selection (if available in the new set of pages)
    int prevPage = -1;
    if (!(setupFlags & Okular::DocumentObserver::DocumentChanged) && d->m_selected) {
        prevPage = d->m_selected->page()->number();
    } else
        prevPage = d->m_document->viewport().pageNumber;

    // delete all the Thumbnails
    QVector<ThumbnailWidget *>::const_iterator tIt = d->m_thumbnails.constBegin(), tEnd = d->m_thumbnails.constEnd();
    for (; tIt != tEnd; ++tIt)
        delete *tIt;
    d->m_thumbnails.clear();
    d->m_visibleThumbnails.clear();
    d->m_selected = nullptr;
    d->m_mouseGrabItem = nullptr;

    if (pages.count() < 1) {
        widget()->resize(0, 0);
        return;
    }

    // show pages containing highlighted text or bookmarked ones
    // RESTORE THIS int flags = Okular::Settings::filterBookmarks() ? Okular::Page::Bookmark : Okular::Page::Highlight;

    // if no page matches filter rule, then display all pages
    QVector<Okular::Page *>::const_iterator pIt = pages.constBegin(), pEnd = pages.constEnd();
    bool skipCheck = true;
    for (; pIt != pEnd; ++pIt)
        // if ( (*pIt)->attributes() & flags )
        if ((*pIt)->hasHighlights(SW_SEARCH_ID))
            skipCheck = false;

    // generate Thumbnails for the given set of pages
    const int width = viewport()->width();
    int height = 0;
    int centerHeight = 0;
    for (pIt = pages.constBegin(); pIt != pEnd; ++pIt)
        // if ( skipCheck || (*pIt)->attributes() & flags )
        if (skipCheck || (*pIt)->hasHighlights(SW_SEARCH_ID)) {
            ThumbnailWidget *t = new ThumbnailWidget(d, *pIt);
            t->move(0, height);
            // add to the internal queue
            d->m_thumbnails.push_back(t);
            // update total height (asking widget its own height)
            t->resizeFitWidth(width);
            // restoring the previous selected page, if any
            if ((*pIt)->number() < prevPage) {
                centerHeight = height + t->height() + this->style()->layoutSpacing(QSizePolicy::Frame, QSizePolicy::Frame, Qt::Vertical) / 2;
            }
            if ((*pIt)->number() == prevPage) {
                d->m_selected = t;
                d->m_selected->setSelected(true);
                centerHeight = height + t->height() / 2;
            }
            height += t->height() + this->style()->layoutSpacing(QSizePolicy::Frame, QSizePolicy::Frame, Qt::Vertical);
        }

    // update scrollview's contents size (sets scrollbars limits)
    height -= this->style()->layoutSpacing(QSizePolicy::Frame, QSizePolicy::Frame, Qt::Vertical);
    widget()->resize(width, height);

    // enable scrollbar when there's something to scroll
    verticalScrollBar()->setEnabled(viewport()->height() < height);
    verticalScrollBar()->setValue(centerHeight - viewport()->height() / 2);

    // request for thumbnail generation
    d->delayedRequestVisiblePixmaps(200);
}

void ThumbnailList::notifyCurrentPageChanged(int previousPage, int currentPage)
{
    Q_UNUSED(previousPage)

    // skip notifies for the current page (already selected)
    if (d->m_selected && d->m_selected->pageNumber() == currentPage)
        return;

    // deselect previous thumbnail
    if (d->m_selected)
        d->m_selected->setSelected(false);
    d->m_selected = nullptr;

    // select the page with viewport and ensure it's centered in the view
    d->m_vectorIndex = 0;
    QVector<ThumbnailWidget *>::const_iterator tIt = d->m_thumbnails.constBegin(), tEnd = d->m_thumbnails.constEnd();
    for (; tIt != tEnd; ++tIt) {
        if ((*tIt)->pageNumber() == currentPage) {
            d->m_selected = *tIt;
            d->m_selected->setSelected(true);
            if (Okular::Settings::syncThumbnailsViewport()) {
                int yOffset = qMax(viewport()->height() / 4, d->m_selected->height() / 2);
                ensureVisible(0, d->m_selected->pos().y() + d->m_selected->height() / 2, 0, yOffset);
            }
            break;
        }
        d->m_vectorIndex++;
    }
}

void ThumbnailList::notifyPageChanged(int pageNumber, int changedFlags)
{
    static const int interestingFlags = DocumentObserver::Pixmap | DocumentObserver::Bookmark | DocumentObserver::Highlights | DocumentObserver::Annotations;
    // only handle change notifications we are interested in
    if (!(changedFlags & interestingFlags))
        return;

    // iterate over visible items: if page(pageNumber) is one of them, repaint it
    QList<ThumbnailWidget *>::const_iterator vIt = d->m_visibleThumbnails.constBegin(), vEnd = d->m_visibleThumbnails.constEnd();
    for (; vIt != vEnd; ++vIt)
        if ((*vIt)->pageNumber() == pageNumber) {
            (*vIt)->update();
            break;
        }
}

void ThumbnailList::notifyContentsCleared(int changedFlags)
{
    // if pixmaps were cleared, re-ask them
    if (changedFlags & DocumentObserver::Pixmap)
        d->slotRequestVisiblePixmaps();
}

void ThumbnailList::notifyVisibleRectsChanged()
{
    bool found = false;
    const QVector<Okular::VisiblePageRect *> &visibleRects = d->m_document->visiblePageRects();
    QVector<ThumbnailWidget *>::const_iterator tIt = d->m_thumbnails.constBegin(), tEnd = d->m_thumbnails.constEnd();
    QVector<Okular::VisiblePageRect *>::const_iterator vEnd = visibleRects.end();
    for (; tIt != tEnd; ++tIt) {
        found = false;
        QVector<Okular::VisiblePageRect *>::const_iterator vIt = visibleRects.begin();
        for (; (vIt != vEnd) && !found; ++vIt) {
            if ((*tIt)->pageNumber() == (*vIt)->pageNumber) {
                (*tIt)->setVisibleRect((*vIt)->rect);
                found = true;
            }
        }
        if (!found) {
            (*tIt)->setVisibleRect(Okular::NormalizedRect());
        }
    }
}

bool ThumbnailList::canUnloadPixmap(int pageNumber) const
{
    // if the thumbnail 'pageNumber' is one of the visible ones, forbid unloading
    QList<ThumbnailWidget *>::const_iterator vIt = d->m_visibleThumbnails.constBegin(), vEnd = d->m_visibleThumbnails.constEnd();
    for (; vIt != vEnd; ++vIt)
        if ((*vIt)->pageNumber() == pageNumber)
            return false;
    // if hidden permit unloading
    return true;
}
// END DocumentObserver inherited methods

void ThumbnailList::updateWidgets()
{
    // Update all visible widgets
    QList<ThumbnailWidget *>::const_iterator vIt = d->m_visibleThumbnails.constBegin(), vEnd = d->m_visibleThumbnails.constEnd();
    for (; vIt != vEnd; ++vIt) {
        ThumbnailWidget *t = *vIt;
        t->update();
    }
}

int ThumbnailListPrivate::getNewPageOffset(int n, ThumbnailListPrivate::ChangePageDirection dir) const
{
    int reason = 1;
    int facingFirst = 0; // facingFirstCentered cornercase
    if (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing)
        reason = 2;
    else if (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered) {
        facingFirst = 1;
        reason = 2;
    } else if (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Summary)
        reason = 3;
    if (dir == ThumbnailListPrivate::Up) {
        if (facingFirst && n == 1)
            return -1;
        return -reason;
    }
    if (dir == ThumbnailListPrivate::Down)
        return reason;
    if (dir == ThumbnailListPrivate::Left && reason > 1 && (n + facingFirst) % reason)
        return -1;
    if (dir == ThumbnailListPrivate::Right && reason > 1 && (n + 1 + facingFirst) % reason)
        return 1;
    return 0;
}

ThumbnailWidget *ThumbnailListPrivate::getThumbnailbyOffset(int current, int offset) const
{
    QVector<ThumbnailWidget *>::const_iterator it = m_thumbnails.begin();
    QVector<ThumbnailWidget *>::const_iterator itE = m_thumbnails.end();
    int idx = 0;
    while (it != itE) {
        if ((*it)->pageNumber() == current)
            break;
        ++idx;
        ++it;
    }
    if (it == itE)
        return nullptr;
    idx += offset;
    if (idx < 0 || idx >= m_thumbnails.size())
        return nullptr;
    return m_thumbnails[idx];
}

ThumbnailListPrivate::ChangePageDirection ThumbnailListPrivate::forwardTrack(const QPoint point, const QSize r)
{
    Okular::DocumentViewport vp = m_document->viewport();
    const double deltaX = (double)point.x() / r.width(), deltaY = (double)point.y() / r.height();
    vp.rePos.normalizedX -= deltaX;
    vp.rePos.normalizedY -= deltaY;
    if (vp.rePos.normalizedY > 1.0)
        return ThumbnailListPrivate::Down;
    if (vp.rePos.normalizedY < 0.0)
        return ThumbnailListPrivate::Up;
    if (vp.rePos.normalizedX > 1.0)
        return ThumbnailListPrivate::Right;
    if (vp.rePos.normalizedX < 0.0)
        return ThumbnailListPrivate::Left;
    vp.rePos.enabled = true;
    m_document->setViewport(vp);
    return ThumbnailListPrivate::Null;
}

const QPixmap *ThumbnailListPrivate::getBookmarkOverlay() const
{
    return m_bookmarkOverlay;
}

void ThumbnailList::slotFilterBookmarks(bool filterOn)
{
    // save state
    Okular::Settings::setFilterBookmarks(filterOn);
    Okular::Settings::self()->save();
    // ask for the 'notifySetup' with a little trick (on reinsertion the
    // document sends the list again)
    d->m_document->removeObserver(this);
    d->m_document->addObserver(this);
}

// BEGIN widget events
void ThumbnailList::keyPressEvent(QKeyEvent *keyEvent)
{
    if (d->m_thumbnails.count() < 1) {
        keyEvent->ignore();
        return;
    }

    int nextPage = -1;
    if (keyEvent->key() == Qt::Key_Up) {
        if (!d->m_selected)
            nextPage = 0;
        else if (d->m_vectorIndex > 0)
            nextPage = d->m_thumbnails[d->m_vectorIndex - 1]->pageNumber();
    } else if (keyEvent->key() == Qt::Key_Down) {
        if (!d->m_selected)
            nextPage = 0;
        else if (d->m_vectorIndex < (int)d->m_thumbnails.count() - 1)
            nextPage = d->m_thumbnails[d->m_vectorIndex + 1]->pageNumber();
    } else if (keyEvent->key() == Qt::Key_PageUp)
        verticalScrollBar()->triggerAction(QScrollBar::SliderPageStepSub);
    else if (keyEvent->key() == Qt::Key_PageDown)
        verticalScrollBar()->triggerAction(QScrollBar::SliderPageStepAdd);
    else if (keyEvent->key() == Qt::Key_Home)
        nextPage = d->m_thumbnails[0]->pageNumber();
    else if (keyEvent->key() == Qt::Key_End)
        nextPage = d->m_thumbnails[d->m_thumbnails.count() - 1]->pageNumber();

    if (nextPage == -1) {
        keyEvent->ignore();
        return;
    }

    keyEvent->accept();
    if (d->m_selected)
        d->m_selected->setSelected(false);
    d->m_selected = nullptr;
    d->m_document->setViewportPage(nextPage);
}

bool ThumbnailList::viewportEvent(QEvent *e)
{
    switch (e->type()) {
    case QEvent::Resize: {
        d->viewportResizeEvent((QResizeEvent *)e);
        break;
    }
    default:;
    }
    return QScrollArea::viewportEvent(e);
}

void ThumbnailListPrivate::viewportResizeEvent(QResizeEvent *e)
{
    if (m_thumbnails.count() < 1 || width() < 1)
        return;

    // if width changed resize all the Thumbnails, reposition them to the
    // right place and recalculate the contents area
    if (e->size().width() != e->oldSize().width()) {
        // runs the timer avoiding a thumbnail regeneration by 'contentsMoving'
        delayedRequestVisiblePixmaps(2000);

        // resize and reposition items
        const int newWidth = q->viewport()->width();
        int newHeight = 0;
        QVector<ThumbnailWidget *>::const_iterator tIt = m_thumbnails.constBegin(), tEnd = m_thumbnails.constEnd();
        for (; tIt != tEnd; ++tIt) {
            ThumbnailWidget *t = *tIt;
            t->move(0, newHeight);
            t->resizeFitWidth(newWidth);
            newHeight += t->height() + this->style()->layoutSpacing(QSizePolicy::Frame, QSizePolicy::Frame, Qt::Vertical);
        }

        // update scrollview's contents size (sets scrollbars limits)
        newHeight -= this->style()->layoutSpacing(QSizePolicy::Frame, QSizePolicy::Frame, Qt::Vertical);
        const int oldHeight = q->widget()->height();
        const int oldYCenter = q->verticalScrollBar()->value() + q->viewport()->height() / 2;
        q->widget()->resize(newWidth, newHeight);

        // enable scrollbar when there's something to scroll
        q->verticalScrollBar()->setEnabled(q->viewport()->height() < newHeight);

        // ensure that what was visible before remains visible now
        q->ensureVisible(0, int((qreal)oldYCenter * q->widget()->height() / oldHeight), 0, q->viewport()->height() / 2);
    } else if (e->size().height() <= e->oldSize().height())
        return;

    // invalidate the bookmark overlay
    if (m_bookmarkOverlay) {
        delete m_bookmarkOverlay;
        m_bookmarkOverlay = nullptr;
    }

    // update Thumbnails since width has changed or height has increased
    delayedRequestVisiblePixmaps(500);
}
// END widget events

// BEGIN internal SLOTS
void ThumbnailListPrivate::slotRequestVisiblePixmaps()
{
    // if an update is already scheduled or the widget is hidden, don't proceed
    if ((m_delayTimer && m_delayTimer->isActive()) || q->isHidden())
        return;

    // scroll from the top to the last visible thumbnail
    m_visibleThumbnails.clear();
    QLinkedList<Okular::PixmapRequest *> requestedPixmaps;
    QVector<ThumbnailWidget *>::const_iterator tIt = m_thumbnails.constBegin(), tEnd = m_thumbnails.constEnd();
    const QRect viewportRect = q->viewport()->rect().translated(q->horizontalScrollBar()->value(), q->verticalScrollBar()->value());
    for (; tIt != tEnd; ++tIt) {
        ThumbnailWidget *t = *tIt;
        const QRect thumbRect = t->rect();
        if (!thumbRect.intersects(viewportRect))
            continue;
        // add ThumbnailWidget to visible list
        m_visibleThumbnails.push_back(t);
        // if pixmap not present add it to requests
        if (!t->page()->hasPixmap(q, t->pixmapWidth(), t->pixmapHeight())) {
            Okular::PixmapRequest *p = new Okular::PixmapRequest(q, t->pageNumber(), t->pixmapWidth(), t->pixmapHeight(), devicePixelRatioF(), THUMBNAILS_PRIO, Okular::PixmapRequest::Asynchronous);
            requestedPixmaps.push_back(p);
        }
    }

    // actually request pixmaps
    if (!requestedPixmaps.isEmpty())
        m_document->requestPixmaps(requestedPixmaps);
}

void ThumbnailListPrivate::slotDelayTimeout()
{
    // resize the bookmark overlay
    delete m_bookmarkOverlay;
    const int expectedWidth = q->viewport()->width() / 4;
    if (expectedWidth > 10)
        m_bookmarkOverlay = new QPixmap(QIcon::fromTheme(QStringLiteral("bookmarks")).pixmap(expectedWidth));
    else
        m_bookmarkOverlay = nullptr;

    // request pixmaps
    slotRequestVisiblePixmaps();
}
// END internal SLOTS

void ThumbnailListPrivate::delayedRequestVisiblePixmaps(int delayMs)
{
    if (!m_delayTimer) {
        m_delayTimer = new QTimer(q);
        m_delayTimer->setSingleShot(true);
        connect(m_delayTimer, &QTimer::timeout, this, &ThumbnailListPrivate::slotDelayTimeout);
    }
    m_delayTimer->start(delayMs);
}

/** ThumbnailWidget implementation **/

ThumbnailWidget::ThumbnailWidget(ThumbnailListPrivate *parent, const Okular::Page *page)
    : m_parent(parent)
    , m_page(page)
    , m_selected(false)
    , m_pixmapWidth(10)
    , m_pixmapHeight(10)
{
    m_labelNumber = m_page->number() + 1;
    m_labelHeight = QFontMetrics(m_parent->font()).height();
}

void ThumbnailWidget::resizeFitWidth(int width)
{
    m_pixmapWidth = width - m_margin;
    m_pixmapHeight = qRound(m_page->ratio() * (double)m_pixmapWidth);
    m_rect.setSize(QSize(width, heightHint()));
}

void ThumbnailWidget::setSelected(bool selected)
{
    // update selected state
    if (m_selected != selected) {
        m_selected = selected;
        update();
    }
}

void ThumbnailWidget::setVisibleRect(const Okular::NormalizedRect &rect)
{
    if (rect == m_visibleRect)
        return;

    m_visibleRect = rect;
    update();
}

void ThumbnailListPrivate::mousePressEvent(QMouseEvent *e)
{
    ThumbnailWidget *item = itemFor(e->pos());
    if (!item) { // mouse on the spacing between items
        e->ignore();
        return;
    }

    const QRect r = item->visibleRect();
    const int margin = ThumbnailWidget::margin();
    const QPoint p = e->pos() - item->pos();

    if (e->button() != Qt::RightButton && r.contains(p - QPoint(margin / 2, margin / 2))) {
        m_mouseGrabPos.setX(0);
        m_mouseGrabPos.setY(0);
        m_mouseGrabItem = item;
        m_pageCurrentlyGrabbed = item->pageNumber();
        m_mouseGrabItem = item;
    } else {
        m_mouseGrabPos.setX(0);
        m_mouseGrabPos.setY(0);
        m_mouseGrabItem = nullptr;
    }

    CursorWrapHelper::startDrag();
}

void ThumbnailListPrivate::mouseReleaseEvent(QMouseEvent *e)
{
    ThumbnailWidget *item = itemFor(e->pos());
    m_mouseGrabItem = item;
    if (!item) { // mouse on the spacing between items
        e->ignore();
        return;
    }

    QRect r = item->visibleRect();
    const QPoint p = e->pos() - item->pos();

    // jump center of viewport to cursor if it wasn't dragged
    if (m_mouseGrabPos.isNull()) {
        r = item->visibleRect();
        Okular::DocumentViewport vp = Okular::DocumentViewport(item->pageNumber());
        vp.rePos.normalizedX = double(p.x()) / double(item->rect().width());
        vp.rePos.normalizedY = double(p.y()) / double(item->rect().height());
        vp.rePos.pos = Okular::DocumentViewport::Center;
        vp.rePos.enabled = true;
        m_document->setViewport(vp, nullptr, true);
    }
    setCursor(Qt::OpenHandCursor);
    m_mouseGrabPos.setX(0);
    m_mouseGrabPos.setY(0);
}

void ThumbnailListPrivate::mouseMoveEvent(QMouseEvent *e)
{
    if (e->buttons() == Qt::NoButton) {
        ThumbnailWidget *item = itemFor(e->pos());
        if (!item) { // mouse on the spacing between items
            e->ignore();
            return;
        }

        QRect r = item->visibleRect();
        const int margin = ThumbnailWidget::margin();
        const QPoint p = e->pos() - item->pos();
        if (r.contains(p - QPoint(margin / 2, margin / 2))) {
            setCursor(Qt::OpenHandCursor);
        } else {
            setCursor(Qt::ArrowCursor);
        }

        e->ignore();
        return;
    }
    // no item under the mouse or previously selected
    if (!m_mouseGrabItem) {
        e->ignore();
        return;
    }
    const QRect r = m_mouseGrabItem->rect();
    if (!m_mouseGrabPos.isNull()) {
        const QPoint mousePos = e->pos();
        const QPoint delta = m_mouseGrabPos - mousePos;
        m_mouseGrabPos = e->pos();
        // don't handle the mouse move, forward it to the thumbnail list
        ThumbnailListPrivate::ChangePageDirection direction;
        if ((direction = forwardTrack(delta, r.size())) != ThumbnailListPrivate::Null) {
            // Changing the selected page
            const int offset = getNewPageOffset(m_pageCurrentlyGrabbed, direction);
            const ThumbnailWidget *newThumb = getThumbnailbyOffset(m_pageCurrentlyGrabbed, offset);
            if (!newThumb)
                return;
            int newPageOn = newThumb->pageNumber();
            if (newPageOn == m_pageCurrentlyGrabbed || newPageOn < 0 || newPageOn >= (int)m_document->pages()) {
                return;
            }
            Okular::DocumentViewport vp = m_document->viewport();
            const float origNormalX = vp.rePos.normalizedX;
            const float origNormalY = vp.rePos.normalizedY;
            vp = Okular::DocumentViewport(newPageOn);
            vp.rePos.normalizedX = origNormalX;
            vp.rePos.normalizedY = origNormalY;
            if (direction == ThumbnailListPrivate::Up) {
                vp.rePos.normalizedY = 1.0;
                if (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered && !newPageOn) {
                    if (m_pageCurrentlyGrabbed == 1)
                        vp.rePos.normalizedX = origNormalX - 0.5;
                    else
                        vp.rePos.normalizedX = origNormalX + 0.5;
                    if (vp.rePos.normalizedX < 0.0)
                        vp.rePos.normalizedX = 0.0;
                    else if (vp.rePos.normalizedX > 1.0)
                        vp.rePos.normalizedX = 1.0;
                }
            } else if (direction == ThumbnailListPrivate::Down) {
                vp.rePos.normalizedY = 0.0;
                if (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered && !m_pageCurrentlyGrabbed) {
                    if (origNormalX < 0.5) {
                        vp = Okular::DocumentViewport(--newPageOn);
                        vp.rePos.normalizedX = origNormalX + 0.5;
                    } else
                        vp.rePos.normalizedX = origNormalX - 0.5;
                    if (vp.rePos.normalizedX < 0.0)
                        vp.rePos.normalizedX = 0.0;
                    else if (vp.rePos.normalizedX > 1.0)
                        vp.rePos.normalizedX = 1.0;
                }
            } else if (Okular::Settings::viewMode() != Okular::Settings::EnumViewMode::Single) {
                if (direction == ThumbnailListPrivate::Left)
                    vp.rePos.normalizedX = 1.0;
                else
                    vp.rePos.normalizedX = 0.0;
            }
            vp.rePos.pos = Okular::DocumentViewport::Center;
            vp.rePos.enabled = true;
            m_document->setViewport(vp);
            m_mouseGrabPos.setX(0);
            m_mouseGrabPos.setY(0);
            m_pageCurrentlyGrabbed = newPageOn;
            m_mouseGrabItem = getPageByNumber(m_pageCurrentlyGrabbed);
        }

        // Wrap mouse cursor
        if (!CursorWrapHelper::wrapCursor(mousePos, Qt::TopEdge | Qt::BottomEdge).isNull()) {
            m_mouseGrabPos.setX(0);
            m_mouseGrabPos.setY(0);
        }
    } else {
        setCursor(Qt::ClosedHandCursor);
        m_mouseGrabPos = e->pos();
    }
}

void ThumbnailListPrivate::wheelEvent(QWheelEvent *e)
{
    const ThumbnailWidget *item = itemFor(e->pos());
    if (!item) { // wheeling on the spacing between items
        e->ignore();
        return;
    }

    const QRect r = item->visibleRect();
    const int margin = ThumbnailWidget::margin();

    if (r.contains(e->pos() - QPoint(margin / 2, margin / 2)) && e->orientation() == Qt::Vertical && e->modifiers() == Qt::ControlModifier) {
        m_document->setZoom(e->angleDelta().y());
    } else {
        e->ignore();
    }
}

void ThumbnailListPrivate::contextMenuEvent(QContextMenuEvent *e)
{
    const ThumbnailWidget *item = itemFor(e->pos());
    if (item) {
        emit q->rightClick(item->page(), e->globalPos());
    }
}

void ThumbnailWidget::paint(QPainter &p, const QRect _clipRect)
{
    const int width = m_pixmapWidth + m_margin;
    QRect clipRect = _clipRect;
    const QPalette pal = m_parent->palette();

    // draw the bottom label + highlight mark
    const QColor fillColor = m_selected ? pal.color(QPalette::Active, QPalette::Highlight) : pal.color(QPalette::Active, QPalette::Base);
    p.fillRect(clipRect, fillColor);
    p.setPen(m_selected ? pal.color(QPalette::Active, QPalette::HighlightedText) : pal.color(QPalette::Active, QPalette::Text));
    p.drawText(0, m_pixmapHeight + (m_margin - 3), width, m_labelHeight, Qt::AlignCenter, QString::number(m_labelNumber));

    // draw page outline and pixmap
    if (clipRect.top() < m_pixmapHeight + m_margin) {
        // if page is bookmarked draw a colored border
        const bool isBookmarked = m_parent->m_document->bookmarkManager()->isBookmarked(pageNumber());
        // draw the inner rect
        p.setPen(isBookmarked ? QColor(0xFF8000) : Qt::black);
        p.drawRect(m_margin / 2 - 1, m_margin / 2 - 1, m_pixmapWidth + 1, m_pixmapHeight + 1);
        // draw the clear rect
        p.setPen(isBookmarked ? QColor(0x804000) : pal.color(QPalette::Active, QPalette::Base));
        // draw the bottom and right shadow edges
        if (!isBookmarked) {
            int left, right, bottom, top;
            left = m_margin / 2 + 1;
            right = m_margin / 2 + m_pixmapWidth + 1;
            bottom = m_pixmapHeight + m_margin / 2 + 1;
            top = m_margin / 2 + 1;
            p.setPen(Qt::gray);
            p.drawLine(left, bottom, right, bottom);
            p.drawLine(right, top, right, bottom);
        }

        // draw the page using the shared PagePainter class
        p.translate(m_margin / 2.0, m_margin / 2.0);
        clipRect.translate(-m_margin / 2, -m_margin / 2);
        clipRect = clipRect.intersected(QRect(0, 0, m_pixmapWidth, m_pixmapHeight));
        if (clipRect.isValid()) {
            int flags = PagePainter::Accessibility | PagePainter::Highlights | PagePainter::Annotations;
            PagePainter::paintPageOnPainter(&p, m_page, m_parent->q, flags, m_pixmapWidth, m_pixmapHeight, clipRect);
        }

        if (!m_visibleRect.isNull()) {
            p.save();
            p.setPen(QColor(255, 255, 0, 200));
            p.setBrush(QColor(0, 0, 0, 100));
            p.drawRect(m_visibleRect.geometry(m_pixmapWidth, m_pixmapHeight).adjusted(0, 0, -1, -1));
            p.restore();
        }

        // draw the bookmark overlay on the top-right corner
        const QPixmap *bookmarkPixmap = m_parent->getBookmarkOverlay();
        if (isBookmarked && bookmarkPixmap) {
            int pixW = bookmarkPixmap->width(), pixH = bookmarkPixmap->height();
            clipRect = clipRect.intersected(QRect(m_pixmapWidth - pixW, 0, pixW, pixH));
            if (clipRect.isValid())
                p.drawPixmap(m_pixmapWidth - pixW, -pixH / 8, *bookmarkPixmap);
        }
    }
}

/** ThumbnailsController implementation **/

#define FILTERB_ID 1

ThumbnailController::ThumbnailController(QWidget *parent, ThumbnailList *list)
    : QToolBar(parent)
{
    setObjectName(QStringLiteral("ThumbsControlBar"));
    // change toolbar appearance
    setIconSize(QSize(16, 16));
    setMovable(false);
    QSizePolicy sp = sizePolicy();
    sp.setVerticalPolicy(QSizePolicy::Minimum);
    setSizePolicy(sp);

    // insert a togglebutton [show only bookmarked pages]
    // insertSeparator();
    QAction *showBoomarkOnlyAction = addAction(QIcon::fromTheme(QStringLiteral("bookmarks")), i18n("Show bookmarked pages only"));
    showBoomarkOnlyAction->setCheckable(true);
    connect(showBoomarkOnlyAction, &QAction::toggled, list, &ThumbnailList::slotFilterBookmarks);
    showBoomarkOnlyAction->setChecked(Okular::Settings::filterBookmarks());
    // insertLineSeparator();
}

#include "thumbnaillist.moc"

/* kate: replace-tabs on; indent-width 4; */
