/*

Copyright 2014, 2021 S. Razi Alavizadeh
Copyright 2020 Johan Björklund
Copyright 2021 Vitaly Cheptsov
Copyright 2013 Thomas Etter
Copyright 2012-2015, 2018, 2021 Adam Reichold
Copyright 2014 Dorian Scholz
Copyright 2018 Egor Zenkov

This file is part of qpdfview.

qpdfview is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.

qpdfview is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with qpdfview.  If not, see <http://www.gnu.org/licenses/>.

*/

#include "documentview.h"

#include <QApplication>
#include <QInputDialog>
#include <QDateTime>
#include <QDebug>
#include <QDesktopServices>
#include <QDir>
#include <QFileSystemWatcher>
#include <QGesture>
#include <QKeyEvent>
#include <qmath.h>
#include <QMenu>
#include <QMessageBox>
#include <QPrintEngine>
#include <QProcess>
#include <QProgressDialog>
#include <QScrollBar>
#include <QTemporaryFile>
#include <QTimer>
#include <QUrl>

#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)

#include <QScreen>

#else

#include <QDesktopWidget>

#endif // QT_VERSION_CHECK

#ifdef WITH_CUPS

#include <cups/cups.h>
#include <cups/ppd.h>

#endif // WITH_CUPS

#ifdef WITH_SYNCTEX

#include <synctex_parser.h>

#ifndef HAS_SYNCTEX_2

typedef synctex_scanner_t synctex_scanner_p;
typedef synctex_node_t synctex_node_p;

#define synctex_scanner_next_result(scanner) synctex_next_result(scanner)

#endif // HAS_SYNCTEX_2

#endif // WITH_SYNCTEX

#include "settings.h"
#include "model.h"
#include "pluginhandler.h"
#include "shortcuthandler.h"
#include "thumbnailitem.h"
#include "presentationview.h"
#include "searchmodel.h"
#include "searchtask.h"
#include "miscellaneous.h"
#include "compatibility.h"
#include "documentlayout.h"

namespace
{

using namespace qpdfview;

// taken from http://rosettacode.org/wiki/Roman_numerals/Decode#C.2B.2B
int romanToInt(const QString& text)
{
    if(text.size() == 1)
    {
        switch(text.at(0).toLower().toLatin1())
        {
        case 'i': return 1;
        case 'v': return 5;
        case 'x': return 10;
        case 'l': return 50;
        case 'c': return 100;
        case 'd': return 500;
        case 'm': return 1000;
        }

        return 0;
    }

    int result = 0;
    int previous = 0, current = 0;

    for(int i = text.size() - 1; i >= 0; --i)
    {
        current = romanToInt(text.at(i));

        result += current < previous ? -current : current;

        previous = current;
    }

    return result;
}

// taken from http://rosettacode.org/wiki/Roman_numerals/Encode#C.2B.2B
QString intToRoman(int number)
{
    struct romandata_t
    {
        int value;
        char const* numeral;
    };

    static const romandata_t romandata[] =
    {
        { 1000, "m" },
        { 900, "cm" },
        { 500, "d" },
        { 400, "cd" },
        { 100, "c" },
        { 90, "xc" },
        { 50, "l" },
        { 40, "xl" },
        { 10, "x" },
        { 9, "ix" },
        { 5, "v" },
        { 4, "iv" },
        { 1, "i" },
        { 0, NULL }
    };

    if(number >= 4000)
    {
        return QLatin1String("?");
    }

    QString result;

    for(const romandata_t* current = romandata; current->value > 0; ++current)
    {
        while(number >= current->value)
        {
            number -= current->value;
            result += QLatin1String(current->numeral);
        }
    }

    return result;
}

bool copyFile(QFile& source, QFile& destination)
{
    const qint64 maxSize = 4096;
    qint64 size = -1;

    QScopedArrayPointer< char > buffer(new char[maxSize]);

    do
    {
        if((size = source.read(buffer.data(), maxSize)) < 0)
        {
            return false;
        }

        if(destination.write(buffer.data(), size) < 0)
        {
            return false;
        }
    }
    while(size > 0);

    return true;
}

inline void adjustFileTemplateSuffix(QTemporaryFile& temporaryFile, const QString& suffix)
{
    temporaryFile.setFileTemplate(temporaryFile.fileTemplate() + QLatin1String(".") + suffix);
}

#ifdef WITH_CUPS

struct RemovePpdFileDeleter
{
    static inline void cleanup(const char* ppdFileName) { if(ppdFileName != 0) { QFile::remove(ppdFileName); } }
};

struct ClosePpdFileDeleter
{
    static inline void cleanup(ppd_file_t* ppdFile) { if(ppdFile != 0) { ppdClose(ppdFile); } }
};

int addCMYKorRGBColorModel(cups_dest_t* dest, int num_options, cups_option_t** options)
{
    QScopedPointer< const char, RemovePpdFileDeleter > ppdFileName(cupsGetPPD(dest->name));

    if(ppdFileName.isNull())
    {
        return num_options;
    }

    QScopedPointer< ppd_file_t, ClosePpdFileDeleter > ppdFile(ppdOpenFile(ppdFileName.data()));

    if(ppdFile.isNull())
    {
        return num_options;
    }

    ppd_option_t* colorModel = ppdFindOption(ppdFile.data(), "ColorModel");

    if(colorModel == 0)
    {
        return num_options;
    }

    for(int index = 0; index < colorModel->num_choices; ++index)
    {
        if(qstrcmp(colorModel->choices[index].choice, "CMYK") == 0)
        {
            return cupsAddOption("ColorModel", "CMYK", num_options, options);
        }
    }

    for(int index = 0; index < colorModel->num_choices; ++index)
    {
        if(qstrcmp(colorModel->choices[index].choice, "RGB") == 0)
        {
            return cupsAddOption("ColorModel", "RGB", num_options, options);
        }
    }

    return num_options;
}

#endif // WITH_CUPS

#ifdef WITH_SYNCTEX

DocumentView::SourceLink scanForSourceLink(const QString& filePath, const int page, QPointF pos)
{
    DocumentView::SourceLink sourceLink;

    if(synctex_scanner_p scanner = synctex_scanner_new_with_output_file(filePath.toLocal8Bit(), 0, 1))
    {
        if(synctex_edit_query(scanner, page, pos.x(), pos.y()) > 0)
        {
            for(synctex_node_p node = synctex_scanner_next_result(scanner); node != 0; node = synctex_scanner_next_result(scanner))
            {
                sourceLink.name = QString::fromLocal8Bit(synctex_scanner_get_name(scanner, synctex_node_tag(node)));
                sourceLink.line = qMax(synctex_node_line(node), 0);
                sourceLink.column = qMax(synctex_node_column(node), 0);
                break;
            }
        }

        synctex_scanner_free(scanner);
    }

    return sourceLink;
}

#endif // WITH_SYNCTEX

inline bool modifiersAreActive(const QWheelEvent* event, Qt::KeyboardModifiers modifiers)
{
    if(modifiers == Qt::NoModifier)
    {
        return false;
    }

    return event->modifiers() == modifiers || (event->buttons() & modifiers) != 0;
}

inline bool modifiersUseMouseButton(Settings* settings, Qt::MouseButton mouseButton)
{
    return ((settings->documentView().zoomModifiers() | settings->documentView().rotateModifiers() | settings->documentView().scrollModifiers()) & mouseButton) != 0;
}

inline void adjustScaleFactor(RenderParam& renderParam, qreal scaleFactor)
{
    if(!qFuzzyCompare(renderParam.scaleFactor(), scaleFactor))
    {
        renderParam.setScaleFactor(scaleFactor);
    }
}

inline void setValueIfNotVisible(QScrollBar* scrollBar, int value)
{
    if(value < scrollBar->value() || value > scrollBar->value() + scrollBar->pageStep())
    {
        scrollBar->setValue(value);
    }
}

inline int pageOfResult(const QModelIndex& index)
{
    return index.data(SearchModel::PageRole).toInt();
}

inline QRectF rectOfResult(const QModelIndex& index)
{
    return index.data(SearchModel::RectRole).toRectF();
}

class OutlineModel : public QAbstractItemModel
{
public:
    OutlineModel(const Model::Outline& outline, DocumentView* parent) : QAbstractItemModel(parent),
        m_outline(outline)
    {
    }

    QModelIndex index(int row, int column, const QModelIndex& parent) const
    {
        if(!hasIndex(row, column, parent))
        {
            return QModelIndex();
        }

        if(parent.isValid())
        {
            const Model::Section* section = resolveIndex(parent);

            return createIndex(row, column, &section->children);
        }
        else
        {
            return createIndex(row, column, &m_outline);
        }
    }

    QModelIndex parent(const QModelIndex& child) const
    {
        if(!child.isValid())
        {
            return QModelIndex();
        }

        const Model::Outline* children = static_cast< const Model::Outline* >(child.internalPointer());

        if(&m_outline != children)
        {
            return findParent(&m_outline, children);
        }

        return QModelIndex();
    }

    int columnCount(const QModelIndex&) const
    {
        return 2;
    }

    int rowCount(const QModelIndex& parent) const
    {
        if(parent.isValid())
        {
            const Model::Section* section = resolveIndex(parent);

            return section->children.size();
        }
        else
        {
            return m_outline.size();
        }
    }

    QVariant data(const QModelIndex& index, int role) const
    {
        if(!index.isValid())
        {
            return QVariant();
        }

        const Model::Section* section = resolveIndex(index);

        switch(role)
        {
        case Qt::DisplayRole:
            switch(index.column())
            {
            case 0:
                return section->title;
            case 1:
                return pageLabel(section->link.page);
            default:
                return QVariant();
            }
        case Model::Document::PageRole:
            return section->link.page;
        case Model::Document::LeftRole:
            return section->link.left;
        case Model::Document::TopRole:
            return section->link.top;
        case Model::Document::FileNameRole:
            return section->link.urlOrFileName;
        case Model::Document::ExpansionRole:
            return m_expanded.contains(section);
        default:
            return QVariant();
        }
    }

    bool setData(const QModelIndex& index, const QVariant& value, int role)
    {
        if(!index.isValid() || role != Model::Document::ExpansionRole)
        {
            return false;
        }

        const Model::Section* section = resolveIndex(index);

        if(value.toBool())
        {
            m_expanded.insert(section);
        }
        else
        {
            m_expanded.remove(section);
        }

        return true;
    }

private:
    const Model::Outline m_outline;

    DocumentView* documentView() const
    {
        return static_cast< DocumentView* >(QObject::parent());
    }

    QString pageLabel(int pageNumber) const
    {
        return documentView()->pageLabelFromNumber(pageNumber);
    }

    QSet< const Model::Section* > m_expanded;

    const Model::Section* resolveIndex(const QModelIndex& index) const
    {
        return &static_cast< const Model::Outline* >(index.internalPointer())->at(index.row());
    }

    QModelIndex createIndex(int row, int column, const Model::Outline* outline) const
    {
        return QAbstractItemModel::createIndex(row, column, const_cast< void* >(static_cast< const void* >(outline)));
    }

    QModelIndex findParent(const Model::Outline* outline, const Model::Outline* children) const
    {
        for(Model::Outline::const_iterator section = outline->begin(); section != outline->end(); ++section)
        {
            if(&section->children == children)
            {
                return createIndex(section - outline->begin(), 0, outline);
            }
        }

        for(Model::Outline::const_iterator section = outline->begin(); section != outline->end(); ++section)
        {
            const QModelIndex parent = findParent(&section->children, children);

            if(parent.isValid())
            {
                return parent;
            }
        }

        return QModelIndex();
    }

};

class FallbackOutlineModel : public QAbstractTableModel
{
public:
    FallbackOutlineModel(DocumentView* parent) : QAbstractTableModel(parent)
    {
    }

    int columnCount(const QModelIndex&) const
    {
        return 2;
    }

    int rowCount(const QModelIndex& parent) const
    {
        if(parent.isValid())
        {
            return 0;
        }

        return numberOfPages();
    }

    QVariant data(const QModelIndex& index, int role) const
    {
        if(!index.isValid())
        {
            return QVariant();
        }

        const int pageNumber = index.row() + 1;

        switch(role)
        {
        case Qt::DisplayRole:
            switch(index.column())
            {
            case 0:
                return DocumentView::tr("Page %1").arg(pageLabel(pageNumber));
            case 1:
                return pageLabel(pageNumber);
            default:
                return QVariant();
            }
        case Model::Document::PageRole:
            return pageNumber;
        case Model::Document::LeftRole:
        case Model::Document::TopRole:
            return qQNaN();
        default:
            return QVariant();
        }
    }

private:
    DocumentView* documentView() const
    {
        return static_cast< DocumentView* >(QObject::parent());
    }

    int numberOfPages() const
    {
        return documentView()->numberOfPages();
    }

    QString pageLabel(int pageNumber) const
    {
        return documentView()->pageLabelFromNumber(pageNumber);
    }

};

class PropertiesModel : public QAbstractTableModel
{
public:
    PropertiesModel(const Model::Properties& properties, DocumentView* parent = 0) : QAbstractTableModel(parent),
        m_properties(properties)
    {
    }

    int columnCount(const QModelIndex&) const
    {
        return 2;
    }

    int rowCount(const QModelIndex& parent) const
    {
        if(parent.isValid())
        {
            return 0;
        }

        return m_properties.size();
    }

    QVariant data(const QModelIndex& index, int role) const
    {
        if(!index.isValid() || role != Qt::DisplayRole)
        {
            return QVariant();
        }

        switch (index.column())
        {
        case 0:
            return m_properties[index.row()].first;
        case 1:
            return m_properties[index.row()].second;
        default:
            return QVariant();
        }
    }

private:
    const Model::Properties m_properties;

};

void addProperty(Model::Properties& properties, const char* name, const QString& value)
{
    properties.append(qMakePair(DocumentView::tr(name), value));
}

QString formatFileSize(qint64 size)
{
    static const char* const units[] = { "B", "kB", "MB", "GB" };
    static const char* const* const lastUnit = &units[sizeof(units) / sizeof(units[0]) - 1];
    const char* const* unit = &units[0];

    while(size > 2048 && unit < lastUnit)
    {
        size /= 1024;
        unit++;
    }

    return QString("%1 %2").arg(size).arg(*unit);
}

void addFileProperties(Model::Properties& properties, const QFileInfo& fileInfo)
{
    addProperty(properties, "File path", fileInfo.absoluteFilePath());
    addProperty(properties, "File size", formatFileSize(fileInfo.size()));

#if QT_VERSION >= QT_VERSION_CHECK(5,10,0)

    addProperty(properties, "File created", fileInfo.birthTime().toString());

#else

    addProperty(properties, "File created", fileInfo.created().toString());

#endif // QT_VERSION

    addProperty(properties, "File last modified", fileInfo.lastModified().toString());
    addProperty(properties, "File owner", fileInfo.owner());
    addProperty(properties, "File group", fileInfo.owner());
}

void appendToPath(const QModelIndex& index, QByteArray& path)
{
    path.append(index.data(Qt::DisplayRole).toByteArray()).append('\0');
}

void saveExpandedPaths(const QAbstractItemModel* model, QSet< QByteArray >& paths, const QModelIndex& index = QModelIndex(), QByteArray path = QByteArray())
{
    appendToPath(index, path);

    if(model->data(index, Model::Document::ExpansionRole).toBool())
    {
        paths.insert(path);
    }

    for(int row = 0, rowCount = model->rowCount(index); row < rowCount; ++row)
    {
        saveExpandedPaths(model, paths, model->index(row, 0, index), path);
    }
}

void restoreExpandedPaths(QAbstractItemModel* model, const QSet< QByteArray >& paths, const QModelIndex& index = QModelIndex(), QByteArray path = QByteArray())
{
    appendToPath(index, path);

    if(paths.contains(path))
    {
        model->setData(index, true, Model::Document::ExpansionRole);
    }

    for(int row = 0, rowCount = model->rowCount(index); row < rowCount; ++row)
    {
        restoreExpandedPaths(model, paths, model->index(row, 0, index), path);
    }
}

} // anonymous

namespace qpdfview
{

class DocumentView::VerticalScrollBarChangedBlocker
{
    Q_DISABLE_COPY(VerticalScrollBarChangedBlocker)

private:
    DocumentView* const that;

public:

    VerticalScrollBarChangedBlocker(DocumentView* that) : that(that)
    {
        that->m_verticalScrollBarChangedBlocked = true;
    }

    ~VerticalScrollBarChangedBlocker()
    {
        that->m_verticalScrollBarChangedBlocked = false;
    }

};

Settings* DocumentView::s_settings = 0;
ShortcutHandler* DocumentView::s_shortcutHandler = 0;
SearchModel* DocumentView::s_searchModel = 0;

DocumentView::DocumentView(QWidget* parent) : QGraphicsView(parent),
    m_autoRefreshWatcher(0),
    m_autoRefreshTimer(0),
    m_prefetchTimer(0),
    m_document(0),
    m_pages(),
    m_fileInfo(),
    m_wasModified(false),
    m_currentPage(-1),
    m_firstPage(-1),
    m_past(),
    m_future(),
    m_layout(new SinglePageLayout),
    m_continuousMode(false),
    m_scaleMode(ScaleFactorMode),
    m_scaleFactor(1.0),
    m_rotation(RotateBy0),
    m_renderFlags(),
    m_highlightAll(false),
    m_rubberBandMode(ModifiersMode),
    m_pageItems(),
    m_thumbnailItems(),
    m_highlight(0),
    m_thumbnailsViewportSize(),
    m_thumbnailsOrientation(Qt::Vertical),
    m_thumbnailsScene(0),
    m_outlineModel(0),
    m_propertiesModel(0),
    m_verticalScrollBarChangedBlocked(false),
    m_currentResult(),
    m_searchTask(0)
{
    if(s_settings == 0)
    {
        s_settings = Settings::instance();
    }

    if(s_shortcutHandler == 0)
    {
        s_shortcutHandler = ShortcutHandler::instance();
    }

    if(s_searchModel == 0)
    {
        s_searchModel = SearchModel::instance();
    }

    setScene(new QGraphicsScene(this));

    setFocusPolicy(Qt::StrongFocus);
    setAcceptDrops(false);
    setDragMode(QGraphicsView::ScrollHandDrag);

    grabGesture(Qt::SwipeGesture);

    connect(verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(on_verticalScrollBar_valueChanged()));

    m_thumbnailsScene = new QGraphicsScene(this);

    // highlight

    m_highlight = new QGraphicsRectItem();
    m_highlight->setGraphicsEffect(new GraphicsCompositionModeEffect(QPainter::CompositionMode_Multiply, this));

    m_highlight->setVisible(false);
    scene()->addItem(m_highlight);

    // search

    m_searchTask = new SearchTask(this);

    connect(m_searchTask, SIGNAL(finished()), SIGNAL(searchFinished()));

    connect(m_searchTask, SIGNAL(progressChanged(int)), SLOT(on_searchTask_progressChanged(int)));
    connect(m_searchTask, SIGNAL(resultsReady(int,QList<QRectF>)), SLOT(on_searchTask_resultsReady(int,QList<QRectF>)));

    // auto-refresh

    m_autoRefreshWatcher = new QFileSystemWatcher(this);

    m_autoRefreshTimer = new QTimer(this);
    m_autoRefreshTimer->setInterval(s_settings->documentView().autoRefreshTimeout());
    m_autoRefreshTimer->setSingleShot(true);

    connect(m_autoRefreshWatcher, SIGNAL(fileChanged(QString)), m_autoRefreshTimer, SLOT(start()));

    connect(m_autoRefreshTimer, SIGNAL(timeout()), this, SLOT(on_autoRefresh_timeout()));

    // prefetch

    m_prefetchTimer = new QTimer(this);
    m_prefetchTimer->setInterval(s_settings->documentView().prefetchTimeout());
    m_prefetchTimer->setSingleShot(true);

    connect(this, SIGNAL(currentPageChanged(int)), m_prefetchTimer, SLOT(start()));
    connect(this, SIGNAL(layoutModeChanged(LayoutMode)), m_prefetchTimer, SLOT(start()));
    connect(this, SIGNAL(scaleModeChanged(ScaleMode)), m_prefetchTimer, SLOT(start()));
    connect(this, SIGNAL(scaleFactorChanged(qreal)), m_prefetchTimer, SLOT(start()));
    connect(this, SIGNAL(rotationChanged(Rotation)), m_prefetchTimer, SLOT(start()));
    connect(this, SIGNAL(renderFlagsChanged(qpdfview::RenderFlags)), m_prefetchTimer, SLOT(start()));

    connect(m_prefetchTimer, SIGNAL(timeout()), SLOT(on_prefetch_timeout()));

    // settings

    m_continuousMode = s_settings->documentView().continuousMode();
    m_layout.reset(DocumentLayout::fromLayoutMode(s_settings->documentView().layoutMode()));
    m_rightToLeftMode = s_settings->documentView().rightToLeftMode();

    m_scaleMode = s_settings->documentView().scaleMode();
    m_scaleFactor = s_settings->documentView().scaleFactor();
    m_rotation = s_settings->documentView().rotation();

    if(s_settings->documentView().invertColors())
    {
        m_renderFlags |= InvertColors;
    }

    if(s_settings->documentView().invertLightness())
    {
        m_renderFlags |= InvertLightness;
    }
    
    if(s_settings->documentView().convertToGrayscale())
    {
        m_renderFlags |= ConvertToGrayscale;
    }

    if(s_settings->documentView().trimMargins())
    {
        m_renderFlags |= TrimMargins;
    }

    switch(s_settings->documentView().compositionMode())
    {
    default:
    case DefaultCompositionMode:
        break;
    case DarkenWithPaperColorMode:
        m_renderFlags |= DarkenWithPaperColor;
        break;
    case LightenWithPaperColorMode:
        m_renderFlags |= LightenWithPaperColor;
        break;
    }

    m_highlightAll = s_settings->documentView().highlightAll();
}

DocumentView::~DocumentView()
{
    m_searchTask->cancel();
    m_searchTask->wait();

    s_searchModel->clearResults(this);

    qDeleteAll(m_pageItems);
    qDeleteAll(m_thumbnailItems);

    qDeleteAll(m_pages);
    delete m_document;
}

void DocumentView::setFirstPage(int firstPage)
{
    if(m_firstPage != firstPage)
    {
        m_firstPage = firstPage;

        for(int index = 0; index < m_thumbnailItems.count(); ++index)
        {
            m_thumbnailItems.at(index)->setText(pageLabelFromNumber(index + 1));
        }

        prepareThumbnailsScene();

        emit numberOfPagesChanged(m_pages.count());
        emit currentPageChanged(m_currentPage);
    }
}

QString DocumentView::defaultPageLabelFromNumber(int number) const
{
    QLocale modifiedLocale = locale();

    modifiedLocale.setNumberOptions(modifiedLocale.numberOptions() | QLocale::OmitGroupSeparator);

    return modifiedLocale.toString(number);
}

QString DocumentView::pageLabelFromNumber(int number) const
{
    QString label;

    if(hasFrontMatter())
    {
        if(number < m_firstPage)
        {
            label = number < 4000 ? intToRoman(number) : defaultPageLabelFromNumber(-number);
        }
        else
        {
            label = defaultPageLabelFromNumber(number - m_firstPage + 1);
        }
    }
    else if(number >= 1 && number <= m_pages.count())
    {
        const QString& pageLabel = m_pages.at(number - 1)->label();

        if(number != pageLabel.toInt())
        {
            label = pageLabel;
        }
    }

    if(label.isEmpty())
    {
        label = defaultPageLabelFromNumber(number);
    }

    return label;
}

int DocumentView::pageNumberFromLabel(const QString& label) const
{
    if(hasFrontMatter())
    {
        bool ok = false;
        int value = locale().toInt(label, &ok);

        if(ok)
        {
            if(value < 0)
            {
                value = -value; // front matter
            }
            else
            {
                value = value + m_firstPage - 1; // body matter
            }
        }
        else
        {
            value = romanToInt(label);
        }

        return value;
    }

    for(int index = 0; index < m_pages.count(); ++index)
    {
        if(m_pages.at(index)->label() == label)
        {
            return index + 1;
        }
    }

    return locale().toInt(label);
}

QString DocumentView::title() const
{
    QString title;

    if(s_settings->mainWindow().documentTitleAsTabTitle())
    {
        for(int row = 0, rowCount = m_propertiesModel->rowCount(); row < rowCount; ++row)
        {
            const QString key = m_propertiesModel->index(row, 0).data().toString();
            const QString value = m_propertiesModel->index(row, 1).data().toString();

            if(QLatin1String("Title") == key)
            {
                title = value;
                break;
            }
        }
    }

    if(title.isEmpty())
    {
        title = m_fileInfo.completeBaseName();
    }

#ifdef Q_OS_MAC

    // On macOS, some Qt versions have a bug of automatically detecting tab entries
    // as menu entries with roles, causing opening files with these names to result
    // in the "Preferences" menu item breaking.
    title.prepend(QChar(0x200B));

#endif // Q_OS_MAC

    return title;
}

QStringList DocumentView::openFilter()
{
    return PluginHandler::openFilter();
}

QStringList DocumentView::saveFilter() const
{
    return m_document->saveFilter();
}

bool DocumentView::canSave() const
{
    return m_document->canSave();
}

void DocumentView::setContinuousMode(bool continuousMode)
{
    if(m_continuousMode != continuousMode)
    {
        m_continuousMode = continuousMode;

        qreal left = 0.0, top = 0.0;
        saveLeftAndTop(left, top);

        adjustScrollBarPolicy();

        prepareView(left, top);

        emit continuousModeChanged(m_continuousMode);

        s_settings->documentView().setContinuousMode(m_continuousMode);
    }
}

LayoutMode DocumentView::layoutMode() const
{
    return m_layout->layoutMode();
}

void DocumentView::setLayoutMode(LayoutMode layoutMode)
{
    if(m_layout->layoutMode() != layoutMode && layoutMode >= 0 && layoutMode < NumberOfLayoutModes)
    {
        m_layout.reset(DocumentLayout::fromLayoutMode(layoutMode));

        if(m_currentPage != m_layout->currentPage(m_currentPage))
        {
            m_currentPage = m_layout->currentPage(m_currentPage);

            emit currentPageChanged(m_currentPage);
        }

        prepareScene();
        prepareView();

        emit layoutModeChanged(layoutMode);

        s_settings->documentView().setLayoutMode(layoutMode);
    }
}

void DocumentView::setRightToLeftMode(bool rightToLeftMode)
{
    if(m_rightToLeftMode != rightToLeftMode)
    {
        m_rightToLeftMode = rightToLeftMode;

        prepareScene();
        prepareView();

        emit rightToLeftModeChanged(m_rightToLeftMode);

        s_settings->documentView().setRightToLeftMode(m_rightToLeftMode);
    }
}

void DocumentView::setScaleMode(ScaleMode scaleMode)
{
    if(m_scaleMode != scaleMode && scaleMode >= 0 && scaleMode < NumberOfScaleModes)
    {
        m_scaleMode = scaleMode;

        qreal left = 0.0, top = 0.0;
        saveLeftAndTop(left, top);

        adjustScrollBarPolicy();

        prepareScene();
        prepareView(left, top);

        emit scaleModeChanged(m_scaleMode);

        s_settings->documentView().setScaleMode(m_scaleMode);
    }
}

void DocumentView::setScaleFactor(qreal scaleFactor)
{
    if(!qFuzzyCompare(m_scaleFactor, scaleFactor)
            && scaleFactor >= s_settings->documentView().minimumScaleFactor()
            && scaleFactor <= s_settings->documentView().maximumScaleFactor())
    {
        m_scaleFactor = scaleFactor;

        if(m_scaleMode == ScaleFactorMode)
        {
            qreal left = 0.0, top = 0.0;
            saveLeftAndTop(left, top);

            prepareScene();
            prepareView(left, top);
        }

        emit scaleFactorChanged(m_scaleFactor);

        s_settings->documentView().setScaleFactor(m_scaleFactor);
    }
}

void DocumentView::setRotation(Rotation rotation)
{
    if(m_rotation != rotation && rotation >= 0 && rotation < NumberOfRotations)
    {
        m_rotation = rotation;

        prepareScene();
        prepareView();

        emit rotationChanged(m_rotation);

        s_settings->documentView().setRotation(m_rotation);
    }
}

void DocumentView::setRenderFlags(qpdfview::RenderFlags renderFlags)
{
    if(m_renderFlags != renderFlags)
    {
        const qpdfview::RenderFlags changedFlags = m_renderFlags ^ renderFlags;

        m_renderFlags = renderFlags;

        qreal left = 0.0, top = 0.0;
        saveLeftAndTop(left, top);

        prepareScene();
        prepareView(left, top);

        prepareThumbnailsScene();

        if(changedFlags.testFlag(InvertColors))
        {
            prepareBackground();

            emit invertColorsChanged(invertColors());

            s_settings->documentView().setInvertColors(invertColors());
        }

        if(changedFlags.testFlag(InvertLightness))
        {
            prepareBackground();

            emit invertLightnessChanged(invertLightness());

            s_settings->documentView().setInvertLightness(invertLightness());
        }

        if(changedFlags.testFlag(ConvertToGrayscale))
        {
            emit convertToGrayscaleChanged(convertToGrayscale());

            s_settings->documentView().setConvertToGrayscale(convertToGrayscale());
        }

        if(changedFlags.testFlag(TrimMargins))
        {
            emit trimMarginsChanged(trimMargins());

            s_settings->documentView().setTrimMargins(trimMargins());
        }

        if(changedFlags.testFlag(DarkenWithPaperColor) || changedFlags.testFlag(LightenWithPaperColor))
        {
            emit compositionModeChanged(compositionMode());

            s_settings->documentView().setCompositionMode(compositionMode());
        }

        emit renderFlagsChanged(m_renderFlags);
    }
}

void DocumentView::setRenderFlag(qpdfview::RenderFlag renderFlag, bool enabled)
{
    if(enabled)
    {
        setRenderFlags(m_renderFlags | renderFlag);
    }
    else
    {
        setRenderFlags(m_renderFlags & ~renderFlag);
    }
}

CompositionMode DocumentView::compositionMode() const
{
    if(m_renderFlags.testFlag(DarkenWithPaperColor))
    {
        return DarkenWithPaperColorMode;
    }
    else if(m_renderFlags.testFlag(LightenWithPaperColor))
    {
        return LightenWithPaperColorMode;
    }
    else
    {
        return DefaultCompositionMode;
    }
}

void DocumentView::setCompositionMode(CompositionMode compositionMode)
{
    switch(compositionMode)
    {
    default:
    case DefaultCompositionMode:
        setRenderFlags((renderFlags() & ~DarkenWithPaperColor) & ~LightenWithPaperColor);
        break;
    case DarkenWithPaperColorMode:
        setRenderFlags((renderFlags() | DarkenWithPaperColor) & ~LightenWithPaperColor);
        break;
    case LightenWithPaperColorMode:
        setRenderFlags((renderFlags() & ~DarkenWithPaperColor) | LightenWithPaperColor);
        break;
    }
}

void DocumentView::setHighlightAll(bool highlightAll)
{
    if(m_highlightAll != highlightAll)
    {
        m_highlightAll = highlightAll;

        if(m_highlightAll)
        {
            for(int index = 0; index < m_pages.count(); ++index)
            {
                const QList< QRectF >& results = s_searchModel->resultsOnPage(this, index + 1);

                m_pageItems.at(index)->setHighlights(results);
                m_thumbnailItems.at(index)->setHighlights(results);
            }
        }
        else
        {
            for(int index = 0; index < m_pages.count(); ++index)
            {
                m_pageItems.at(index)->setHighlights(QList< QRectF >());
                m_thumbnailItems.at(index)->setHighlights(QList< QRectF >());
            }
        }

        emit highlightAllChanged(m_highlightAll);

        s_settings->documentView().setHighlightAll(highlightAll);
    }
}

void DocumentView::setRubberBandMode(RubberBandMode rubberBandMode)
{
    if(m_rubberBandMode != rubberBandMode && rubberBandMode >= 0 && rubberBandMode < NumberOfRubberBandModes)
    {
        m_rubberBandMode = rubberBandMode;

        foreach(PageItem* page, m_pageItems)
        {
            page->setRubberBandMode(m_rubberBandMode);
        }

        emit rubberBandModeChanged(m_rubberBandMode);
    }
}

void DocumentView::setThumbnailsViewportSize(QSize thumbnailsViewportSize)
{
    if(m_thumbnailsViewportSize != thumbnailsViewportSize)
    {
        m_thumbnailsViewportSize = thumbnailsViewportSize;

        prepareThumbnailsScene();
    }
}

void DocumentView::setThumbnailsOrientation(Qt::Orientation thumbnailsOrientation)
{
    if(m_thumbnailsOrientation != thumbnailsOrientation)
    {
        m_thumbnailsOrientation = thumbnailsOrientation;

        prepareThumbnailsScene();
    }
}

QSet< QByteArray > DocumentView::saveExpandedPaths() const
{
    QSet< QByteArray > expandedPaths;

    ::saveExpandedPaths(m_outlineModel.data(), expandedPaths);

    return expandedPaths;
}

void DocumentView::restoreExpandedPaths(const QSet< QByteArray >& expandedPaths)
{
    ::restoreExpandedPaths(m_outlineModel.data(), expandedPaths);
}

QAbstractItemModel* DocumentView::fontsModel() const
{
    return m_document->fonts();
}

bool DocumentView::searchWasCanceled() const
{
    return m_searchTask->wasCanceled();
}

int DocumentView::searchProgress() const
{
    return m_searchTask->progress();
}

QString DocumentView::searchText() const
{
    return m_searchTask->text();
}

bool DocumentView::searchMatchCase() const
{
    return m_searchTask->matchCase();
}

bool DocumentView::searchWholeWords() const
{
    return m_searchTask->wholeWords();
}

QPair< QString, QString > DocumentView::searchContext(int page, const QRectF& rect) const
{
    if(page < 1 || page > m_pages.size() || rect.isEmpty())
    {
        return qMakePair(QString(), QString());
    }

    // Fetch at most half of a line as centered on the given rectangle as possible.
    const qreal pageWidth = m_pages.at(page - 1)->size().width();
    const qreal width = qMax(rect.width(), pageWidth / qreal(2));
    const qreal x = qBound(qreal(0), rect.x() + rect.width() / qreal(2) - width / qreal(2), pageWidth - width);

    const QRectF surroundingRect(x, rect.top(), width, rect.height());

    const QString& matchedText = m_pages.at(page - 1)->cachedText(rect);
    const QString& surroundingText = m_pages.at(page - 1)->cachedText(surroundingRect);

    return qMakePair(matchedText, surroundingText);
}

bool DocumentView::hasSearchResults()
{
    return s_searchModel->hasResults(this);
}

QString DocumentView::resolveFileName(QString fileName) const
{
    if(QFileInfo(fileName).isRelative())
    {
        fileName = m_fileInfo.dir().filePath(fileName);
    }

    return fileName;
}

QUrl DocumentView::resolveUrl(QUrl url) const
{
    const QString path = url.path();

    if(url.isRelative() && QFileInfo(path).isRelative())
    {
        url.setPath(m_fileInfo.dir().filePath(path));
    }

    return url;
}

DocumentView::SourceLink DocumentView::sourceLink(QPoint pos)
{
    SourceLink sourceLink;

#ifdef WITH_SYNCTEX

    if(const PageItem* page = dynamic_cast< PageItem* >(itemAt(pos)))
    {
        const int sourcePage = page->index() + 1;
        const QPointF sourcePos = page->sourcePos(page->mapFromScene(mapToScene(pos)));

        sourceLink = scanForSourceLink(m_fileInfo.absoluteFilePath(), sourcePage, sourcePos);
    }

#else

    Q_UNUSED(pos);

#endif // WITH_SYNCTEX

    return sourceLink;
}

void DocumentView::openInSourceEditor(const DocumentView::SourceLink& sourceLink)
{
    if(!s_settings->documentView().sourceEditor().isEmpty())
    {
        const QString absoluteFilePath = m_fileInfo.dir().absoluteFilePath(sourceLink.name);
        const QString sourceEditorCommand = s_settings->documentView().sourceEditor().arg(absoluteFilePath, QString::number(sourceLink.line), QString::number(sourceLink.column));

        startDetached(sourceEditorCommand);
    }
    else
    {
        QMessageBox::information(this, tr("Information"), tr("The source editor has not been set."));
    }
}

void DocumentView::show()
{
    QGraphicsView::show();

    prepareView();
}

bool DocumentView::open(const QString& filePath)
{
    Model::Document* document = PluginHandler::instance()->loadDocument(filePath);

    if(document != 0)
    {
        QVector< Model::Page* > pages;

        if(!checkDocument(filePath, document, pages))
        {
            delete document;
            qDeleteAll(pages);

            return false;
        }

        m_fileInfo.setFile(filePath);
        m_wasModified = false;

        m_currentPage = 1;

        m_past.clear();
        m_future.clear();

        prepareDocument(document, pages);

        loadDocumentDefaults();

        adjustScrollBarPolicy();

        prepareScene();
        prepareView();

        prepareThumbnailsScene();

        emit documentChanged();

        emit numberOfPagesChanged(m_pages.count());
        emit currentPageChanged(m_currentPage);

        emit canJumpChanged(false, false);

        emit continuousModeChanged(m_continuousMode);
        emit layoutModeChanged(m_layout->layoutMode());
        emit rightToLeftModeChanged(m_rightToLeftMode);
    }

    return document != 0;
}

bool DocumentView::refresh()
{
    Model::Document* document = PluginHandler::instance()->loadDocument(m_fileInfo.filePath());

    if(document != 0)
    {
        QVector< Model::Page* > pages;

        if(!checkDocument(m_fileInfo.filePath(), document, pages))
        {
            delete document;
            qDeleteAll(pages);

            return false;
        }

        qreal left = 0.0, top = 0.0;
        saveLeftAndTop(left, top);

        m_wasModified = false;

        m_currentPage = qMin(m_currentPage, document->numberOfPages());

        QSet< QByteArray > expandedPaths;
        ::saveExpandedPaths(m_outlineModel.data(), expandedPaths);

        prepareDocument(document, pages);

        ::restoreExpandedPaths(m_outlineModel.data(), expandedPaths);

        prepareScene();
        prepareView(left, top);

        prepareThumbnailsScene();

        emit documentChanged();

        emit numberOfPagesChanged(m_pages.count());
        emit currentPageChanged(m_currentPage);
    }

    return document != 0;
}

bool DocumentView::save(const QString& filePath, bool withChanges)
{
    // Save document to temporary file...
    QTemporaryFile temporaryFile;

    adjustFileTemplateSuffix(temporaryFile, QFileInfo(filePath).suffix());

    if(!temporaryFile.open())
    {
        return false;
    }

    temporaryFile.close();

    if(!m_document->save(temporaryFile.fileName(), withChanges))
    {
        return false;
    }

    // Copy from temporary file to actual file...
    QFile file(filePath);

    if(!temporaryFile.open())
    {
        return false;
    }

    if(!file.open(QIODevice::WriteOnly | QIODevice::Truncate))
    {
        return false;
    }

    if(!copyFile(temporaryFile, file))
    {
        return false;
    }

    if(withChanges)
    {
        m_wasModified = false;
    }

    return true;
}

bool DocumentView::print(QPrinter* printer, const PrintOptions& printOptions)
{
    const int fromPage = printer->fromPage() != 0 ? printer->fromPage() : 1;
    const int toPage = printer->toPage() != 0 ? printer->toPage() : m_pages.count();

#ifdef WITH_CUPS

    if(m_document->canBePrintedUsingCUPS())
    {
        return printUsingCUPS(printer, printOptions, fromPage, toPage);
    }

#endif // WITH_CUPS

    return printUsingQt(printer, printOptions, fromPage, toPage);
}

void DocumentView::previousPage()
{
    const qreal leftAndTop = s_settings->documentView().relativeJumps() ? qQNaN() : 0.0;

    jumpToPage(m_layout->previousPage(m_currentPage), true, leftAndTop, leftAndTop);
}

void DocumentView::nextPage()
{
    const qreal leftAndTop = s_settings->documentView().relativeJumps() ? qQNaN() : 0.0;

    jumpToPage(m_layout->nextPage(m_currentPage, m_pages.count()), true, leftAndTop, leftAndTop);
}

void DocumentView::firstPage()
{
    const qreal leftAndTop = s_settings->documentView().relativeJumps() ? qQNaN() : 0.0;

    jumpToPage(1, true, leftAndTop, leftAndTop);
}

void DocumentView::lastPage()
{
    const qreal leftAndTop = s_settings->documentView().relativeJumps() ? qQNaN() : 0.0;

    jumpToPage(m_pages.count(), leftAndTop, leftAndTop);
}

void DocumentView::jumpToPage(int page, bool trackChange, qreal newLeft, qreal newTop)
{
    if(page >= 1 && page <= m_pages.count())
    {
        qreal left = 0.0, top = 0.0;
        saveLeftAndTop(left, top);

        if(qIsNaN(newLeft))
        {
            newLeft = qBound(qreal(0.0), left, qreal(1.0));
        }

        if(qIsNaN(newTop))
        {
            newTop = qBound(qreal(0.0), top, qreal(1.0));
        }

        if(m_currentPage != m_layout->currentPage(page) || qAbs(left - newLeft) > 0.01 || qAbs(top - newTop) > 0.01)
        {
            if(trackChange)
            {
                m_past.append(Position(m_currentPage, left, top));
                m_future.clear();

                emit canJumpChanged(true, false);
            }

            m_currentPage = m_layout->currentPage(page);

            prepareView(newLeft, newTop, false, page);

            emit currentPageChanged(m_currentPage, trackChange);
        }
    }
}

bool DocumentView::canJumpBackward() const
{
    return !m_past.isEmpty();
}

void DocumentView::jumpBackward()
{
    if(!m_past.isEmpty())
    {
        qreal left = 0.0, top = 0.0;
        saveLeftAndTop(left, top);

        m_future.prepend(Position(m_currentPage, left, top));

        const Position pos = m_past.takeLast();
        jumpToPage(pos.page, false, pos.left, pos.top);

        emit canJumpChanged(!m_past.isEmpty(), !m_future.isEmpty());
    }
}

bool DocumentView::canJumpForward() const
{
    return !m_future.isEmpty();
}

void DocumentView::jumpForward()
{
    if(!m_future.isEmpty())
    {
        qreal left = 0.0, top = 0.0;
        saveLeftAndTop(left, top);

        m_past.append(Position(m_currentPage, left, top));

        const Position pos = m_future.takeFirst();
        jumpToPage(pos.page, false, pos.left, pos.top);

        emit canJumpChanged(!m_past.isEmpty(), !m_future.isEmpty());
    }
}

void DocumentView::temporaryHighlight(int page, const QRectF& highlight)
{
    if(page >= 1 && page <= m_pages.count() && !highlight.isNull())
    {
        prepareHighlight(page - 1, highlight);

        QTimer::singleShot(s_settings->documentView().highlightDuration(), this, SLOT(on_temporaryHighlight_timeout()));
    }
}

void DocumentView::startSearch(const QString& text, bool matchCase, bool wholeWords)
{
    cancelSearch();
    clearResults();

    m_searchTask->start(m_pages, text, matchCase, wholeWords, m_currentPage, s_settings->documentView().parallelSearchExecution());
}

void DocumentView::cancelSearch()
{
    m_searchTask->cancel();
    m_searchTask->wait();
}

void DocumentView::clearResults()
{
    s_searchModel->clearResults(this);

    m_currentResult = QModelIndex();

    m_highlight->setVisible(false);

    foreach(PageItem* page, m_pageItems)
    {
        page->setHighlights(QList< QRectF >());
    }

    foreach(ThumbnailItem* page, m_thumbnailItems)
    {
        page->setHighlights(QList< QRectF >());
    }

    if(s_settings->documentView().limitThumbnailsToResults())
    {
        prepareThumbnailsScene();
    }
}

void DocumentView::findPrevious()
{
    checkResult();

    m_currentResult = s_searchModel->findResult(this, m_currentResult, m_currentPage, SearchModel::FindPrevious);

    applyResult();
}

void DocumentView::findNext()
{
    checkResult();

    m_currentResult = s_searchModel->findResult(this, m_currentResult, m_currentPage, SearchModel::FindNext);

    applyResult();
}

void DocumentView::findResult(const QModelIndex& index)
{
    const int page = pageOfResult(index);
    const QRectF rect = rectOfResult(index);

    if(page >= 1 && page <= m_pages.count() && !rect.isEmpty())
    {
        m_currentResult = index;

        applyResult();
    }
}

void DocumentView::zoomIn()
{
    if(scaleMode() != ScaleFactorMode)
    {
        const qreal currentScaleFactor = m_pageItems.at(m_currentPage - 1)->renderParam().scaleFactor();

        setScaleFactor(qMin(currentScaleFactor * s_settings->documentView().zoomFactor(),
                            s_settings->documentView().maximumScaleFactor()));

        setScaleMode(ScaleFactorMode);
    }
    else
    {
        setScaleFactor(qMin(m_scaleFactor * s_settings->documentView().zoomFactor(),
                            s_settings->documentView().maximumScaleFactor()));
    }
}

void DocumentView::zoomOut()
{
    if(scaleMode() != ScaleFactorMode)
    {
        const qreal currentScaleFactor = m_pageItems.at(m_currentPage - 1)->renderParam().scaleFactor();

        setScaleFactor(qMax(currentScaleFactor / s_settings->documentView().zoomFactor(),
                            s_settings->documentView().minimumScaleFactor()));

        setScaleMode(ScaleFactorMode);
    }
    else
    {
        setScaleFactor(qMax(m_scaleFactor / s_settings->documentView().zoomFactor(),
                            s_settings->documentView().minimumScaleFactor()));
    }
}

void DocumentView::originalSize()
{
    setScaleFactor(1.0);
    setScaleMode(ScaleFactorMode);
}

void DocumentView::rotateLeft()
{
    switch(m_rotation)
    {
    default:
    case RotateBy0:
        setRotation(RotateBy270);
        break;
    case RotateBy90:
        setRotation(RotateBy0);
        break;
    case RotateBy180:
        setRotation(RotateBy90);
        break;
    case RotateBy270:
        setRotation(RotateBy180);
        break;
    }
}

void DocumentView::rotateRight()
{
    switch(m_rotation)
    {
    default:
    case RotateBy0:
        setRotation(RotateBy90);
        break;
    case RotateBy90:
        setRotation(RotateBy180);
        break;
    case RotateBy180:
        setRotation(RotateBy270);
        break;
    case RotateBy270:
        setRotation(RotateBy0);
        break;
    }
}

void DocumentView::startPresentation()
{
    const int screen = s_settings->presentationView().screen();

    PresentationView* presentationView = new PresentationView(m_pages);

#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)

    const QRect screenGeometry = screen != -1 ?
                    QGuiApplication::screens().at(screen)->geometry() :
                    QGuiApplication::primaryScreen()->geometry();

#else

    const QRect screenGeometry = QApplication::desktop()->screenGeometry(screen);

#endif // QT_VERSION

    presentationView->setGeometry(screenGeometry);

    presentationView->show();
    presentationView->setAttribute(Qt::WA_DeleteOnClose);

    connect(this, SIGNAL(destroyed()), presentationView, SLOT(close()));
    connect(this, SIGNAL(documentChanged()), presentationView, SLOT(close()));

    presentationView->setRotation(rotation());
    presentationView->setRenderFlags(renderFlags());

    presentationView->jumpToPage(currentPage(), false);

    if(s_settings->presentationView().synchronize())
    {
        connect(this, SIGNAL(currentPageChanged(int,bool)), presentationView, SLOT(jumpToPage(int,bool)));
        connect(presentationView, SIGNAL(currentPageChanged(int,bool)), this, SLOT(jumpToPage(int,bool)));
    }
}

void DocumentView::on_verticalScrollBar_valueChanged()
{
    if(m_verticalScrollBarChangedBlocked || !m_continuousMode)
    {
        return;
    }

    int currentPage = -1;
    const QRectF visibleRect = mapToScene(viewport()->rect()).boundingRect();

    for(int index = 0, count = m_pageItems.count(); index < count; ++index)
    {
        PageItem* page = m_pageItems.at((m_currentPage - 1 + index) % count);

        const int pageNumber = page->index() + 1;
        const QRectF pageRect = page->boundingRect().translated(page->pos());

        if(!pageRect.intersects(visibleRect))
        {
            page->cancelRender();
        }
        else if(currentPage == -1 &&
                 m_layout->currentPage(pageNumber) == pageNumber &&
                 m_layout->isCurrentPage(visibleRect, pageRect))
        {
            currentPage = pageNumber;
        }
    }

    if(currentPage != -1 && m_currentPage != currentPage)
    {
        m_currentPage = currentPage;

        emit currentPageChanged(m_currentPage);

        if(s_settings->documentView().highlightCurrentThumbnail())
        {
            for(int index = 0; index < m_thumbnailItems.count(); ++index)
            {
                m_thumbnailItems.at(index)->setHighlighted(index == m_currentPage - 1);
            }
        }
    }
}

void DocumentView::on_autoRefresh_timeout()
{
    if(m_fileInfo.exists())
    {
        refresh();
    }
    else
    {
        m_wasModified = true;

        emit documentModified();
    }
}

void DocumentView::on_prefetch_timeout()
{
    const QPair< int, int > prefetchRange = m_layout->prefetchRange(m_currentPage, m_pages.count());

    const int maxCost = prefetchRange.second - prefetchRange.first + 1;
    int cost = 0;

    for(int index = m_currentPage - 1; index <= prefetchRange.second - 1; ++index)
    {
        cost += m_pageItems.at(index)->startRender(true);

        if(cost >= maxCost)
        {
            return;
        }
    }

    for(int index = m_currentPage - 1; index >= prefetchRange.first - 1; --index)
    {
        cost += m_pageItems.at(index)->startRender(true);

        if(cost >= maxCost)
        {
            return;
        }
    }
}

void DocumentView::on_temporaryHighlight_timeout()
{
    m_highlight->setVisible(false);
}

void DocumentView::on_searchTask_progressChanged(int progress)
{
    s_searchModel->updateProgress(this);

    emit searchProgressChanged(progress);
}

void DocumentView::on_searchTask_resultsReady(int index, const QList< QRectF >& results)
{
    if(m_searchTask->wasCanceled())
    {
        return;
    }

    s_searchModel->insertResults(this, index + 1, results);

    if(m_highlightAll)
    {
        m_pageItems.at(index)->setHighlights(results);
        m_thumbnailItems.at(index)->setHighlights(results);
    }

    if(s_settings->documentView().limitThumbnailsToResults())
    {
        prepareThumbnailsScene();
    }

    if(!results.isEmpty() && !m_currentResult.isValid())
    {
        setFocus();

        findNext();
    }
}

void DocumentView::on_pages_cropRectChanged()
{
    qreal left = 0.0, top = 0.0;
    saveLeftAndTop(left, top);

    prepareScene();
    prepareView(left, top);
}

void DocumentView::on_thumbnails_cropRectChanged()
{
    prepareThumbnailsScene();
}

void DocumentView::on_pages_linkClicked(bool newTab, int page, qreal left, qreal top)
{
    if(newTab)
    {
        emit linkClicked(page);
    }
    else
    {
        jumpToPage(page, true, left, top);
    }
}

void DocumentView::on_pages_linkClicked(bool newTab, const QString& fileName, int page)
{
    emit linkClicked(newTab, resolveFileName(fileName), page);
}

void DocumentView::on_pages_linkClicked(const QString& url)
{
    if(s_settings->documentView().openUrl())
    {
        QDesktopServices::openUrl(resolveUrl(url));
    }
    else
    {
        QMessageBox::information(this, tr("Information"), tr("Opening URL is disabled in the settings."));
    }
}

void DocumentView::on_pages_rubberBandFinished()
{
    setRubberBandMode(ModifiersMode);
}

void DocumentView::on_pages_zoomToSelection(int page, const QRectF& rect)
{
    if(rect.isEmpty())
    {
        return;
    }

    const qreal visibleWidth = m_layout->visibleWidth(viewport()->width());
    const qreal visibleHeight = m_layout->visibleHeight(viewport()->height());

    const QSizeF displayedSize = m_pageItems.at(page - 1)->displayedSize();

    setScaleFactor(qMin(qMin(visibleWidth / displayedSize.width() / rect.width(),
                             visibleHeight / displayedSize.height() / rect.height()),
                        Defaults::DocumentView::maximumScaleFactor()));

    setScaleMode(ScaleFactorMode);

    jumpToPage(page, false, rect.left(), rect.top());
}

void DocumentView::on_pages_openInSourceEditor(int page, QPointF pos)
{
#ifdef WITH_SYNCTEX

    if(const DocumentView::SourceLink sourceLink = scanForSourceLink(m_fileInfo.absoluteFilePath(), page, pos))
    {
        openInSourceEditor(sourceLink);
    }
    else
    {
        QMessageBox::warning(this, tr("Warning"), tr("SyncTeX data for '%1' could not be found.").arg(m_fileInfo.absoluteFilePath()));
    }

#else

    Q_UNUSED(page);
    Q_UNUSED(pos);

#endif // WITH_SYNCTEX
}

void DocumentView::on_pages_wasModified()
{
    m_wasModified = true;

    emit documentModified();
}

bool DocumentView::event(QEvent* event)
{
    if (event->type() == QEvent::Gesture)
    {
        gestureEvent(static_cast< QGestureEvent* >(event));
        return true;
    }

    return QGraphicsView::event(event);
}

void DocumentView::resizeEvent(QResizeEvent* event)
{
    qreal left = 0.0, top = 0.0;
    saveLeftAndTop(left, top);

    QGraphicsView::resizeEvent(event);

    if(m_scaleMode != ScaleFactorMode)
    {
        prepareScene();
        prepareView(left, top);
    }
}

void DocumentView::keyPressEvent(QKeyEvent* event)
{
    foreach(const PageItem* page, m_pageItems)
    {
        if(page->showsAnnotationOverlay() || page->showsFormFieldOverlay())
        {
            QGraphicsView::keyPressEvent(event);
            return;
        }
    }

    const QKeySequence keySequence(event->modifiers() | event->key());

    int maskedKey = -1;
    bool maskedKeyActive = false;

    switch(event->key())
    {
    case Qt::Key_PageUp:
    case Qt::Key_PageDown:
    case Qt::Key_Up:
    case Qt::Key_Down:
    case Qt::Key_Left:
    case Qt::Key_Right:
        maskedKeyActive = true;
        break;
    }

    if(s_shortcutHandler->matchesSkipBackward(keySequence))
    {
        maskedKey = Qt::Key_PageUp;
    }
    else if(s_shortcutHandler->matchesSkipForward(keySequence))
    {
        maskedKey = Qt::Key_PageDown;
    }
    else if(s_shortcutHandler->matchesMoveUp(keySequence))
    {
        maskedKey = Qt::Key_Up;
    }
    else if(s_shortcutHandler->matchesMoveDown(keySequence))
    {
        maskedKey = Qt::Key_Down;
    }
    else if(s_shortcutHandler->matchesMoveLeft(keySequence))
    {
        maskedKey = Qt::Key_Left;
    }
    else if(s_shortcutHandler->matchesMoveRight(keySequence))
    {
        maskedKey = Qt::Key_Right;
    }
    else if(maskedKeyActive)
    {
        event->ignore();
        return;
    }

    if(maskedKey == -1)
    {
        QGraphicsView::keyPressEvent(event);
    }
    else
    {
        if(!m_continuousMode)
        {
            if((maskedKey == Qt::Key_Up || maskedKey == Qt::Key_PageUp) && verticalScrollBar()->value() == verticalScrollBar()->minimum() && m_currentPage != 1)
            {
                previousPage();

                verticalScrollBar()->setValue(verticalScrollBar()->maximum());

                event->accept();
                return;
            }
            else if((maskedKey == Qt::Key_Down || maskedKey == Qt::Key_PageDown) && verticalScrollBar()->value() == verticalScrollBar()->maximum() && m_currentPage != m_layout->currentPage(m_pages.count()))
            {
                nextPage();

                verticalScrollBar()->setValue(verticalScrollBar()->minimum());

                event->accept();
                return;
            }
        }

        if((maskedKey == Qt::Key_Up && verticalScrollBar()->minimum() == verticalScrollBar()->maximum()) ||
           (maskedKey == Qt::Key_Left && !horizontalScrollBar()->isVisible()))
        {
            previousPage();

            event->accept();
            return;
        }
        else if((maskedKey == Qt::Key_Down && verticalScrollBar()->minimum() == verticalScrollBar()->maximum()) ||
                (maskedKey == Qt::Key_Right && !horizontalScrollBar()->isVisible()))
        {
            nextPage();

            event->accept();
            return;
        }

        QKeyEvent keyEvent(event->type(), maskedKey, Qt::NoModifier, event->text(), event->isAutoRepeat(), event->count());
        QGraphicsView::keyPressEvent(&keyEvent);
    }
}

void DocumentView::mousePressEvent(QMouseEvent* event)
{
    if(event->button() == Qt::XButton1)
    {
        event->accept();

        jumpBackward();
    }
    else if(event->button() == Qt::XButton2)
    {
        event->accept();

        jumpForward();
    }

    QGraphicsView::mousePressEvent(event);
}

void DocumentView::wheelEvent(QWheelEvent* event)
{
    const bool forward = rotatedForward(event);

    const bool noModifiersActive = event->modifiers() == Qt::NoModifier;
    const bool zoomModifiersActive = modifiersAreActive(event, s_settings->documentView().zoomModifiers());
    const bool rotateModifiersActive = modifiersAreActive(event, s_settings->documentView().rotateModifiers());
    const bool scrollModifiersActive = modifiersAreActive(event, s_settings->documentView().scrollModifiers());

    if(zoomModifiersActive)
    {
        if(forward)
        {
            zoomIn();
        }
        else
        {
            zoomOut();
        }

        event->accept();
        return;
    }
    else if(rotateModifiersActive)
    {
        if(forward)
        {
            rotateLeft();
        }
        else
        {
            rotateRight();
        }

        event->accept();
        return;
    }
    else if(scrollModifiersActive)
    {
#if QT_VERSION >= QT_VERSION_CHECK(5,15,0)

        QPoint pixelDelta = event->pixelDelta();
        QPoint angleDelta = event->angleDelta();

        if(s_settings->documentView().scrollModifiers() != Qt::AltModifier)
        {
            qSwap(pixelDelta.rx(), pixelDelta.ry());
            qSwap(angleDelta.rx(), angleDelta.ry());
        }

        QWheelEvent wheelEvent(event->position(), event->globalPosition(), pixelDelta, angleDelta, event->buttons(), Qt::AltModifier, event->phase(), event->inverted(), event->source());

#else

        QWheelEvent wheelEvent(event->pos(), event->delta(), event->buttons(), Qt::AltModifier, Qt::Horizontal);

#endif // QT_VERSION

        QGraphicsView::wheelEvent(&wheelEvent);

        event->accept();
        return;
    }
    else if(noModifiersActive && !m_continuousMode)
    {
        if(forward && verticalScrollBar()->value() == verticalScrollBar()->minimum() && m_currentPage != 1)
        {
            previousPage();

            verticalScrollBar()->setValue(verticalScrollBar()->maximum());

            event->accept();
            return;
        }
        else if(!forward && verticalScrollBar()->value() == verticalScrollBar()->maximum() && m_currentPage != m_layout->currentPage(m_pages.count()))
        {
            nextPage();

            verticalScrollBar()->setValue(verticalScrollBar()->minimum());

            event->accept();
            return;
        }
    }

    QGraphicsView::wheelEvent(event);
}

void DocumentView::gestureEvent(QGestureEvent* event)
{
    if(QGesture* const gesture = event->gesture(Qt::SwipeGesture))
    {
        QSwipeGesture* const swipeGesture = static_cast< QSwipeGesture* >(gesture);

        if(swipeGesture->horizontalDirection() == QSwipeGesture::Left || swipeGesture->verticalDirection() == QSwipeGesture::Up)
        {
            if(swipeGesture->state() == Qt::GestureFinished)
            {
                previousPage();
            }

            event->accept();
            return;
        }
        else if(swipeGesture->horizontalDirection() == QSwipeGesture::Left || swipeGesture->verticalDirection() == QSwipeGesture::Up)
        {
            if(swipeGesture->state() == Qt::GestureFinished)
            {
                nextPage();
            }

            event->accept();
            return;
        }
    }

    event->setAccepted(false);
}

void DocumentView::contextMenuEvent(QContextMenuEvent* event)
{
    if(event->reason() == QContextMenuEvent::Mouse && modifiersUseMouseButton(s_settings, Qt::RightButton))
    {
        event->accept();
        return;
    }

    event->setAccepted(false);

    QGraphicsView::contextMenuEvent(event);

    if(!event->isAccepted())
    {
        event->setAccepted(true);

        emit customContextMenuRequested(event->pos());
    }
}

#ifdef WITH_CUPS

bool DocumentView::printUsingCUPS(QPrinter* printer, const PrintOptions& printOptions, int fromPage, int toPage)
{
    cups_dest_t* dests = 0;
    const int num_dests = cupsGetDests(&dests);

    cups_dest_t* const dest = cupsGetDest(printer->printerName().toUtf8(), 0, num_dests, dests);

    if(dest == 0)
    {
        qWarning() << cupsLastErrorString();

        cupsFreeDests(num_dests, dests);

        return false;
    }

    cups_option_t* options = 0;
    int num_options = 0;

    for(int index = 0; index < dest->num_options; ++index)
    {
        num_options = cupsAddOption(dest->options[index].name, dest->options[index].value, num_options, &options);
    }

    const QStringList cupsOptions = printer->printEngine()->property(QPrintEngine::PrintEnginePropertyKey(0xfe00)).toStringList();

    for(int index = 0; index < cupsOptions.count() - 1; index += 2)
    {
        num_options = cupsAddOption(cupsOptions.at(index).toUtf8(), cupsOptions.at(index + 1).toUtf8(), num_options, &options);
    }

#if QT_VERSION >= QT_VERSION_CHECK(4,7,0)

    num_options = cupsAddOption("copies", QString::number(printer->copyCount()).toUtf8(), num_options, &options);

#endif // QT_VERSION

    num_options = cupsAddOption("Collate", printer->collateCopies() ? "true" : "false", num_options, &options);

    switch(printer->pageOrder())
    {
    case QPrinter::FirstPageFirst:
        num_options = cupsAddOption("outputorder", "normal", num_options, &options);
        break;
    case QPrinter::LastPageFirst:
        num_options = cupsAddOption("outputorder", "reverse", num_options, &options);
        break;
    }

    num_options = cupsAddOption("fit-to-page", printOptions.fitToPage ? "true" : "false", num_options, &options);

#if QT_VERSION >= QT_VERSION_CHECK(5,3,0)

    switch(printer->pageLayout().orientation())

#else

    switch(printer->orientation())

#endif // QT_VERSION
    {
    case PageOrientationValues::Portrait:
        num_options = cupsAddOption("landscape", "false", num_options, &options);
        break;
    case PageOrientationValues::Landscape:
        num_options = cupsAddOption("landscape", "true", num_options, &options);
        break;
    }

    switch(printer->colorMode())
    {
    case QPrinter::Color:
        num_options = addCMYKorRGBColorModel(dest, num_options, &options);
        num_options = cupsAddOption("Ink", "COLOR", num_options, &options);
        break;
    case QPrinter::GrayScale:
        num_options = cupsAddOption("ColorModel", "Gray", num_options, &options);
        num_options = cupsAddOption("Ink", "MONO", num_options, &options);
        break;
    }

    switch(printer->duplex())
    {
    case QPrinter::DuplexNone:
        num_options = cupsAddOption("sides", "one-sided", num_options, &options);
        break;
    case QPrinter::DuplexAuto:
        break;
    case QPrinter::DuplexLongSide:
        num_options = cupsAddOption("sides", "two-sided-long-edge", num_options, &options);
        break;
    case QPrinter::DuplexShortSide:
        num_options = cupsAddOption("sides", "two-sided-short-edge", num_options, &options);
        break;
    }

    int numberUp = 1;

#if QT_VERSION < QT_VERSION_CHECK(5,2,0)

    switch(printOptions.numberUp)
    {
    case PrintOptions::SinglePage:
        num_options = cupsAddOption("number-up", "1", num_options, &options);
        numberUp = 1;
        break;
    case PrintOptions::TwoPages:
        num_options = cupsAddOption("number-up", "2", num_options, &options);
        numberUp = 2;
        break;
    case PrintOptions::FourPages:
        num_options = cupsAddOption("number-up", "4", num_options, &options);
        numberUp = 4;
        break;
    case PrintOptions::SixPages:
        num_options = cupsAddOption("number-up", "6", num_options, &options);
        numberUp = 6;
        break;
    case PrintOptions::NinePages:
        num_options = cupsAddOption("number-up", "9", num_options, &options);
        numberUp = 9;
        break;
    case PrintOptions::SixteenPages:
        num_options = cupsAddOption("number-up", "16", num_options, &options);
        numberUp = 16;
        break;
    }

    switch(printOptions.numberUpLayout)
    {
    case PrintOptions::BottomTopLeftRight:
        num_options = cupsAddOption("number-up-layout", "btlr", num_options, &options);
        break;
    case PrintOptions::BottomTopRightLeft:
        num_options = cupsAddOption("number-up-layout", "btrl", num_options, &options);
        break;
    case PrintOptions::LeftRightBottomTop:
        num_options = cupsAddOption("number-up-layout", "lrbt", num_options, &options);
        break;
    case PrintOptions::LeftRightTopBottom:
        num_options = cupsAddOption("number-up-layout", "lrtb", num_options, &options);
        break;
    case PrintOptions::RightLeftBottomTop:
        num_options = cupsAddOption("number-up-layout", "rlbt", num_options, &options);
        break;
    case PrintOptions::RightLeftTopBottom:
        num_options = cupsAddOption("number-up-layout", "rltb", num_options, &options);
        break;
    case PrintOptions::TopBottomLeftRight:
        num_options = cupsAddOption("number-up-layout", "tblr", num_options, &options);
        break;
    case PrintOptions::TopBottomRightLeft:
        num_options = cupsAddOption("number-up-layout", "tbrl", num_options, &options);
        break;
    }

    switch(printOptions.pageSet)
    {
    case PrintOptions::AllPages:
        break;
    case PrintOptions::EvenPages:
        num_options = cupsAddOption("page-set", "even", num_options, &options);
        break;
    case PrintOptions::OddPages:
        num_options = cupsAddOption("page-set", "odd", num_options, &options);
        break;
    }

#else

    {
        bool ok = false;
        const int value = QString::fromUtf8(cupsGetOption("number-up", num_options, options)).toInt(&ok);

        if(ok)
        {
            numberUp = value;
        }
    }

    if(fromPage % 2 == 0)
    {
        const QString value = QString::fromUtf8(cupsGetOption("page-set", num_options, options));

        if(value == "odd")
        {
            num_options = cupsAddOption("page-set", "even", num_options, &options);
        }
        else if(value == "even")
        {
            num_options = cupsAddOption("page-set", "odd", num_options, &options);
        }
    }

#endif // QT_VERSION

    fromPage = (fromPage - 1) / numberUp + 1;
    toPage = (toPage - 1) / numberUp + 1;

    if(cupsGetOption("page-ranges", num_options, options) == 0)
    {
        if(printOptions.pageRanges.isEmpty())
        {
            num_options = cupsAddOption("page-ranges", QString("%1-%2").arg(fromPage).arg(toPage).toUtf8(), num_options, &options);
        }
        else
        {
            num_options = cupsAddOption("page-ranges", printOptions.pageRanges.toUtf8(), num_options, &options);
        }
    }

    QTemporaryFile temporaryFile;

    adjustFileTemplateSuffix(temporaryFile, m_fileInfo.suffix());

    if(!temporaryFile.open())
    {
        cupsFreeDests(num_dests, dests);
        cupsFreeOptions(num_options, options);

        return false;
    }

    temporaryFile.close();

    if(!m_document->save(temporaryFile.fileName(), true))
    {
        cupsFreeDests(num_dests, dests);
        cupsFreeOptions(num_options, options);

        return false;
    }

    const int jobId = cupsPrintFile(dest->name, QFile::encodeName(temporaryFile.fileName()), m_fileInfo.completeBaseName().toUtf8(), num_options, options);

    if(jobId < 1)
    {
        qWarning() << cupsLastErrorString();
    }

    cupsFreeDests(num_dests, dests);
    cupsFreeOptions(num_options, options);

    return jobId >= 1;
}

#endif // WITH_CUPS

bool DocumentView::printUsingQt(QPrinter* printer, const PrintOptions& printOptions, int fromPage, int toPage)
{
    QScopedPointer< QProgressDialog > progressDialog(new QProgressDialog(this));
    progressDialog->setLabelText(tr("Printing '%1'...").arg(m_fileInfo.completeBaseName()));
    progressDialog->setRange(fromPage - 1, toPage);

    QPainter painter;

    if(!painter.begin(printer))
    {
        return false;
    }

    for(int index = fromPage - 1; index <= toPage - 1; ++index)
    {
        progressDialog->setValue(index);

        QApplication::processEvents();

        painter.save();

        const Model::Page* page = m_pages.at(index);

        if(printOptions.fitToPage)
        {
            const qreal pageWidth = printer->physicalDpiX() / 72.0 * page->size().width();
            const qreal pageHeight = printer->physicalDpiY() / 72.0 * page->size().width();

            const qreal scaleFactor = qMin(printer->width() / pageWidth, printer->height() / pageHeight);

            painter.setTransform(QTransform::fromScale(scaleFactor, scaleFactor));
        }
        else
        {
            const qreal scaleFactorX = static_cast< qreal >(printer->logicalDpiX()) / static_cast< qreal >(printer->physicalDpiX());
            const qreal scaleFactorY = static_cast< qreal >(printer->logicalDpiY()) / static_cast< qreal >(printer->physicalDpiY());

            painter.setTransform(QTransform::fromScale(scaleFactorX, scaleFactorY));
        }

        painter.drawImage(QPointF(), page->render(printer->physicalDpiX(), printer->physicalDpiY()));

        painter.restore();

        if(index < toPage - 1)
        {
            printer->newPage();
        }

        QApplication::processEvents();

        if(progressDialog->wasCanceled())
        {
            printer->abort();

            return false;
        }
    }

    return true;
}

void DocumentView::saveLeftAndTop(qreal& left, qreal& top) const
{
    const PageItem* page = m_pageItems.at(m_currentPage - 1);
    const QRectF boundingRect = page->uncroppedBoundingRect().translated(page->pos());

    const QPointF topLeft = mapToScene(viewport()->rect().topLeft());

    left = (topLeft.x() - boundingRect.x()) / boundingRect.width();
    top = (topLeft.y() - boundingRect.y()) / boundingRect.height();
}

bool DocumentView::checkDocument(const QString& filePath, Model::Document* document, QVector< Model::Page* >& pages)
{
    if(document->isLocked())
    {
        QString password = QInputDialog::getText(this, tr("Unlock %1").arg(QFileInfo(filePath).completeBaseName()), tr("Password:"), QLineEdit::Password);

        if(document->unlock(password))
        {
            return false;
        }
    }

    const int numberOfPages = document->numberOfPages();

    if(numberOfPages == 0)
    {
        qWarning() << "No pages were found in document at" << filePath;

        return false;
    }

    pages.reserve(numberOfPages);

    for(int index = 0; index < numberOfPages; ++index)
    {
        Model::Page* page = document->page(index);

        if(page == 0)
        {
            qWarning() << "No page" << index << "was found in document at" << filePath;

            return false;
        }

        pages.append(page);
    }

    return true;
}

void DocumentView::loadDocumentDefaults()
{
    if(m_document->wantsContinuousMode())
    {
        m_continuousMode = true;
    }

    if(m_document->wantsSinglePageMode())
    {
        m_layout.reset(new SinglePageLayout);
    }
    else if(m_document->wantsTwoPagesMode())
    {
        m_layout.reset(new TwoPagesLayout);
    }
    else if(m_document->wantsTwoPagesWithCoverPageMode())
    {
        m_layout.reset(new TwoPagesWithCoverPageLayout);
    }

    if(m_document->wantsRightToLeftMode())
    {
        m_rightToLeftMode = true;
    }
}

void DocumentView::adjustScrollBarPolicy()
{
    switch(m_scaleMode)
    {
    default:
    case ScaleFactorMode:
        setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
        setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
        break;
    case FitToPageWidthMode:
        setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
        setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
        break;
    case FitToPageSizeMode:
        setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
        setVerticalScrollBarPolicy(m_continuousMode ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff);
        break;
    }
}

void DocumentView::prepareDocument(Model::Document* document, const QVector< Model::Page* >& pages)
{
    m_prefetchTimer->blockSignals(true);
    m_prefetchTimer->stop();

    cancelSearch();
    clearResults();

    qDeleteAll(m_pageItems);
    qDeleteAll(m_thumbnailItems);

    qDeleteAll(m_pages);
    m_pages = pages;

    delete m_document;
    m_document = document;

    if(!m_autoRefreshWatcher->files().isEmpty())
    {
        m_autoRefreshWatcher->removePaths(m_autoRefreshWatcher->files());
    }

    if(s_settings->documentView().autoRefresh())
    {
        m_autoRefreshWatcher->addPath(m_fileInfo.filePath());
    }

    m_document->setPaperColor(s_settings->pageItem().paperColor());

    preparePages();
    prepareThumbnails();
    prepareBackground();

    const Model::Outline outline = m_document->outline();

    if(!outline.empty())
    {
        m_outlineModel.reset(new OutlineModel(outline, this));
    }
    else
    {
        m_outlineModel.reset(new FallbackOutlineModel(this));
    }

    Model::Properties properties = m_document->properties();

    addFileProperties(properties, m_fileInfo);

    m_propertiesModel.reset(new PropertiesModel(properties, this));

    if(s_settings->documentView().prefetch())
    {
        m_prefetchTimer->blockSignals(false);
        m_prefetchTimer->start();
    }
}

void DocumentView::preparePages()
{
    m_pageItems.clear();
    m_pageItems.reserve(m_pages.count());

    for(int index = 0; index < m_pages.count(); ++index)
    {
        PageItem* page = new PageItem(m_pages.at(index), index);

        page->setRubberBandMode(m_rubberBandMode);

        scene()->addItem(page);
        m_pageItems.append(page);

        connect(page, SIGNAL(cropRectChanged()), SLOT(on_pages_cropRectChanged()));

        connect(page, SIGNAL(linkClicked(bool,int,qreal,qreal)), SLOT(on_pages_linkClicked(bool,int,qreal,qreal)));
        connect(page, SIGNAL(linkClicked(bool,QString,int)), SLOT(on_pages_linkClicked(bool,QString,int)));
        connect(page, SIGNAL(linkClicked(QString)), SLOT(on_pages_linkClicked(QString)));

        connect(page, SIGNAL(rubberBandFinished()), SLOT(on_pages_rubberBandFinished()));

        connect(page, SIGNAL(appendTextToBookmarkComment(int,QString)), SIGNAL(appendTextToBookmarkComment(int,QString)));
        connect(page, SIGNAL(zoomToSelection(int,QRectF)), SLOT(on_pages_zoomToSelection(int,QRectF)));
        connect(page, SIGNAL(openInSourceEditor(int,QPointF)), SLOT(on_pages_openInSourceEditor(int,QPointF)));

        connect(page, SIGNAL(wasModified()), SLOT(on_pages_wasModified()));
    }
}

void DocumentView::prepareThumbnails()
{
    m_thumbnailItems.clear();
    m_thumbnailItems.reserve(m_pages.count());

    for(int index = 0; index < m_pages.count(); ++index)
    {
        ThumbnailItem* page = new ThumbnailItem(m_pages.at(index), pageLabelFromNumber(index + 1), index);

        m_thumbnailsScene->addItem(page);
        m_thumbnailItems.append(page);

        connect(page, SIGNAL(cropRectChanged()), SLOT(on_thumbnails_cropRectChanged()));

        connect(page, SIGNAL(linkClicked(bool,int,qreal,qreal)), SLOT(on_pages_linkClicked(bool,int,qreal,qreal)));
    }
}

void DocumentView::prepareBackground()
{
    QColor backgroundColor;

    if(s_settings->pageItem().decoratePages())
    {
        backgroundColor = s_settings->pageItem().backgroundColor();
    }
    else
    {
        backgroundColor = s_settings->pageItem().paperColor();

        if(invertColors())
        {
            backgroundColor.setRgb(~backgroundColor.rgb());
        }

        if(invertLightness())
        {
            backgroundColor.setRgb(~backgroundColor.rgb());
        }
    }

    scene()->setBackgroundBrush(QBrush(backgroundColor));
    m_thumbnailsScene->setBackgroundBrush(QBrush(backgroundColor));
}

void DocumentView::prepareScene()
{
    // prepare render parameters and adjust scale factor

    RenderParam renderParam(scaleFactor(), rotation(), renderFlags());

#if QT_VERSION >= QT_VERSION_CHECK(5,1,0)

    if(s_settings->pageItem().useDevicePixelRatio())
    {
#if QT_VERSION >= QT_VERSION_CHECK(5,6,0)

        renderParam.setDevicePixelRatio(devicePixelRatioF());

#else

        renderParam.setDevicePixelRatio(devicePixelRatio());

#endif // QT_VERSION
    }

#endif // QT_VERSION

    if(s_settings->pageItem().useLogicalDpi())
    {
        renderParam.setResolution(logicalDpiX(), logicalDpiY());
    }

    qreal viewportWidth = viewport()->width();
    qreal viewportHeight = viewport()->height();

    if(verticalScrollBarPolicy() == Qt::ScrollBarAsNeeded && !verticalScrollBar()->isVisible())
    {
        viewportWidth -= verticalScrollBar()->width();
    }

    if(horizontalScrollBarPolicy() == Qt::ScrollBarAsNeeded && !horizontalScrollBar()->isVisible())
    {
        viewportHeight -= horizontalScrollBar()->height();
    }

    const qreal visibleWidth = m_layout->visibleWidth(viewportWidth);
    const qreal visibleHeight = m_layout->visibleHeight(viewportHeight);

    foreach(PageItem* page, m_pageItems)
    {
        const QSizeF displayedSize = page->displayedSize(renderParam);

        if(m_scaleMode == FitToPageWidthMode)
        {
            adjustScaleFactor(renderParam, visibleWidth / displayedSize.width());
        }
        else if(m_scaleMode == FitToPageSizeMode)
        {
            adjustScaleFactor(renderParam, qMin(visibleWidth / displayedSize.width(), visibleHeight / displayedSize.height()));
        }

        page->setRenderParam(renderParam);
    }

    // prepare layout

    qreal left = 0.0;
    qreal right = 0.0;
    qreal height = s_settings->documentView().pageSpacing();

    m_layout->prepareLayout(m_pageItems, m_rightToLeftMode,
                            left, right, height);

    scene()->setSceneRect(left, 0.0, right - left, height);
}

void DocumentView::prepareView(qreal newLeft, qreal newTop, bool forceScroll, int scrollToPage)
{
    const QRectF sceneRect = scene()->sceneRect();

    qreal top = sceneRect.top();
    qreal height = sceneRect.height();

    int horizontalValue = 0;
    int verticalValue = 0;

    scrollToPage = scrollToPage != 0 ? scrollToPage : m_currentPage;

    const int highlightIsOnPage = m_currentResult.isValid() ? pageOfResult(m_currentResult) : 0;
    const bool highlightCurrentThumbnail = s_settings->documentView().highlightCurrentThumbnail();

    for(int index = 0; index < m_pageItems.count(); ++index)
    {
        PageItem* page = m_pageItems.at(index);

        if(m_continuousMode)
        {
            page->setVisible(true);
        }
        else
        {
            if(m_layout->leftIndex(index) == m_currentPage - 1)
            {
                page->setVisible(true);

                const QRectF boundingRect = page->boundingRect().translated(page->pos());

                top = boundingRect.top() - s_settings->documentView().pageSpacing();
                height = boundingRect.height() + 2.0 * s_settings->documentView().pageSpacing();
            }
            else
            {
                page->setVisible(false);

                page->cancelRender();
            }
        }

        if(index == scrollToPage - 1)
        {
            const QRectF boundingRect = page->uncroppedBoundingRect().translated(page->pos());

            horizontalValue = qFloor(boundingRect.left() + newLeft * boundingRect.width());
            verticalValue = qFloor(boundingRect.top() + newTop * boundingRect.height());
        }

        if(index == highlightIsOnPage - 1)
        {
            m_highlight->setPos(page->pos());
            m_highlight->setTransform(page->transform());

            page->stackBefore(m_highlight);
        }

        m_thumbnailItems.at(index)->setHighlighted(highlightCurrentThumbnail && (index == m_currentPage - 1));
    }

    setSceneRect(sceneRect.left(), top, sceneRect.width(), height);

    if(!forceScroll && s_settings->documentView().minimalScrolling())
    {
        setValueIfNotVisible(horizontalScrollBar(), horizontalValue);
        setValueIfNotVisible(verticalScrollBar(), verticalValue);
    }
    else
    {
        horizontalScrollBar()->setValue(horizontalValue);
        verticalScrollBar()->setValue(verticalValue);
    }

    viewport()->update();
}

void DocumentView::prepareThumbnailsScene()
{
    // prepare render parameters and adjust scale factor

    RenderParam renderParam(scaleFactor(), rotation(), renderFlags());

#if QT_VERSION >= QT_VERSION_CHECK(5,1,0)

    if(s_settings->pageItem().useDevicePixelRatio())
    {
#if QT_VERSION >= QT_VERSION_CHECK(5,6,0)

        renderParam.setDevicePixelRatio(devicePixelRatioF());

#else

        renderParam.setDevicePixelRatio(devicePixelRatio());

#endif // QT_VERSION
    }

#endif // QT_VERSION

    if(s_settings->pageItem().useLogicalDpi())
    {
        renderParam.setResolution(logicalDpiX(), logicalDpiY());
    }

    const qreal thumbnailSize = s_settings->documentView().thumbnailSize();
    const qreal thumbnailSpacing = s_settings->documentView().thumbnailSpacing();

    qreal visibleWidth = Defaults::DocumentView::thumbnailSize();
    qreal visibleHeight = Defaults::DocumentView::thumbnailSize();

    if(!m_thumbnailsViewportSize.isNull())
    {
        visibleWidth = m_thumbnailsViewportSize.width() - 3.0 * thumbnailSpacing;
        visibleHeight = m_thumbnailsViewportSize.height() - 3.0 * thumbnailSpacing;
    }

    foreach(ThumbnailItem* page, m_thumbnailItems)
    {
        const QSizeF displayedSize = page->displayedSize(renderParam);

        if(thumbnailSize != 0.0)
        {
            adjustScaleFactor(renderParam, qMin(thumbnailSize / displayedSize.width(), thumbnailSize / displayedSize.height()));
        }
        else
        {
            if(m_thumbnailsOrientation == Qt::Vertical)
            {
                adjustScaleFactor(renderParam, visibleWidth / displayedSize.width());
            }
            else
            {
                adjustScaleFactor(renderParam, (visibleHeight - page->textHeight()) / displayedSize.height());
            }
        }

        page->setRenderParam(renderParam);
    }

    // prepare layout

    qreal left = 0.0;
    qreal right = m_thumbnailsOrientation == Qt::Vertical ? 0.0 : thumbnailSpacing;
    qreal top = 0.0;
    qreal bottom = m_thumbnailsOrientation == Qt::Vertical ? thumbnailSpacing : 0.0;

    const bool limitThumbnailsToResults = s_settings->documentView().limitThumbnailsToResults();

    for(int index = 0; index < m_thumbnailItems.count(); ++index)
    {
        ThumbnailItem* page = m_thumbnailItems.at(index);

        // prepare visibility

        if(limitThumbnailsToResults && s_searchModel->hasResults(this) && !s_searchModel->hasResultsOnPage(this, index + 1))
        {
            page->setVisible(false);

            page->cancelRender();

            continue;
        }

        page->setVisible(true);

        // prepare position

        const QRectF boundingRect = page->boundingRect();

        if(m_thumbnailsOrientation == Qt::Vertical)
        {
            page->setPos(-boundingRect.left() - 0.5 * boundingRect.width(), bottom - boundingRect.top());

            left = qMin(left, -0.5f * boundingRect.width() - thumbnailSpacing);
            right = qMax(right, 0.5f * boundingRect.width() + thumbnailSpacing);
            bottom += boundingRect.height() + thumbnailSpacing;
        }
        else
        {
            page->setPos(right - boundingRect.left(), -boundingRect.top() - 0.5 * boundingRect.height());

            top = qMin(top, -0.5f * boundingRect.height() - thumbnailSpacing);
            bottom = qMax(bottom, 0.5f * boundingRect.height() + thumbnailSpacing);
            right += boundingRect.width() + thumbnailSpacing;
        }
    }

    m_thumbnailsScene->setSceneRect(left, top, right - left, bottom - top);
}

void DocumentView::prepareHighlight(int index, const QRectF& rect)
{
    PageItem* page = m_pageItems.at(index);

    m_highlight->setPos(page->pos());
    m_highlight->setTransform(page->transform());

    m_highlight->setRect(rect.normalized());
    m_highlight->setBrush(QBrush(s_settings->pageItem().highlightColor()));

    page->stackBefore(m_highlight);

    m_highlight->setVisible(true);

    {
        VerticalScrollBarChangedBlocker verticalScrollBarChangedBlocker(this);

        centerOn(m_highlight);
    }

    viewport()->update();
}

void DocumentView::checkResult()
{
    if(m_currentResult.isValid() && m_layout->currentPage(pageOfResult(m_currentResult)) != m_currentPage)
    {
        m_currentResult = QModelIndex();
    }
}

void DocumentView::applyResult()
{
    if(m_currentResult.isValid())
    {
        const int page = pageOfResult(m_currentResult);
        const QRectF rect = rectOfResult(m_currentResult);

        jumpToPage(page);

        prepareHighlight(page - 1, rect);
    }
    else
    {
        m_highlight->setVisible(false);
    }
}

} // qpdfview
