File indexing completed on 2024-04-28 15:51:49

0001 /*
0002     SPDX-FileCopyrightText: 2004-2005 Enrico Ros <eros.kde@email.it>
0003     SPDX-FileCopyrightText: 2004-2006 Albert Astals Cid <aacid@kde.org>
0004 
0005     Work sponsored by the LiMux project of the city of Munich:
0006     SPDX-FileCopyrightText: 2017 Klarälvdalens Datakonsult AB a KDAB Group company <info@kdab.com>
0007 
0008     With portions of code from kpdf/kpdf_pagewidget.cc by:
0009     SPDX-FileCopyrightText: 2002 Wilco Greven <greven@kde.org>
0010     SPDX-FileCopyrightText: 2003 Christophe Devriese <Christophe.Devriese@student.kuleuven.ac.be>
0011     SPDX-FileCopyrightText: 2003 Laurent Montel <montel@kde.org>
0012     SPDX-FileCopyrightText: 2003 Dirk Mueller <mueller@kde.org>
0013     SPDX-FileCopyrightText: 2004 James Ots <kde@jamesots.com>
0014     SPDX-FileCopyrightText: 2011 Jiri Baum - NICTA <jiri@baum.com.au>
0015 
0016     SPDX-License-Identifier: GPL-2.0-or-later
0017 */
0018 
0019 #include "pageview.h"
0020 
0021 // qt/kde includes
0022 #include <QActionGroup>
0023 #include <QApplication>
0024 #include <QClipboard>
0025 #include <QCursor>
0026 #include <QDesktopServices>
0027 #include <QElapsedTimer>
0028 #include <QEvent>
0029 #include <QGestureEvent>
0030 #include <QImage>
0031 #include <QInputDialog>
0032 #include <QLoggingCategory>
0033 #include <QMenu>
0034 #include <QMimeData>
0035 #include <QMimeDatabase>
0036 #include <QPainter>
0037 #include <QScrollBar>
0038 #include <QScroller>
0039 #include <QScrollerProperties>
0040 #include <QSet>
0041 #include <QTimer>
0042 #include <QToolTip>
0043 
0044 #include <KActionCollection>
0045 #include <KActionMenu>
0046 #include <KConfigWatcher>
0047 #include <KIO/CommandLauncherJob>
0048 #include <KIO/JobUiDelegate>
0049 #include <KIO/JobUiDelegateFactory>
0050 #include <KIO/OpenUrlJob>
0051 #include <KLocalizedString>
0052 #include <KMessageBox>
0053 #include <KSelectAction>
0054 #include <KStandardAction>
0055 #include <KStringHandler>
0056 #include <KToggleAction>
0057 #include <KUriFilter>
0058 #include <QAction>
0059 #include <QDebug>
0060 #include <QIcon>
0061 #include <kio_version.h>
0062 #include <kwidgetsaddons_version.h>
0063 
0064 // system includes
0065 #include <array>
0066 #include <math.h>
0067 #include <stdlib.h>
0068 
0069 // local includes
0070 #include "annotationpopup.h"
0071 #include "annotwindow.h"
0072 #include "colormodemenu.h"
0073 #include "core/annotations.h"
0074 #include "cursorwraphelper.h"
0075 #include "formwidgets.h"
0076 #include "gui/debug_ui.h"
0077 #include "gui/guiutils.h"
0078 #include "gui/pagepainter.h"
0079 #include "gui/priorities.h"
0080 #include "okmenutitle.h"
0081 #include "pageviewannotator.h"
0082 #include "pageviewmouseannotation.h"
0083 #include "pageviewutils.h"
0084 #include "toggleactionmenu.h"
0085 #if HAVE_SPEECH
0086 #include "tts.h"
0087 #endif
0088 #include "core/action.h"
0089 #include "core/audioplayer.h"
0090 #include "core/document_p.h"
0091 #include "core/form.h"
0092 #include "core/generator.h"
0093 #include "core/misc.h"
0094 #include "core/movie.h"
0095 #include "core/page.h"
0096 #include "core/page_p.h"
0097 #include "core/sourcereference.h"
0098 #include "core/tile.h"
0099 #include "magnifierview.h"
0100 #include "settings.h"
0101 #include "settings_core.h"
0102 #include "url_utils.h"
0103 #include "videowidget.h"
0104 
0105 static const int pageflags = PagePainter::Accessibility | PagePainter::EnhanceLinks | PagePainter::EnhanceImages | PagePainter::Highlights | PagePainter::TextSelection | PagePainter::Annotations;
0106 
0107 static const std::array<float, 16> kZoomValues {0.12, 0.25, 0.33, 0.50, 0.66, 0.75, 1.00, 1.25, 1.50, 2.00, 4.00, 8.00, 16.00, 25.00, 50.00, 100.00};
0108 
0109 // This is the length of the text that will be shown when the user is searching for a specific piece of text.
0110 static const int searchTextPreviewLength = 21;
0111 
0112 // When following a link, only a preview of this length will be used to set the text of the action.
0113 static const int linkTextPreviewLength = 30;
0114 
0115 static inline double normClamp(double value, double def)
0116 {
0117     return (value < 0.0 || value > 1.0) ? def : value;
0118 }
0119 
0120 struct TableSelectionPart {
0121     PageViewItem *item;
0122     Okular::NormalizedRect rectInItem;
0123     Okular::NormalizedRect rectInSelection;
0124 
0125     TableSelectionPart(PageViewItem *item_p, const Okular::NormalizedRect &rectInItem_p, const Okular::NormalizedRect &rectInSelection_p);
0126 };
0127 
0128 TableSelectionPart::TableSelectionPart(PageViewItem *item_p, const Okular::NormalizedRect &rectInItem_p, const Okular::NormalizedRect &rectInSelection_p)
0129     : item(item_p)
0130     , rectInItem(rectInItem_p)
0131     , rectInSelection(rectInSelection_p)
0132 {
0133 }
0134 
0135 // structure used internally by PageView for data storage
0136 class PageViewPrivate
0137 {
0138 public:
0139     explicit PageViewPrivate(PageView *qq);
0140 
0141     FormWidgetsController *formWidgetsController();
0142 #if HAVE_SPEECH
0143     OkularTTS *tts();
0144 #endif
0145     QString selectedText() const;
0146 
0147     // the document, pageviewItems and the 'visible cache'
0148     PageView *q;
0149     Okular::Document *document;
0150     QVector<PageViewItem *> items;
0151     QList<PageViewItem *> visibleItems;
0152     MagnifierView *magnifierView;
0153 
0154     // view layout (columns in Settings), zoom and mouse
0155     PageView::ZoomMode zoomMode;
0156     float zoomFactor;
0157     QPoint mouseGrabOffset;
0158     QPointF mousePressPos;
0159     QPointF mouseSelectPos;
0160     QPointF previousMouseMovePos;
0161     qreal mouseMidLastY;
0162     bool mouseSelecting;
0163     QRect mouseSelectionRect;
0164     QColor mouseSelectionColor;
0165     bool mouseTextSelecting;
0166     QSet<int> pagesWithTextSelection;
0167     bool mouseOnRect;
0168     int mouseMode;
0169     MouseAnnotation *mouseAnnotation;
0170 
0171     // table selection
0172     QList<double> tableSelectionCols;
0173     QList<double> tableSelectionRows;
0174     QList<TableSelectionPart> tableSelectionParts;
0175     bool tableDividersGuessed;
0176 
0177     int lastSourceLocationViewportPageNumber;
0178     double lastSourceLocationViewportNormalizedX;
0179     double lastSourceLocationViewportNormalizedY;
0180 
0181     // for everything except PgUp/PgDn and scroll to arbitrary locations
0182     const int baseShortScrollDuration = 100;
0183     int currentShortScrollDuration;
0184     // for PgUp/PgDn and scroll to arbitrary locations
0185     const int baseLongScrollDuration = baseShortScrollDuration * 2;
0186     int currentLongScrollDuration;
0187 
0188     // auto scroll
0189     int scrollIncrement;
0190     QTimer *autoScrollTimer;
0191     // annotations
0192     PageViewAnnotator *annotator;
0193     // text annotation dialogs list
0194     QSet<AnnotWindow *> m_annowindows;
0195     // other stuff
0196     QTimer *delayResizeEventTimer;
0197     bool dirtyLayout;
0198     bool blockViewport;             // prevents changes to viewport
0199     bool blockPixmapsRequest;       // prevent pixmap requests
0200     PageViewMessage *messageWindow; // in pageviewutils.h
0201     bool m_formsVisible;
0202     FormWidgetsController *formsWidgetController;
0203 #if HAVE_SPEECH
0204     OkularTTS *m_tts;
0205 #endif
0206     QTimer *refreshTimer;
0207     QSet<int> refreshPages;
0208 
0209     // bbox state for Trim to Selection mode
0210     Okular::NormalizedRect trimBoundingBox;
0211 
0212     // infinite resizing loop prevention
0213     bool verticalScrollBarVisible = false;
0214     bool horizontalScrollBarVisible = false;
0215 
0216     // drag scroll
0217     QPoint dragScrollVector;
0218     QTimer dragScrollTimer;
0219 
0220     // left click depress
0221     QTimer leftClickTimer;
0222 
0223     // actions
0224     QAction *aRotateClockwise;
0225     QAction *aRotateCounterClockwise;
0226     QAction *aRotateOriginal;
0227     KActionMenu *aTrimMode;
0228     KToggleAction *aTrimMargins;
0229     KToggleAction *aReadingDirection;
0230     QAction *aMouseNormal;
0231     QAction *aMouseZoom;
0232     QAction *aMouseSelect;
0233     QAction *aMouseTextSelect;
0234     QAction *aMouseTableSelect;
0235     QAction *aMouseMagnifier;
0236     KToggleAction *aTrimToSelection;
0237     QAction *aSignature;
0238     KSelectAction *aZoom;
0239     QAction *aZoomIn;
0240     QAction *aZoomOut;
0241     QAction *aZoomActual;
0242     KToggleAction *aZoomFitWidth;
0243     KToggleAction *aZoomFitPage;
0244     KToggleAction *aZoomAutoFit;
0245     KActionMenu *aViewModeMenu;
0246     QActionGroup *viewModeActionGroup;
0247     ColorModeMenu *aColorModeMenu;
0248     KToggleAction *aViewContinuous;
0249     QAction *aPrevAction;
0250     KToggleAction *aToggleForms;
0251     QAction *aSpeakDoc;
0252     QAction *aSpeakPage;
0253     QAction *aSpeakStop;
0254     QAction *aSpeakPauseResume;
0255     KActionCollection *actionCollection;
0256     QActionGroup *mouseModeActionGroup;
0257     ToggleActionMenu *aMouseModeMenu;
0258     QAction *aFitWindowToPage;
0259 
0260     int setting_viewCols;
0261     bool rtl_Mode;
0262     // Keep track of whether tablet pen is currently pressed down
0263     bool penDown;
0264 
0265     // Keep track of mouse over link object
0266     const Okular::ObjectRect *mouseOverLinkObject;
0267 
0268     QScroller *scroller;
0269 
0270     bool pinchZoomActive;
0271     // The remaining scroll from the previous zoom event
0272     QPointF remainingScroll;
0273 };
0274 
0275 PageViewPrivate::PageViewPrivate(PageView *qq)
0276     : q(qq)
0277 #if HAVE_SPEECH
0278     , m_tts(nullptr)
0279 #endif
0280 {
0281 }
0282 
0283 FormWidgetsController *PageViewPrivate::formWidgetsController()
0284 {
0285     if (!formsWidgetController) {
0286         formsWidgetController = new FormWidgetsController(document);
0287         QObject::connect(formsWidgetController, &FormWidgetsController::changed, q, &PageView::slotFormChanged);
0288         QObject::connect(formsWidgetController, &FormWidgetsController::action, q, &PageView::slotAction);
0289         QObject::connect(formsWidgetController, &FormWidgetsController::mouseUpAction, q, &PageView::slotMouseUpAction);
0290     }
0291 
0292     return formsWidgetController;
0293 }
0294 
0295 #if HAVE_SPEECH
0296 OkularTTS *PageViewPrivate::tts()
0297 {
0298     if (!m_tts) {
0299         m_tts = new OkularTTS(q);
0300         if (aSpeakStop) {
0301             QObject::connect(m_tts, &OkularTTS::canPauseOrResume, aSpeakStop, &QAction::setEnabled);
0302         }
0303 
0304         if (aSpeakPauseResume) {
0305             QObject::connect(m_tts, &OkularTTS::canPauseOrResume, aSpeakPauseResume, &QAction::setEnabled);
0306         }
0307     }
0308 
0309     return m_tts;
0310 }
0311 #endif
0312 
0313 /* PageView. What's in this file? -> quick overview.
0314  * Code weight (in rows) and meaning:
0315  *  160 - constructor and creating actions plus their connected slots (empty stuff)
0316  *  70  - DocumentObserver inherited methodes (important)
0317  *  550 - events: mouse, keyboard, drag
0318  *  170 - slotRelayoutPages: set contents of the scrollview on continuous/single modes
0319  *  100 - zoom: zooming pages in different ways, keeping update the toolbar actions, etc..
0320  *  other misc functions: only slotRequestVisiblePixmaps and pickItemOnPoint noticeable,
0321  * and many insignificant stuff like this comment :-)
0322  */
0323 PageView::PageView(QWidget *parent, Okular::Document *document)
0324     : QAbstractScrollArea(parent)
0325     , Okular::View(QStringLiteral("PageView"))
0326 {
0327     // create and initialize private storage structure
0328     d = new PageViewPrivate(this);
0329     d->document = document;
0330     d->aRotateClockwise = nullptr;
0331     d->aRotateCounterClockwise = nullptr;
0332     d->aRotateOriginal = nullptr;
0333     d->aViewModeMenu = nullptr;
0334     d->zoomMode = PageView::ZoomFitWidth;
0335     d->zoomFactor = 1.0;
0336     d->mouseSelecting = false;
0337     d->mouseTextSelecting = false;
0338     d->mouseOnRect = false;
0339     d->mouseMode = Okular::Settings::mouseMode();
0340     d->mouseAnnotation = new MouseAnnotation(this, document);
0341     d->tableDividersGuessed = false;
0342     d->lastSourceLocationViewportPageNumber = -1;
0343     d->lastSourceLocationViewportNormalizedX = 0.0;
0344     d->lastSourceLocationViewportNormalizedY = 0.0;
0345     d->currentShortScrollDuration = d->baseShortScrollDuration;
0346     d->currentLongScrollDuration = d->baseLongScrollDuration;
0347     d->scrollIncrement = 0;
0348     d->autoScrollTimer = nullptr;
0349     d->annotator = nullptr;
0350     d->dirtyLayout = false;
0351     d->blockViewport = false;
0352     d->blockPixmapsRequest = false;
0353     d->messageWindow = new PageViewMessage(this);
0354     d->m_formsVisible = false;
0355     d->formsWidgetController = nullptr;
0356 #if HAVE_SPEECH
0357     d->m_tts = nullptr;
0358 #endif
0359     d->refreshTimer = nullptr;
0360     d->aRotateClockwise = nullptr;
0361     d->aRotateCounterClockwise = nullptr;
0362     d->aRotateOriginal = nullptr;
0363     d->aTrimMode = nullptr;
0364     d->aTrimMargins = nullptr;
0365     d->aTrimToSelection = nullptr;
0366     d->aReadingDirection = nullptr;
0367     d->aMouseNormal = nullptr;
0368     d->aMouseZoom = nullptr;
0369     d->aMouseSelect = nullptr;
0370     d->aMouseTextSelect = nullptr;
0371     d->aSignature = nullptr;
0372     d->aZoomFitWidth = nullptr;
0373     d->aZoomFitPage = nullptr;
0374     d->aZoomAutoFit = nullptr;
0375     d->aViewModeMenu = nullptr;
0376     d->aViewContinuous = nullptr;
0377     d->viewModeActionGroup = nullptr;
0378     d->aColorModeMenu = nullptr;
0379     d->aPrevAction = nullptr;
0380     d->aToggleForms = nullptr;
0381     d->aSpeakDoc = nullptr;
0382     d->aSpeakPage = nullptr;
0383     d->aSpeakStop = nullptr;
0384     d->aSpeakPauseResume = nullptr;
0385     d->actionCollection = nullptr;
0386     d->setting_viewCols = Okular::Settings::viewColumns();
0387     d->rtl_Mode = Okular::Settings::rtlReadingDirection();
0388     d->mouseModeActionGroup = nullptr;
0389     d->aMouseModeMenu = nullptr;
0390     d->penDown = false;
0391     d->aMouseMagnifier = nullptr;
0392     d->aFitWindowToPage = nullptr;
0393     d->trimBoundingBox = Okular::NormalizedRect(); // Null box
0394     d->pinchZoomActive = false;
0395     d->remainingScroll = QPointF(0.0, 0.0);
0396 
0397     switch (Okular::Settings::zoomMode()) {
0398     case 0: {
0399         d->zoomFactor = 1;
0400         d->zoomMode = PageView::ZoomFixed;
0401         break;
0402     }
0403     case 1: {
0404         d->zoomMode = PageView::ZoomFitWidth;
0405         break;
0406     }
0407     case 2: {
0408         d->zoomMode = PageView::ZoomFitPage;
0409         break;
0410     }
0411     case 3: {
0412         d->zoomMode = PageView::ZoomFitAuto;
0413         break;
0414     }
0415     }
0416 
0417     connect(Okular::Settings::self(), &Okular::Settings::viewContinuousChanged, this, [=]() {
0418         if (d->aViewContinuous && !d->document->isOpened()) {
0419             d->aViewContinuous->setChecked(Okular::Settings::viewContinuous());
0420         }
0421     });
0422 
0423     d->delayResizeEventTimer = new QTimer(this);
0424     d->delayResizeEventTimer->setSingleShot(true);
0425     d->delayResizeEventTimer->setObjectName(QStringLiteral("delayResizeEventTimer"));
0426     connect(d->delayResizeEventTimer, &QTimer::timeout, this, &PageView::delayedResizeEvent);
0427 
0428     setFrameStyle(QFrame::NoFrame);
0429 
0430     setAttribute(Qt::WA_StaticContents);
0431 
0432     setObjectName(QStringLiteral("okular::pageView"));
0433 
0434     // viewport setup: setup focus, and track mouse
0435     viewport()->setFocusProxy(this);
0436     viewport()->setFocusPolicy(Qt::StrongFocus);
0437     viewport()->setAttribute(Qt::WA_OpaquePaintEvent);
0438     viewport()->setAttribute(Qt::WA_NoSystemBackground);
0439     viewport()->setMouseTracking(true);
0440     viewport()->setAutoFillBackground(false);
0441 
0442     d->scroller = QScroller::scroller(viewport());
0443 
0444     QScrollerProperties prop;
0445     prop.setScrollMetric(QScrollerProperties::DecelerationFactor, 0.3);
0446     prop.setScrollMetric(QScrollerProperties::MaximumVelocity, 1);
0447     prop.setScrollMetric(QScrollerProperties::AcceleratingFlickMaximumTime, 0.2); // Workaround for QTBUG-88249 (non-flick gestures recognized as accelerating flick)
0448     prop.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
0449     prop.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
0450     prop.setScrollMetric(QScrollerProperties::DragStartDistance, 0.0);
0451     d->scroller->setScrollerProperties(prop);
0452 
0453     connect(d->scroller, &QScroller::stateChanged, this, [this](QScroller::State s) { slotRequestVisiblePixmaps(s); });
0454 
0455     // the apparently "magic" value of 20 is the same used internally in QScrollArea
0456     verticalScrollBar()->setCursor(Qt::ArrowCursor);
0457     verticalScrollBar()->setSingleStep(20);
0458     horizontalScrollBar()->setCursor(Qt::ArrowCursor);
0459     horizontalScrollBar()->setSingleStep(20);
0460 
0461     // make the smooth scroll animation durations respect the global animation
0462     // scale
0463     KConfigWatcher::Ptr animationSpeedWatcher = KConfigWatcher::create(KSharedConfig::openConfig());
0464     connect(animationSpeedWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) {
0465         if (group.name() == QLatin1String("KDE") && names.contains(QByteArrayLiteral("AnimationDurationFactor"))) {
0466             PageView::updateSmoothScrollAnimationSpeed();
0467         }
0468     });
0469 
0470     // connect the padding of the viewport to pixmaps requests
0471     connect(horizontalScrollBar(), &QAbstractSlider::valueChanged, this, &PageView::slotRequestVisiblePixmaps);
0472     connect(verticalScrollBar(), &QAbstractSlider::valueChanged, this, &PageView::slotRequestVisiblePixmaps);
0473 
0474     // Keep the scroller in sync with user input on the scrollbars.
0475     // QAbstractSlider::sliderMoved() and sliderReleased are the intuitive signals,
0476     // but are only emitted when the “slider is down”, i. e. not when the user scrolls on the scrollbar.
0477     // QAbstractSlider::actionTriggered() is emitted in all user input cases,
0478     // but before the value() changes, so we need queued connection here.
0479     auto update_scroller = [=]() {
0480         d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); // sync scroller with scrollbar
0481     };
0482     connect(verticalScrollBar(), &QAbstractSlider::actionTriggered, this, update_scroller, Qt::QueuedConnection);
0483     connect(horizontalScrollBar(), &QAbstractSlider::actionTriggered, this, update_scroller, Qt::QueuedConnection);
0484 
0485     connect(&d->dragScrollTimer, &QTimer::timeout, this, &PageView::slotDragScroll);
0486 
0487     d->leftClickTimer.setSingleShot(true);
0488     connect(&d->leftClickTimer, &QTimer::timeout, this, &PageView::slotShowSizeAllCursor);
0489 
0490     // set a corner button to resize the view to the page size
0491     //    QPushButton * resizeButton = new QPushButton( viewport() );
0492     //    resizeButton->setPixmap( SmallIcon("crop") );
0493     //    setCornerWidget( resizeButton );
0494     //    resizeButton->setEnabled( false );
0495     // connect(...);
0496     setAttribute(Qt::WA_InputMethodEnabled, true);
0497 
0498     // Grab pinch gestures to zoom and rotate the view
0499     grabGesture(Qt::PinchGesture);
0500 
0501     d->magnifierView = new MagnifierView(document, this);
0502     d->magnifierView->hide();
0503     d->magnifierView->setGeometry(0, 0, 351, 201); // TODO: more dynamic?
0504 
0505     connect(document, &Okular::Document::processMovieAction, this, &PageView::slotProcessMovieAction);
0506     connect(document, &Okular::Document::processRenditionAction, this, &PageView::slotProcessRenditionAction);
0507 
0508     // schedule the welcome message
0509     QMetaObject::invokeMethod(this, "slotShowWelcome", Qt::QueuedConnection);
0510 }
0511 
0512 PageView::~PageView()
0513 {
0514 #if HAVE_SPEECH
0515     if (d->m_tts) {
0516         d->m_tts->stopAllSpeechs();
0517     }
0518 #endif
0519 
0520     delete d->mouseAnnotation;
0521 
0522     // delete the local storage structure
0523 
0524     // We need to assign it to a different list otherwise slotAnnotationWindowDestroyed
0525     // will bite us and clear d->m_annowindows
0526     QSet<AnnotWindow *> annowindows = d->m_annowindows;
0527     d->m_annowindows.clear();
0528     qDeleteAll(annowindows);
0529 
0530     // delete all widgets
0531     qDeleteAll(d->items);
0532     delete d->formsWidgetController;
0533     d->document->removeObserver(this);
0534     delete d;
0535 }
0536 
0537 void PageView::setupBaseActions(KActionCollection *ac)
0538 {
0539     d->actionCollection = ac;
0540 
0541     // Zoom actions ( higher scales takes lots of memory! )
0542     d->aZoom = new KSelectAction(QIcon::fromTheme(QStringLiteral("page-zoom")), i18n("Zoom"), this);
0543     ac->addAction(QStringLiteral("zoom_to"), d->aZoom);
0544     d->aZoom->setEditable(true);
0545     d->aZoom->setMaxComboViewCount(kZoomValues.size() + 3);
0546     connect(d->aZoom, &KSelectAction::actionTriggered, this, &PageView::slotZoom);
0547     updateZoomText();
0548 
0549     d->aZoomIn = KStandardAction::zoomIn(this, SLOT(slotZoomIn()), ac);
0550 
0551     d->aZoomOut = KStandardAction::zoomOut(this, SLOT(slotZoomOut()), ac);
0552 
0553     d->aZoomActual = KStandardAction::actualSize(this, &PageView::slotZoomActual, ac);
0554     d->aZoomActual->setText(i18n("Zoom to 100%"));
0555 }
0556 
0557 void PageView::setupViewerActions(KActionCollection *ac)
0558 {
0559     d->actionCollection = ac;
0560 
0561     ac->setDefaultShortcut(d->aZoomIn, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_Plus));
0562     ac->setDefaultShortcut(d->aZoomOut, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_Minus));
0563 
0564     // orientation menu actions
0565     d->aRotateClockwise = new QAction(QIcon::fromTheme(QStringLiteral("object-rotate-right")), i18n("Rotate &Right"), this);
0566     d->aRotateClockwise->setIconText(i18nc("Rotate right", "Right"));
0567     ac->addAction(QStringLiteral("view_orientation_rotate_cw"), d->aRotateClockwise);
0568     d->aRotateClockwise->setEnabled(false);
0569     connect(d->aRotateClockwise, &QAction::triggered, this, &PageView::slotRotateClockwise);
0570     d->aRotateCounterClockwise = new QAction(QIcon::fromTheme(QStringLiteral("object-rotate-left")), i18n("Rotate &Left"), this);
0571     d->aRotateCounterClockwise->setIconText(i18nc("Rotate left", "Left"));
0572     ac->addAction(QStringLiteral("view_orientation_rotate_ccw"), d->aRotateCounterClockwise);
0573     d->aRotateCounterClockwise->setEnabled(false);
0574     connect(d->aRotateCounterClockwise, &QAction::triggered, this, &PageView::slotRotateCounterClockwise);
0575     d->aRotateOriginal = new QAction(i18n("Original Orientation"), this);
0576     ac->addAction(QStringLiteral("view_orientation_original"), d->aRotateOriginal);
0577     d->aRotateOriginal->setEnabled(false);
0578     connect(d->aRotateOriginal, &QAction::triggered, this, &PageView::slotRotateOriginal);
0579 
0580     // Trim View actions
0581     d->aTrimMode = new KActionMenu(i18n("&Trim View"), this);
0582     d->aTrimMode->setPopupMode(QToolButton::InstantPopup);
0583     ac->addAction(QStringLiteral("view_trim_mode"), d->aTrimMode);
0584 
0585     d->aTrimMargins = new KToggleAction(QIcon::fromTheme(QStringLiteral("trim-margins")), i18n("&Trim Margins"), d->aTrimMode->menu());
0586     d->aTrimMode->addAction(d->aTrimMargins);
0587     ac->addAction(QStringLiteral("view_trim_margins"), d->aTrimMargins);
0588     d->aTrimMargins->setData(QVariant::fromValue((int)Okular::Settings::EnumTrimMode::Margins));
0589     connect(d->aTrimMargins, &QAction::toggled, this, &PageView::slotTrimMarginsToggled);
0590     d->aTrimMargins->setChecked(Okular::Settings::trimMargins());
0591 
0592     d->aTrimToSelection = new KToggleAction(QIcon::fromTheme(QStringLiteral("trim-to-selection")), i18n("Trim To &Selection"), d->aTrimMode->menu());
0593     d->aTrimMode->addAction(d->aTrimToSelection);
0594     ac->addAction(QStringLiteral("view_trim_selection"), d->aTrimToSelection);
0595     d->aTrimToSelection->setData(QVariant::fromValue((int)Okular::Settings::EnumTrimMode::Selection));
0596     connect(d->aTrimToSelection, &QAction::toggled, this, &PageView::slotTrimToSelectionToggled);
0597 
0598     d->aZoomFitWidth = new KToggleAction(QIcon::fromTheme(QStringLiteral("zoom-fit-width")), i18n("Fit &Width"), this);
0599     ac->addAction(QStringLiteral("view_fit_to_width"), d->aZoomFitWidth);
0600     connect(d->aZoomFitWidth, &QAction::toggled, this, &PageView::slotFitToWidthToggled);
0601 
0602     d->aZoomFitPage = new KToggleAction(QIcon::fromTheme(QStringLiteral("zoom-fit-best")), i18n("Fit &Page"), this);
0603     ac->addAction(QStringLiteral("view_fit_to_page"), d->aZoomFitPage);
0604     connect(d->aZoomFitPage, &QAction::toggled, this, &PageView::slotFitToPageToggled);
0605 
0606     d->aZoomAutoFit = new KToggleAction(QIcon::fromTheme(QStringLiteral("zoom-fit-best")), i18n("&Auto Fit"), this);
0607     ac->addAction(QStringLiteral("view_auto_fit"), d->aZoomAutoFit);
0608     connect(d->aZoomAutoFit, &QAction::toggled, this, &PageView::slotAutoFitToggled);
0609 
0610     d->aFitWindowToPage = new QAction(QIcon::fromTheme(QStringLiteral("zoom-fit-width")), i18n("Fit Wi&ndow to Page"), this);
0611     d->aFitWindowToPage->setEnabled(Okular::Settings::viewMode() == (int)Okular::Settings::EnumViewMode::Single);
0612     ac->setDefaultShortcut(d->aFitWindowToPage, QKeySequence(Qt::CTRL | Qt::Key_J));
0613     ac->addAction(QStringLiteral("fit_window_to_page"), d->aFitWindowToPage);
0614     connect(d->aFitWindowToPage, &QAction::triggered, this, &PageView::slotFitWindowToPage);
0615 
0616     // View Mode action menu (Single Page, Facing Pages,...(choose), and Continuous (on/off))
0617     d->aViewModeMenu = new KActionMenu(QIcon::fromTheme(QStringLiteral("view-split-left-right")), i18n("&View Mode"), this);
0618     d->aViewModeMenu->setPopupMode(QToolButton::InstantPopup);
0619     ac->addAction(QStringLiteral("view_render_mode"), d->aViewModeMenu);
0620 
0621     d->viewModeActionGroup = new QActionGroup(this);
0622     auto addViewMode = [=](QAction *a, const QString &name, Okular::Settings::EnumViewMode::type id) {
0623         a->setCheckable(true);
0624         a->setData(int(id));
0625         d->aViewModeMenu->addAction(a);
0626         ac->addAction(name, a);
0627         d->viewModeActionGroup->addAction(a);
0628     };
0629     addViewMode(new QAction(QIcon::fromTheme(QStringLiteral("view-pages-single")), i18nc("@item:inmenu", "&Single Page"), this), QStringLiteral("view_render_mode_single"), Okular::Settings::EnumViewMode::Single);
0630     addViewMode(new QAction(QIcon::fromTheme(QStringLiteral("view-pages-facing")), i18nc("@item:inmenu", "&Facing Pages"), this), QStringLiteral("view_render_mode_facing"), Okular::Settings::EnumViewMode::Facing);
0631     addViewMode(new QAction(QIcon::fromTheme(QStringLiteral("view-pages-facing-first-centered")), i18nc("@item:inmenu", "Facing Pages (&Center First Page)"), this),
0632                 QStringLiteral("view_render_mode_facing_center_first"),
0633                 Okular::Settings::EnumViewMode::FacingFirstCentered);
0634     addViewMode(new QAction(QIcon::fromTheme(QStringLiteral("view-pages-overview")), i18nc("@item:inmenu", "&Overview"), this), QStringLiteral("view_render_mode_overview"), Okular::Settings::EnumViewMode::Summary);
0635     const QList<QAction *> viewModeActions = d->viewModeActionGroup->actions();
0636     for (QAction *viewModeAction : viewModeActions) {
0637         if (viewModeAction->data().toInt() == Okular::Settings::viewMode()) {
0638             viewModeAction->setChecked(true);
0639             break;
0640         }
0641     }
0642     connect(d->viewModeActionGroup, &QActionGroup::triggered, this, &PageView::slotViewMode);
0643 
0644     // Continuous view action, add to view mode action menu.
0645     d->aViewModeMenu->addSeparator();
0646     d->aViewContinuous = new KToggleAction(QIcon::fromTheme(QStringLiteral("view-pages-continuous")), i18n("&Continuous"), this);
0647     d->aViewModeMenu->addAction(d->aViewContinuous);
0648     ac->addAction(QStringLiteral("view_continuous"), d->aViewContinuous);
0649     connect(d->aViewContinuous, &QAction::toggled, this, &PageView::slotContinuousToggled);
0650     d->aViewContinuous->setChecked(Okular::Settings::viewContinuous());
0651 
0652     // Reading direction toggle action. (Checked means RTL, unchecked means LTR.)
0653     d->aReadingDirection = new KToggleAction(QIcon::fromTheme(QStringLiteral("format-text-direction-rtl")), i18nc("@action page layout", "Use Right to Left Reading Direction"), this);
0654     d->aReadingDirection->setChecked(Okular::Settings::rtlReadingDirection());
0655     ac->addAction(QStringLiteral("rtl_page_layout"), d->aReadingDirection);
0656     connect(d->aReadingDirection, &QAction::toggled, this, &PageView::slotReadingDirectionToggled);
0657     connect(Okular::SettingsCore::self(), &Okular::SettingsCore::configChanged, this, &PageView::slotUpdateReadingDirectionAction);
0658 
0659     // Mouse mode actions for viewer mode
0660     d->mouseModeActionGroup = new QActionGroup(this);
0661     d->mouseModeActionGroup->setExclusive(true);
0662     d->aMouseNormal = new QAction(QIcon::fromTheme(QStringLiteral("transform-browse")), i18n("&Browse"), this);
0663     ac->addAction(QStringLiteral("mouse_drag"), d->aMouseNormal);
0664     connect(d->aMouseNormal, &QAction::triggered, this, &PageView::slotSetMouseNormal);
0665     d->aMouseNormal->setCheckable(true);
0666     ac->setDefaultShortcut(d->aMouseNormal, QKeySequence(Qt::CTRL | Qt::Key_1));
0667     d->aMouseNormal->setActionGroup(d->mouseModeActionGroup);
0668     d->aMouseNormal->setChecked(Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::Browse);
0669 
0670     d->aMouseZoom = new QAction(QIcon::fromTheme(QStringLiteral("page-zoom")), i18n("&Zoom"), this);
0671     ac->addAction(QStringLiteral("mouse_zoom"), d->aMouseZoom);
0672     connect(d->aMouseZoom, &QAction::triggered, this, &PageView::slotSetMouseZoom);
0673     d->aMouseZoom->setCheckable(true);
0674     ac->setDefaultShortcut(d->aMouseZoom, QKeySequence(Qt::CTRL | Qt::Key_2));
0675     d->aMouseZoom->setActionGroup(d->mouseModeActionGroup);
0676     d->aMouseZoom->setChecked(Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::Zoom);
0677 
0678     d->aColorModeMenu = new ColorModeMenu(ac, this);
0679 }
0680 
0681 // WARNING: 'setupViewerActions' must have been called before this method
0682 void PageView::setupActions(KActionCollection *ac)
0683 {
0684     d->actionCollection = ac;
0685 
0686     ac->setDefaultShortcuts(d->aZoomIn, KStandardShortcut::zoomIn());
0687     ac->setDefaultShortcuts(d->aZoomOut, KStandardShortcut::zoomOut());
0688 
0689     // Mouse-Mode actions
0690     d->aMouseSelect = new QAction(QIcon::fromTheme(QStringLiteral("select-rectangular")), i18n("Area &Selection"), this);
0691     ac->addAction(QStringLiteral("mouse_select"), d->aMouseSelect);
0692     connect(d->aMouseSelect, &QAction::triggered, this, &PageView::slotSetMouseSelect);
0693     d->aMouseSelect->setCheckable(true);
0694     ac->setDefaultShortcut(d->aMouseSelect, Qt::CTRL | Qt::Key_3);
0695     d->aMouseSelect->setActionGroup(d->mouseModeActionGroup);
0696 
0697     d->aMouseTextSelect = new QAction(QIcon::fromTheme(QStringLiteral("edit-select-text")), i18n("&Text Selection"), this);
0698     ac->addAction(QStringLiteral("mouse_textselect"), d->aMouseTextSelect);
0699     connect(d->aMouseTextSelect, &QAction::triggered, this, &PageView::slotSetMouseTextSelect);
0700     d->aMouseTextSelect->setCheckable(true);
0701     ac->setDefaultShortcut(d->aMouseTextSelect, Qt::CTRL | Qt::Key_4);
0702     d->aMouseTextSelect->setActionGroup(d->mouseModeActionGroup);
0703 
0704     d->aMouseTableSelect = new QAction(QIcon::fromTheme(QStringLiteral("table")), i18n("T&able Selection"), this);
0705     ac->addAction(QStringLiteral("mouse_tableselect"), d->aMouseTableSelect);
0706     connect(d->aMouseTableSelect, &QAction::triggered, this, &PageView::slotSetMouseTableSelect);
0707     d->aMouseTableSelect->setCheckable(true);
0708     ac->setDefaultShortcut(d->aMouseTableSelect, Qt::CTRL | Qt::Key_5);
0709     d->aMouseTableSelect->setActionGroup(d->mouseModeActionGroup);
0710 
0711     d->aMouseMagnifier = new QAction(QIcon::fromTheme(QStringLiteral("document-preview")), i18n("&Magnifier"), this);
0712     ac->addAction(QStringLiteral("mouse_magnifier"), d->aMouseMagnifier);
0713     connect(d->aMouseMagnifier, &QAction::triggered, this, &PageView::slotSetMouseMagnifier);
0714     d->aMouseMagnifier->setCheckable(true);
0715     ac->setDefaultShortcut(d->aMouseMagnifier, Qt::CTRL | Qt::Key_6);
0716     d->aMouseMagnifier->setActionGroup(d->mouseModeActionGroup);
0717     d->aMouseMagnifier->setChecked(Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::Magnifier);
0718 
0719     // Mouse mode selection tools menu
0720     d->aMouseModeMenu = new ToggleActionMenu(i18nc("@action", "Selection Tools"), this);
0721     d->aMouseModeMenu->setPopupMode(QToolButton::MenuButtonPopup);
0722     d->aMouseModeMenu->addAction(d->aMouseSelect);
0723     d->aMouseModeMenu->addAction(d->aMouseTextSelect);
0724     d->aMouseModeMenu->addAction(d->aMouseTableSelect);
0725     connect(d->aMouseModeMenu->menu(), &QMenu::triggered, d->aMouseModeMenu, &ToggleActionMenu::setDefaultAction);
0726     ac->addAction(QStringLiteral("mouse_selecttools"), d->aMouseModeMenu);
0727 
0728     switch (Okular::Settings::mouseMode()) {
0729     case Okular::Settings::EnumMouseMode::TextSelect:
0730         d->aMouseTextSelect->setChecked(true);
0731         d->aMouseModeMenu->setDefaultAction(d->aMouseTextSelect);
0732         break;
0733     case Okular::Settings::EnumMouseMode::RectSelect:
0734         d->aMouseSelect->setChecked(true);
0735         d->aMouseModeMenu->setDefaultAction(d->aMouseSelect);
0736         break;
0737     case Okular::Settings::EnumMouseMode::TableSelect:
0738         d->aMouseTableSelect->setChecked(true);
0739         d->aMouseModeMenu->setDefaultAction(d->aMouseTableSelect);
0740         break;
0741     default:
0742         d->aMouseModeMenu->setDefaultAction(d->aMouseTextSelect);
0743     }
0744 
0745     // Create signature action
0746     d->aSignature = new QAction(QIcon::fromTheme(QStringLiteral("document-edit-sign")), i18n("Digitally &Sign..."), this);
0747     ac->addAction(QStringLiteral("add_digital_signature"), d->aSignature);
0748     connect(d->aSignature, &QAction::triggered, this, &PageView::slotSignature);
0749 
0750     // speak actions
0751 #if HAVE_SPEECH
0752     d->aSpeakDoc = new QAction(QIcon::fromTheme(QStringLiteral("text-speak")), i18n("Speak Whole Document"), this);
0753     ac->addAction(QStringLiteral("speak_document"), d->aSpeakDoc);
0754     d->aSpeakDoc->setEnabled(false);
0755     connect(d->aSpeakDoc, &QAction::triggered, this, &PageView::slotSpeakDocument);
0756 
0757     d->aSpeakPage = new QAction(QIcon::fromTheme(QStringLiteral("text-speak")), i18n("Speak Current Page"), this);
0758     ac->addAction(QStringLiteral("speak_current_page"), d->aSpeakPage);
0759     d->aSpeakPage->setEnabled(false);
0760     connect(d->aSpeakPage, &QAction::triggered, this, &PageView::slotSpeakCurrentPage);
0761 
0762     d->aSpeakStop = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-stop")), i18n("Stop Speaking"), this);
0763     ac->addAction(QStringLiteral("speak_stop_all"), d->aSpeakStop);
0764     d->aSpeakStop->setEnabled(false);
0765     connect(d->aSpeakStop, &QAction::triggered, this, &PageView::slotStopSpeaks);
0766 
0767     d->aSpeakPauseResume = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-pause")), i18n("Pause/Resume Speaking"), this);
0768     ac->addAction(QStringLiteral("speak_pause_resume"), d->aSpeakPauseResume);
0769     d->aSpeakPauseResume->setEnabled(false);
0770     connect(d->aSpeakPauseResume, &QAction::triggered, this, &PageView::slotPauseResumeSpeech);
0771 #else
0772     d->aSpeakDoc = nullptr;
0773     d->aSpeakPage = nullptr;
0774     d->aSpeakStop = nullptr;
0775     d->aSpeakPauseResume = nullptr;
0776 #endif
0777 
0778     // Other actions
0779     QAction *su = new QAction(i18n("Scroll Up"), this);
0780     ac->addAction(QStringLiteral("view_scroll_up"), su);
0781     connect(su, &QAction::triggered, this, &PageView::slotAutoScrollUp);
0782     ac->setDefaultShortcut(su, QKeySequence(Qt::SHIFT | Qt::Key_Up));
0783     addAction(su);
0784 
0785     QAction *sd = new QAction(i18n("Scroll Down"), this);
0786     ac->addAction(QStringLiteral("view_scroll_down"), sd);
0787     connect(sd, &QAction::triggered, this, &PageView::slotAutoScrollDown);
0788     ac->setDefaultShortcut(sd, QKeySequence(Qt::SHIFT | Qt::Key_Down));
0789     addAction(sd);
0790 
0791     QAction *spu = new QAction(i18n("Scroll Page Up"), this);
0792     ac->addAction(QStringLiteral("view_scroll_page_up"), spu);
0793     connect(spu, &QAction::triggered, this, &PageView::slotScrollUp);
0794     ac->setDefaultShortcut(spu, QKeySequence(Qt::SHIFT | Qt::Key_Space));
0795     addAction(spu);
0796 
0797     QAction *spd = new QAction(i18n("Scroll Page Down"), this);
0798     ac->addAction(QStringLiteral("view_scroll_page_down"), spd);
0799     connect(spd, &QAction::triggered, this, &PageView::slotScrollDown);
0800     ac->setDefaultShortcut(spd, QKeySequence(Qt::Key_Space));
0801     addAction(spd);
0802 
0803     d->aToggleForms = new KToggleAction(i18n("Show Forms"), this);
0804     ac->addAction(QStringLiteral("view_toggle_forms"), d->aToggleForms);
0805     connect(d->aToggleForms, &QAction::toggled, this, &PageView::slotToggleForms);
0806     d->aToggleForms->setEnabled(false);
0807     toggleFormWidgets(false);
0808 
0809     // Setup undo and redo actions
0810     QAction *kundo = KStandardAction::create(KStandardAction::Undo, d->document, SLOT(undo()), ac);
0811     QAction *kredo = KStandardAction::create(KStandardAction::Redo, d->document, SLOT(redo()), ac);
0812     connect(d->document, &Okular::Document::canUndoChanged, kundo, &QAction::setEnabled);
0813     connect(d->document, &Okular::Document::canRedoChanged, kredo, &QAction::setEnabled);
0814     kundo->setEnabled(false);
0815     kredo->setEnabled(false);
0816 
0817     d->annotator = new PageViewAnnotator(this, d->document);
0818     connect(d->annotator, &PageViewAnnotator::toolActive, this, [&](bool selected) {
0819         if (selected) {
0820             QAction *aMouseMode = d->mouseModeActionGroup->checkedAction();
0821             if (aMouseMode) {
0822                 aMouseMode->setChecked(false);
0823             }
0824         } else {
0825             switch (d->mouseMode) {
0826             case Okular::Settings::EnumMouseMode::Browse:
0827                 d->aMouseNormal->setChecked(true);
0828                 break;
0829             case Okular::Settings::EnumMouseMode::Zoom:
0830                 d->aMouseZoom->setChecked(true);
0831                 break;
0832             case Okular::Settings::EnumMouseMode::RectSelect:
0833                 d->aMouseSelect->setChecked(true);
0834                 break;
0835             case Okular::Settings::EnumMouseMode::TableSelect:
0836                 d->aMouseTableSelect->setChecked(true);
0837                 break;
0838             case Okular::Settings::EnumMouseMode::Magnifier:
0839                 d->aMouseMagnifier->setChecked(true);
0840                 break;
0841             case Okular::Settings::EnumMouseMode::TextSelect:
0842                 d->aMouseTextSelect->setChecked(true);
0843                 break;
0844             }
0845         }
0846     });
0847     connect(d->annotator, &PageViewAnnotator::toolActive, d->mouseAnnotation, &MouseAnnotation::reset);
0848     connect(d->annotator, &PageViewAnnotator::requestOpenFile, this, &PageView::requestOpenFile);
0849     d->annotator->setupActions(ac);
0850 }
0851 
0852 bool PageView::canFitPageWidth() const
0853 {
0854     return Okular::Settings::viewMode() != Okular::Settings::EnumViewMode::Single || d->zoomMode != ZoomFitWidth;
0855 }
0856 
0857 void PageView::fitPageWidth(int page)
0858 {
0859     // zoom: Fit Width, columns: 1. setActions + relayout + setPage + update
0860     d->zoomMode = ZoomFitWidth;
0861     Okular::Settings::setViewMode(Okular::Settings::EnumViewMode::Single);
0862     d->aZoomFitWidth->setChecked(true);
0863     d->aZoomFitPage->setChecked(false);
0864     d->aZoomAutoFit->setChecked(false);
0865     updateViewMode(Okular::Settings::EnumViewMode::Single);
0866     viewport()->setUpdatesEnabled(false);
0867     slotRelayoutPages();
0868     viewport()->setUpdatesEnabled(true);
0869     d->document->setViewportPage(page);
0870     updateZoomText();
0871     setFocus();
0872 }
0873 
0874 void PageView::openAnnotationWindow(Okular::Annotation *annotation, int pageNumber)
0875 {
0876     if (!annotation) {
0877         return;
0878     }
0879 
0880     // find the annot window
0881     AnnotWindow *existWindow = nullptr;
0882     for (AnnotWindow *aw : std::as_const(d->m_annowindows)) {
0883         if (aw->annotation() == annotation) {
0884             existWindow = aw;
0885             break;
0886         }
0887     }
0888 
0889     if (existWindow == nullptr) {
0890         existWindow = new AnnotWindow(this, annotation, d->document, pageNumber);
0891         connect(existWindow, &QObject::destroyed, this, &PageView::slotAnnotationWindowDestroyed);
0892 
0893         d->m_annowindows << existWindow;
0894     } else {
0895         existWindow->raise();
0896         existWindow->findChild<KTextEdit *>()->setFocus();
0897     }
0898 
0899     existWindow->show();
0900 }
0901 
0902 void PageView::slotAnnotationWindowDestroyed(QObject *window)
0903 {
0904     d->m_annowindows.remove(static_cast<AnnotWindow *>(window));
0905 }
0906 
0907 void PageView::displayMessage(const QString &message, const QString &details, PageViewMessage::Icon icon, int duration)
0908 {
0909     if (!Okular::Settings::showOSD()) {
0910         if (icon == PageViewMessage::Error) {
0911             if (!details.isEmpty()) {
0912                 KMessageBox::detailedError(this, message, details);
0913             } else {
0914                 KMessageBox::error(this, message);
0915             }
0916         }
0917         return;
0918     }
0919 
0920     // hide messageWindow if string is empty
0921     if (message.isEmpty()) {
0922         d->messageWindow->hide();
0923         return;
0924     }
0925 
0926     // display message (duration is length dependent)
0927     if (duration == -1) {
0928         duration = 500 + 100 * message.length();
0929         if (!details.isEmpty()) {
0930             duration += 500 + 100 * details.length();
0931         }
0932     }
0933     d->messageWindow->display(message, details, icon, duration);
0934 }
0935 
0936 void PageView::reparseConfig()
0937 {
0938     // set smooth scrolling policies
0939     PageView::updateSmoothScrollAnimationSpeed();
0940 
0941     // set the scroll bars policies
0942     Qt::ScrollBarPolicy scrollBarMode = Okular::Settings::showScrollBars() ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff;
0943     if (horizontalScrollBarPolicy() != scrollBarMode) {
0944         setHorizontalScrollBarPolicy(scrollBarMode);
0945         setVerticalScrollBarPolicy(scrollBarMode);
0946     }
0947 
0948     if (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Summary && ((int)Okular::Settings::viewColumns() != d->setting_viewCols)) {
0949         d->setting_viewCols = Okular::Settings::viewColumns();
0950 
0951         slotRelayoutPages();
0952     }
0953 
0954     if (Okular::Settings::rtlReadingDirection() != d->rtl_Mode) {
0955         d->rtl_Mode = Okular::Settings::rtlReadingDirection();
0956         slotRelayoutPages();
0957     }
0958 
0959     updatePageStep();
0960 
0961     if (d->annotator) {
0962         d->annotator->reparseConfig();
0963     }
0964 
0965     // Something like invert colors may have changed
0966     // As we don't have a way to find out the old value
0967     // We just update the viewport, this shouldn't be that bad
0968     // since it's just a repaint of pixmaps we already have
0969     viewport()->update();
0970 }
0971 
0972 KActionCollection *PageView::actionCollection() const
0973 {
0974     return d->actionCollection;
0975 }
0976 
0977 QAction *PageView::toggleFormsAction() const
0978 {
0979     return d->aToggleForms;
0980 }
0981 
0982 int PageView::contentAreaWidth() const
0983 {
0984     return horizontalScrollBar()->maximum() + viewport()->width();
0985 }
0986 
0987 int PageView::contentAreaHeight() const
0988 {
0989     return verticalScrollBar()->maximum() + viewport()->height();
0990 }
0991 
0992 QPoint PageView::contentAreaPosition() const
0993 {
0994     return QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value());
0995 }
0996 
0997 QPoint PageView::contentAreaPoint(const QPoint pos) const
0998 {
0999     return pos + contentAreaPosition();
1000 }
1001 
1002 QPointF PageView::contentAreaPoint(const QPointF pos) const
1003 {
1004     return pos + contentAreaPosition();
1005 }
1006 
1007 QString PageViewPrivate::selectedText() const
1008 {
1009     if (pagesWithTextSelection.isEmpty()) {
1010         return QString();
1011     }
1012 
1013     QString text;
1014     QList<int> selpages = pagesWithTextSelection.values();
1015     std::sort(selpages.begin(), selpages.end());
1016     const Okular::Page *pg = nullptr;
1017     if (selpages.count() == 1) {
1018         pg = document->page(selpages.first());
1019         text.append(pg->text(pg->textSelection(), Okular::TextPage::CentralPixelTextAreaInclusionBehaviour));
1020     } else {
1021         pg = document->page(selpages.first());
1022         text.append(pg->text(pg->textSelection(), Okular::TextPage::CentralPixelTextAreaInclusionBehaviour));
1023         int end = selpages.count() - 1;
1024         for (int i = 1; i < end; ++i) {
1025             pg = document->page(selpages.at(i));
1026             text.append(pg->text(nullptr, Okular::TextPage::CentralPixelTextAreaInclusionBehaviour));
1027         }
1028         pg = document->page(selpages.last());
1029         text.append(pg->text(pg->textSelection(), Okular::TextPage::CentralPixelTextAreaInclusionBehaviour));
1030     }
1031 
1032     if (text.endsWith(QLatin1Char('\n'))) {
1033         text.chop(1);
1034     }
1035     return text;
1036 }
1037 
1038 QMimeData *PageView::getTableContents() const
1039 {
1040     QString selText;
1041     QString selHtml;
1042     QList<double> xs = d->tableSelectionCols;
1043     QList<double> ys = d->tableSelectionRows;
1044     xs.prepend(0.0);
1045     xs.append(1.0);
1046     ys.prepend(0.0);
1047     ys.append(1.0);
1048     selHtml = QString::fromLatin1(
1049         "<html><head>"
1050         "<meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">"
1051         "</head><body><table>");
1052     for (int r = 0; r + 1 < ys.length(); r++) {
1053         selHtml += QLatin1String("<tr>");
1054         for (int c = 0; c + 1 < xs.length(); c++) {
1055             Okular::NormalizedRect cell(xs[c], ys[r], xs[c + 1], ys[r + 1]);
1056             if (c) {
1057                 selText += QLatin1Char('\t');
1058             }
1059             QString txt;
1060             for (const TableSelectionPart &tsp : std::as_const(d->tableSelectionParts)) {
1061                 // first, crop the cell to this part
1062                 if (!tsp.rectInSelection.intersects(cell)) {
1063                     continue;
1064                 }
1065                 Okular::NormalizedRect cellPart = tsp.rectInSelection & cell; // intersection
1066 
1067                 // second, convert it from table coordinates to part coordinates
1068                 cellPart.left -= tsp.rectInSelection.left;
1069                 cellPart.left /= (tsp.rectInSelection.right - tsp.rectInSelection.left);
1070                 cellPart.right -= tsp.rectInSelection.left;
1071                 cellPart.right /= (tsp.rectInSelection.right - tsp.rectInSelection.left);
1072                 cellPart.top -= tsp.rectInSelection.top;
1073                 cellPart.top /= (tsp.rectInSelection.bottom - tsp.rectInSelection.top);
1074                 cellPart.bottom -= tsp.rectInSelection.top;
1075                 cellPart.bottom /= (tsp.rectInSelection.bottom - tsp.rectInSelection.top);
1076 
1077                 // third, convert from part coordinates to item coordinates
1078                 cellPart.left *= (tsp.rectInItem.right - tsp.rectInItem.left);
1079                 cellPart.left += tsp.rectInItem.left;
1080                 cellPart.right *= (tsp.rectInItem.right - tsp.rectInItem.left);
1081                 cellPart.right += tsp.rectInItem.left;
1082                 cellPart.top *= (tsp.rectInItem.bottom - tsp.rectInItem.top);
1083                 cellPart.top += tsp.rectInItem.top;
1084                 cellPart.bottom *= (tsp.rectInItem.bottom - tsp.rectInItem.top);
1085                 cellPart.bottom += tsp.rectInItem.top;
1086 
1087                 // now get the text
1088                 Okular::RegularAreaRect rects;
1089                 rects.append(cellPart);
1090                 txt += tsp.item->page()->text(&rects, Okular::TextPage::CentralPixelTextAreaInclusionBehaviour);
1091             }
1092             QString html = txt;
1093             selText += txt.replace(QLatin1Char('\n'), QLatin1Char(' '));
1094             html.replace(QLatin1Char('&'), QLatin1String("&amp;")).replace(QLatin1Char('<'), QLatin1String("&lt;")).replace(QLatin1Char('>'), QLatin1String("&gt;"));
1095             // Remove newlines, do not turn them into <br>, because
1096             // Excel interprets <br> within cell as new cell...
1097             html.replace(QLatin1Char('\n'), QLatin1String(" "));
1098             selHtml += QStringLiteral("<td>") + html + QStringLiteral("</td>");
1099         }
1100         selText += QLatin1Char('\n');
1101         selHtml += QLatin1String("</tr>\n");
1102     }
1103     selHtml += QLatin1String("</table></body></html>\n");
1104 
1105     QMimeData *md = new QMimeData();
1106     md->setText(selText);
1107     md->setHtml(selHtml);
1108 
1109     return md;
1110 }
1111 
1112 void PageView::copyTextSelection() const
1113 {
1114     switch (d->mouseMode) {
1115     case Okular::Settings::EnumMouseMode::Browse: {
1116         if (auto *annotation = d->mouseAnnotation->annotation()) {
1117             const QString text = annotation->contents();
1118             if (!text.isEmpty()) {
1119                 QClipboard *cb = QApplication::clipboard();
1120                 cb->setText(text, QClipboard::Clipboard);
1121             }
1122         }
1123     } break;
1124 
1125     case Okular::Settings::EnumMouseMode::TableSelect: {
1126         QClipboard *cb = QApplication::clipboard();
1127         cb->setMimeData(getTableContents(), QClipboard::Clipboard);
1128     } break;
1129 
1130     case Okular::Settings::EnumMouseMode::TextSelect: {
1131         const QString text = d->selectedText();
1132         if (!text.isEmpty()) {
1133             QClipboard *cb = QApplication::clipboard();
1134             cb->setText(text, QClipboard::Clipboard);
1135         }
1136     } break;
1137     }
1138 }
1139 
1140 void PageView::selectAll()
1141 {
1142     for (const PageViewItem *item : std::as_const(d->items)) {
1143         Okular::RegularAreaRect *area = textSelectionForItem(item);
1144         d->pagesWithTextSelection.insert(item->pageNumber());
1145         d->document->setPageTextSelection(item->pageNumber(), area, palette().color(QPalette::Active, QPalette::Highlight));
1146     }
1147 }
1148 
1149 void PageView::createAnnotationsVideoWidgets(PageViewItem *item, const QList<Okular::Annotation *> &annotations)
1150 {
1151     qDeleteAll(item->videoWidgets());
1152     item->videoWidgets().clear();
1153 
1154     for (Okular::Annotation *a : annotations) {
1155         if (a->subType() == Okular::Annotation::AMovie) {
1156             Okular::MovieAnnotation *movieAnn = static_cast<Okular::MovieAnnotation *>(a);
1157             VideoWidget *vw = new VideoWidget(movieAnn, movieAnn->movie(), d->document, viewport());
1158             item->videoWidgets().insert(movieAnn->movie(), vw);
1159             vw->pageInitialized();
1160         } else if (a->subType() == Okular::Annotation::ARichMedia) {
1161             Okular::RichMediaAnnotation *richMediaAnn = static_cast<Okular::RichMediaAnnotation *>(a);
1162             VideoWidget *vw = new VideoWidget(richMediaAnn, richMediaAnn->movie(), d->document, viewport());
1163             item->videoWidgets().insert(richMediaAnn->movie(), vw);
1164             vw->pageInitialized();
1165         } else if (a->subType() == Okular::Annotation::AScreen) {
1166             const Okular::ScreenAnnotation *screenAnn = static_cast<Okular::ScreenAnnotation *>(a);
1167             Okular::Movie *movie = GuiUtils::renditionMovieFromScreenAnnotation(screenAnn);
1168             if (movie) {
1169                 VideoWidget *vw = new VideoWidget(screenAnn, movie, d->document, viewport());
1170                 item->videoWidgets().insert(movie, vw);
1171                 vw->pageInitialized();
1172             }
1173         }
1174     }
1175 }
1176 
1177 // BEGIN DocumentObserver inherited methods
1178 void PageView::notifySetup(const QVector<Okular::Page *> &pageSet, int setupFlags)
1179 {
1180     bool documentChanged = setupFlags & Okular::DocumentObserver::DocumentChanged;
1181     const bool allowfillforms = d->document->isAllowed(Okular::AllowFillForms);
1182 
1183     // reuse current pages if nothing new
1184     if ((pageSet.count() == d->items.count()) && !documentChanged && !(setupFlags & Okular::DocumentObserver::NewLayoutForPages)) {
1185         int count = pageSet.count();
1186         for (int i = 0; (i < count) && !documentChanged; i++) {
1187             if ((int)pageSet[i]->number() != d->items[i]->pageNumber()) {
1188                 documentChanged = true;
1189             } else {
1190                 // even if the document has not changed, allowfillforms may have
1191                 // changed, so update all fields' "canBeFilled" flag
1192                 const QSet<FormWidgetIface *> formWidgetsList = d->items[i]->formWidgets();
1193                 for (FormWidgetIface *w : formWidgetsList) {
1194                     w->setCanBeFilled(allowfillforms);
1195                 }
1196             }
1197         }
1198 
1199         if (!documentChanged) {
1200             if (setupFlags & Okular::DocumentObserver::UrlChanged) {
1201                 // Here with UrlChanged and no document changed it means we
1202                 // need to update all the Annotation* and Form* otherwise
1203                 // they still point to the old document ones, luckily the old ones are still
1204                 // around so we can look for the new ones using unique ids, etc
1205                 d->mouseAnnotation->updateAnnotationPointers();
1206 
1207                 for (AnnotWindow *aw : std::as_const(d->m_annowindows)) {
1208                     Okular::Annotation *newA = d->document->page(aw->pageNumber())->annotation(aw->annotation()->uniqueName());
1209                     aw->updateAnnotation(newA);
1210                 }
1211 
1212                 const QRect viewportRect(horizontalScrollBar()->value(), verticalScrollBar()->value(), viewport()->width(), viewport()->height());
1213                 for (int i = 0; i < count; i++) {
1214                     PageViewItem *item = d->items[i];
1215                     const QSet<FormWidgetIface *> fws = item->formWidgets();
1216                     for (FormWidgetIface *w : fws) {
1217                         Okular::FormField *f = Okular::PagePrivate::findEquivalentForm(d->document->page(i), w->formField());
1218                         if (f) {
1219                             w->setFormField(f);
1220                         } else {
1221                             qWarning() << "Lost form field on document save, something is wrong";
1222                             item->formWidgets().remove(w);
1223                             delete w;
1224                         }
1225                     }
1226 
1227                     // For the video widgets we don't really care about reusing them since they don't contain much info so just
1228                     // create them again
1229                     createAnnotationsVideoWidgets(item, pageSet[i]->annotations());
1230                     const QHash<Okular::Movie *, VideoWidget *> videoWidgets = item->videoWidgets();
1231                     for (VideoWidget *vw : videoWidgets) {
1232                         const Okular::NormalizedRect r = vw->normGeometry();
1233                         vw->setGeometry(qRound(item->uncroppedGeometry().left() + item->uncroppedWidth() * r.left) + 1 - viewportRect.left(),
1234                                         qRound(item->uncroppedGeometry().top() + item->uncroppedHeight() * r.top) + 1 - viewportRect.top(),
1235                                         qRound(fabs(r.right - r.left) * item->uncroppedGeometry().width()),
1236                                         qRound(fabs(r.bottom - r.top) * item->uncroppedGeometry().height()));
1237 
1238                         // Workaround, otherwise the size somehow gets lost
1239                         vw->show();
1240                         vw->hide();
1241                     }
1242                 }
1243             }
1244 
1245             return;
1246         }
1247     }
1248 
1249     // mouseAnnotation must not access our PageViewItem widgets any longer
1250     d->mouseAnnotation->reset();
1251 
1252     // delete all widgets (one for each page in pageSet)
1253     qDeleteAll(d->items);
1254     d->items.clear();
1255     d->visibleItems.clear();
1256     d->pagesWithTextSelection.clear();
1257     toggleFormWidgets(false);
1258     if (d->formsWidgetController) {
1259         d->formsWidgetController->dropRadioButtons();
1260     }
1261 
1262     bool haspages = !pageSet.isEmpty();
1263     bool hasformwidgets = false;
1264     // create children widgets
1265     for (const Okular::Page *page : pageSet) {
1266         PageViewItem *item = new PageViewItem(page);
1267         d->items.push_back(item);
1268 #ifdef PAGEVIEW_DEBUG
1269         qCDebug(OkularUiDebug).nospace() << "cropped geom for " << d->items.last()->pageNumber() << " is " << d->items.last()->croppedGeometry();
1270 #endif
1271         const QList<Okular::FormField *> pageFields = page->formFields();
1272         for (Okular::FormField *ff : pageFields) {
1273             FormWidgetIface *w = FormWidgetFactory::createWidget(ff, this);
1274             if (w) {
1275                 w->setPageItem(item);
1276                 w->setFormWidgetsController(d->formWidgetsController());
1277                 w->setVisibility(false);
1278                 w->setCanBeFilled(allowfillforms);
1279                 item->formWidgets().insert(w);
1280                 hasformwidgets = true;
1281             }
1282         }
1283 
1284         createAnnotationsVideoWidgets(item, page->annotations());
1285     }
1286 
1287     // invalidate layout so relayout/repaint will happen on next viewport change
1288     if (haspages) {
1289         // We do a delayed call to slotRelayoutPages but also set the dirtyLayout
1290         // because we might end up in notifyViewportChanged while slotRelayoutPages
1291         // has not been done and we don't want that to happen
1292         d->dirtyLayout = true;
1293         QMetaObject::invokeMethod(this, "slotRelayoutPages", Qt::QueuedConnection);
1294     } else {
1295         // update the mouse cursor when closing because we may have close through a link and
1296         // want the cursor to come back to the normal cursor
1297         updateCursor();
1298         // then, make the message window and scrollbars disappear, and trigger a repaint
1299         d->messageWindow->hide();
1300         resizeContentArea(QSize(0, 0));
1301         viewport()->update(); // when there is no change to the scrollbars, no repaint would
1302                               // be done and the old document would still be shown
1303     }
1304 
1305     // OSD (Message balloons) to display pages
1306     if (documentChanged && pageSet.count() > 0) {
1307         d->messageWindow->display(i18np(" Loaded a one-page document.", " Loaded a %1-page document.", pageSet.count()), QString(), PageViewMessage::Info, 4000);
1308     }
1309 
1310     updateActionState(haspages, hasformwidgets);
1311 
1312     // We need to assign it to a different list otherwise slotAnnotationWindowDestroyed
1313     // will bite us and clear d->m_annowindows
1314     QSet<AnnotWindow *> annowindows = d->m_annowindows;
1315     d->m_annowindows.clear();
1316     qDeleteAll(annowindows);
1317 
1318     selectionClear();
1319 }
1320 
1321 void PageView::updateActionState(bool haspages, bool hasformwidgets)
1322 {
1323     if (d->aTrimMargins) {
1324         d->aTrimMargins->setEnabled(haspages);
1325     }
1326 
1327     if (d->aTrimToSelection) {
1328         d->aTrimToSelection->setEnabled(haspages);
1329     }
1330 
1331     if (d->aViewModeMenu) {
1332         d->aViewModeMenu->setEnabled(haspages);
1333     }
1334 
1335     if (d->aViewContinuous) {
1336         d->aViewContinuous->setEnabled(haspages);
1337     }
1338 
1339     updateZoomActionsEnabledStatus();
1340 
1341     if (d->aColorModeMenu) {
1342         d->aColorModeMenu->setEnabled(haspages);
1343     }
1344 
1345     if (d->aReadingDirection) {
1346         d->aReadingDirection->setEnabled(haspages);
1347     }
1348 
1349     if (d->mouseModeActionGroup) {
1350         d->mouseModeActionGroup->setEnabled(haspages);
1351     }
1352     if (d->aMouseModeMenu) {
1353         d->aMouseModeMenu->setEnabled(haspages);
1354     }
1355 
1356     if (d->aRotateClockwise) {
1357         d->aRotateClockwise->setEnabled(haspages);
1358     }
1359     if (d->aRotateCounterClockwise) {
1360         d->aRotateCounterClockwise->setEnabled(haspages);
1361     }
1362     if (d->aRotateOriginal) {
1363         d->aRotateOriginal->setEnabled(haspages);
1364     }
1365     if (d->aToggleForms) { // may be null if dummy mode is on
1366         d->aToggleForms->setEnabled(haspages && hasformwidgets);
1367     }
1368     bool allowAnnotations = d->document->isAllowed(Okular::AllowNotes);
1369     if (d->annotator) {
1370         bool allowTools = haspages && allowAnnotations;
1371         d->annotator->setToolsEnabled(allowTools);
1372         d->annotator->setTextToolsEnabled(allowTools && d->document->supportsSearching());
1373     }
1374 
1375     if (d->aSignature) {
1376         const bool canSign = d->document->canSign();
1377         d->aSignature->setEnabled(canSign && haspages);
1378     }
1379 
1380 #if HAVE_SPEECH
1381     if (d->aSpeakDoc) {
1382         const bool enablettsactions = haspages ? Okular::Settings::useTTS() : false;
1383         d->aSpeakDoc->setEnabled(enablettsactions);
1384         d->aSpeakPage->setEnabled(enablettsactions);
1385     }
1386 #endif
1387     if (d->aMouseMagnifier) {
1388         d->aMouseMagnifier->setEnabled(d->document->supportsTiles());
1389     }
1390     if (d->aFitWindowToPage) {
1391         d->aFitWindowToPage->setEnabled(haspages && !getContinuousMode());
1392     }
1393 }
1394 
1395 void PageView::setupActionsPostGUIActivated()
1396 {
1397     d->annotator->setupActionsPostGUIActivated();
1398 }
1399 
1400 bool PageView::areSourceLocationsShownGraphically() const
1401 {
1402     return Okular::Settings::showSourceLocationsGraphically();
1403 }
1404 
1405 void PageView::setShowSourceLocationsGraphically(bool show)
1406 {
1407     if (show == Okular::Settings::showSourceLocationsGraphically()) {
1408         return;
1409     }
1410     Okular::Settings::setShowSourceLocationsGraphically(show);
1411     viewport()->update();
1412 }
1413 
1414 void PageView::setLastSourceLocationViewport(const Okular::DocumentViewport &vp)
1415 {
1416     if (vp.rePos.enabled) {
1417         d->lastSourceLocationViewportNormalizedX = normClamp(vp.rePos.normalizedX, 0.5);
1418         d->lastSourceLocationViewportNormalizedY = normClamp(vp.rePos.normalizedY, 0.0);
1419     } else {
1420         d->lastSourceLocationViewportNormalizedX = 0.5;
1421         d->lastSourceLocationViewportNormalizedY = 0.0;
1422     }
1423     d->lastSourceLocationViewportPageNumber = vp.pageNumber;
1424     viewport()->update();
1425 }
1426 
1427 void PageView::clearLastSourceLocationViewport()
1428 {
1429     d->lastSourceLocationViewportPageNumber = -1;
1430     d->lastSourceLocationViewportNormalizedX = 0.0;
1431     d->lastSourceLocationViewportNormalizedY = 0.0;
1432     viewport()->update();
1433 }
1434 
1435 void PageView::notifyViewportChanged(bool smoothMove)
1436 {
1437     QMetaObject::invokeMethod(this, "slotRealNotifyViewportChanged", Qt::QueuedConnection, Q_ARG(bool, smoothMove));
1438 }
1439 
1440 void PageView::slotRealNotifyViewportChanged(bool smoothMove)
1441 {
1442     // if we are the one changing viewport, skip this notify
1443     if (d->blockViewport) {
1444         return;
1445     }
1446 
1447     // block setViewport outgoing calls
1448     d->blockViewport = true;
1449 
1450     // find PageViewItem matching the viewport description
1451     const Okular::DocumentViewport &vp = d->document->viewport();
1452     const PageViewItem *item = nullptr;
1453     for (const PageViewItem *tmpItem : std::as_const(d->items)) {
1454         if (tmpItem->pageNumber() == vp.pageNumber) {
1455             item = tmpItem;
1456             break;
1457         }
1458     }
1459     if (!item) {
1460         qCWarning(OkularUiDebug) << "viewport for page" << vp.pageNumber << "has no matching item!";
1461         d->blockViewport = false;
1462         return;
1463     }
1464 #ifdef PAGEVIEW_DEBUG
1465     qCDebug(OkularUiDebug) << "document viewport changed";
1466 #endif
1467     // relayout in "Single Pages" mode or if a relayout is pending
1468     d->blockPixmapsRequest = true;
1469     if (!getContinuousMode() || d->dirtyLayout) {
1470         slotRelayoutPages();
1471     }
1472 
1473     // restore viewport center or use default {x-center,v-top} alignment
1474     const QPoint centerCoord = viewportToContentArea(vp);
1475 
1476     // if smooth movement requested, setup parameters and start it
1477     center(centerCoord.x(), centerCoord.y(), smoothMove);
1478 
1479     d->blockPixmapsRequest = false;
1480 
1481     // request visible pixmaps in the current viewport and recompute it
1482     slotRequestVisiblePixmaps();
1483 
1484     // enable setViewport calls
1485     d->blockViewport = false;
1486 
1487     if (viewport()) {
1488         viewport()->update();
1489     }
1490 
1491     // since the page has moved below cursor, update it
1492     updateCursor();
1493 }
1494 
1495 void PageView::notifyPageChanged(int pageNumber, int changedFlags)
1496 {
1497     // only handle pixmap / highlight changes notifies
1498     if (changedFlags & DocumentObserver::Bookmark) {
1499         return;
1500     }
1501 
1502     if (changedFlags & DocumentObserver::Annotations) {
1503         const QList<Okular::Annotation *> annots = d->document->page(pageNumber)->annotations();
1504         const QList<Okular::Annotation *>::ConstIterator annItEnd = annots.end();
1505         QSet<AnnotWindow *>::Iterator it = d->m_annowindows.begin();
1506         for (; it != d->m_annowindows.end();) {
1507             QList<Okular::Annotation *>::ConstIterator annIt = std::find(annots.begin(), annots.end(), (*it)->annotation());
1508             if (annIt != annItEnd) {
1509                 (*it)->reloadInfo();
1510                 ++it;
1511             } else {
1512                 AnnotWindow *w = *it;
1513                 it = d->m_annowindows.erase(it);
1514                 // Need to delete after removing from the list
1515                 // otherwise deleting will call slotAnnotationWindowDestroyed which will mess
1516                 // the list and the iterators
1517                 delete w;
1518             }
1519         }
1520 
1521         d->mouseAnnotation->notifyAnnotationChanged(pageNumber);
1522     }
1523 
1524     if (changedFlags & DocumentObserver::BoundingBox) {
1525 #ifdef PAGEVIEW_DEBUG
1526         qCDebug(OkularUiDebug) << "BoundingBox change on page" << pageNumber;
1527 #endif
1528         slotRelayoutPages();
1529         slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already!
1530         // Repaint the whole widget since layout may have changed
1531         viewport()->update();
1532         return;
1533     }
1534 
1535     // iterate over visible items: if page(pageNumber) is one of them, repaint it
1536     for (const PageViewItem *visibleItem : std::as_const(d->visibleItems)) {
1537         if (visibleItem->pageNumber() == pageNumber && visibleItem->isVisible()) {
1538             // update item's rectangle plus the little outline
1539             QRect expandedRect = visibleItem->croppedGeometry();
1540             // a PageViewItem is placed in the global page layout,
1541             // while we need to map its position in the viewport coordinates
1542             // (to get the correct area to repaint)
1543             expandedRect.translate(-contentAreaPosition());
1544             expandedRect.adjust(-1, -1, 3, 3);
1545             viewport()->update(expandedRect);
1546 
1547             // if we were "zoom-dragging" do not overwrite the "zoom-drag" cursor
1548             if (cursor().shape() != Qt::SizeVerCursor) {
1549                 // since the page has been regenerated below cursor, update it
1550                 updateCursor();
1551             }
1552             break;
1553         }
1554     }
1555 }
1556 
1557 void PageView::notifyContentsCleared(int changedFlags)
1558 {
1559     // if pixmaps were cleared, re-ask them
1560     if (changedFlags & DocumentObserver::Pixmap) {
1561         QMetaObject::invokeMethod(this, "slotRequestVisiblePixmaps", Qt::QueuedConnection);
1562     }
1563 }
1564 
1565 void PageView::notifyZoom(int factor)
1566 {
1567     if (factor > 0) {
1568         updateZoom(ZoomIn);
1569     } else {
1570         updateZoom(ZoomOut);
1571     }
1572 }
1573 
1574 bool PageView::canUnloadPixmap(int pageNumber) const
1575 {
1576     if (Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Low || Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Normal) {
1577         // if the item is visible, forbid unloading
1578         for (const PageViewItem *visibleItem : std::as_const(d->visibleItems)) {
1579             if (visibleItem->pageNumber() == pageNumber) {
1580                 return false;
1581             }
1582         }
1583     } else {
1584         // forbid unloading of the visible items, and of the previous and next
1585         for (const PageViewItem *visibleItem : std::as_const(d->visibleItems)) {
1586             if (abs(visibleItem->pageNumber() - pageNumber) <= 1) {
1587                 return false;
1588             }
1589         }
1590     }
1591     // if hidden premit unloading
1592     return true;
1593 }
1594 
1595 void PageView::notifyCurrentPageChanged(int previous, int current)
1596 {
1597     if (previous != -1) {
1598         PageViewItem *item = d->items.at(previous);
1599         if (item) {
1600             const QHash<Okular::Movie *, VideoWidget *> videoWidgetsList = item->videoWidgets();
1601             for (VideoWidget *videoWidget : videoWidgetsList) {
1602                 videoWidget->pageLeft();
1603             }
1604         }
1605 
1606         // On close, run the widget scripts, needed for running animated PDF
1607         const Okular::Page *page = d->document->page(previous);
1608         const QList<Okular::Annotation *> annotations = page->annotations();
1609         for (Okular::Annotation *annotation : annotations) {
1610             if (annotation->subType() == Okular::Annotation::AWidget) {
1611                 Okular::WidgetAnnotation *widgetAnnotation = static_cast<Okular::WidgetAnnotation *>(annotation);
1612                 d->document->processAction(widgetAnnotation->additionalAction(Okular::Annotation::PageClosing));
1613             }
1614         }
1615     }
1616 
1617     if (current != -1) {
1618         PageViewItem *item = d->items.at(current);
1619         if (item) {
1620             const QHash<Okular::Movie *, VideoWidget *> videoWidgetsList = item->videoWidgets();
1621             for (VideoWidget *videoWidget : videoWidgetsList) {
1622                 videoWidget->pageEntered();
1623             }
1624         }
1625 
1626         // update zoom text and factor if in a ZoomFit/* zoom mode
1627         if (d->zoomMode != ZoomFixed) {
1628             updateZoomText();
1629         }
1630 
1631         // Opening any widget scripts, needed for running animated PDF
1632         const Okular::Page *page = d->document->page(current);
1633         const QList<Okular::Annotation *> annotations = page->annotations();
1634         for (Okular::Annotation *annotation : annotations) {
1635             if (annotation->subType() == Okular::Annotation::AWidget) {
1636                 Okular::WidgetAnnotation *widgetAnnotation = static_cast<Okular::WidgetAnnotation *>(annotation);
1637                 d->document->processAction(widgetAnnotation->additionalAction(Okular::Annotation::PageOpening));
1638             }
1639         }
1640     }
1641 
1642     // if the view is paged (or not continuous) and there is a selected annotation,
1643     // we call reset to avoid creating an artifact in the next page.
1644     if (!getContinuousMode()) {
1645         if (d->mouseAnnotation && d->mouseAnnotation->isFocused()) {
1646             d->mouseAnnotation->reset();
1647         }
1648     }
1649 }
1650 
1651 // END DocumentObserver inherited methods
1652 
1653 // BEGIN View inherited methods
1654 bool PageView::supportsCapability(ViewCapability capability) const
1655 {
1656     switch (capability) {
1657     case Zoom:
1658     case ZoomModality:
1659     case Continuous:
1660     case ViewModeModality:
1661     case TrimMargins:
1662         return true;
1663     }
1664     return false;
1665 }
1666 
1667 Okular::View::CapabilityFlags PageView::capabilityFlags(ViewCapability capability) const
1668 {
1669     switch (capability) {
1670     case Zoom:
1671     case ZoomModality:
1672     case Continuous:
1673     case ViewModeModality:
1674     case TrimMargins:
1675         return CapabilityRead | CapabilityWrite | CapabilitySerializable;
1676     }
1677     return NoFlag;
1678 }
1679 
1680 QVariant PageView::capability(ViewCapability capability) const
1681 {
1682     switch (capability) {
1683     case Zoom:
1684         return d->zoomFactor;
1685     case ZoomModality:
1686         return d->zoomMode;
1687     case Continuous:
1688         return getContinuousMode();
1689     case ViewModeModality: {
1690         if (d->viewModeActionGroup) {
1691             const QList<QAction *> actions = d->viewModeActionGroup->actions();
1692             for (const QAction *action : actions) {
1693                 if (action->isChecked()) {
1694                     return action->data();
1695                 }
1696             }
1697         }
1698         return QVariant();
1699     }
1700     case TrimMargins:
1701         return d->aTrimMargins ? d->aTrimMargins->isChecked() : false;
1702     }
1703     return QVariant();
1704 }
1705 
1706 void PageView::setCapability(ViewCapability capability, const QVariant &option)
1707 {
1708     switch (capability) {
1709     case Zoom: {
1710         bool ok = true;
1711         double factor = option.toDouble(&ok);
1712         if (ok && factor > 0.0) {
1713             d->zoomFactor = static_cast<float>(factor);
1714             updateZoom(ZoomRefreshCurrent);
1715         }
1716         break;
1717     }
1718     case ZoomModality: {
1719         bool ok = true;
1720         int mode = option.toInt(&ok);
1721         if (ok) {
1722             if (mode >= 0 && mode < 3) {
1723                 updateZoom((ZoomMode)mode);
1724             }
1725         }
1726         break;
1727     }
1728     case ViewModeModality: {
1729         bool ok = true;
1730         int mode = option.toInt(&ok);
1731         if (ok) {
1732             if (mode >= 0 && mode < Okular::Settings::EnumViewMode::COUNT) {
1733                 updateViewMode(mode);
1734             }
1735         }
1736         break;
1737     }
1738     case Continuous: {
1739         bool mode = option.toBool();
1740         d->aViewContinuous->setChecked(mode);
1741         break;
1742     }
1743     case TrimMargins: {
1744         bool value = option.toBool();
1745         d->aTrimMargins->setChecked(value);
1746         slotTrimMarginsToggled(value);
1747         break;
1748     }
1749     }
1750 }
1751 
1752 // END View inherited methods
1753 
1754 // BEGIN widget events
1755 bool PageView::event(QEvent *event)
1756 {
1757     if (event->type() == QEvent::Gesture) {
1758         return gestureEvent(static_cast<QGestureEvent *>(event));
1759     }
1760 
1761     // do not stop the event
1762     return QAbstractScrollArea::event(event);
1763 }
1764 
1765 bool PageView::gestureEvent(QGestureEvent *event)
1766 {
1767     QPinchGesture *pinch = static_cast<QPinchGesture *>(event->gesture(Qt::PinchGesture));
1768 
1769     if (pinch) {
1770         // Viewport zoom level at the moment where the pinch gesture starts.
1771         // The viewport zoom level _during_ the gesture will be this value
1772         // times the relative zoom reported by QGestureEvent.
1773         static qreal vanillaZoom = d->zoomFactor;
1774 
1775         if (pinch->state() == Qt::GestureStarted) {
1776             vanillaZoom = d->zoomFactor;
1777             d->pinchZoomActive = true;
1778             d->scroller->handleInput(QScroller::InputRelease, QPointF());
1779             d->scroller->stop();
1780         }
1781 
1782         const QPinchGesture::ChangeFlags changeFlags = pinch->changeFlags();
1783 
1784         // Zoom
1785         if (pinch->changeFlags() & QPinchGesture::ScaleFactorChanged) {
1786             zoomWithFixedCenter(ZoomRefreshCurrent, mapFromGlobal(pinch->centerPoint().toPoint()), vanillaZoom * pinch->totalScaleFactor());
1787         }
1788 
1789         // Count the number of 90-degree rotations we did since the start of the pinch gesture.
1790         // Otherwise a pinch turned to 90 degrees and held there will rotate the page again and again.
1791         static int rotations = 0;
1792 
1793         if (changeFlags & QPinchGesture::RotationAngleChanged) {
1794             // Rotation angle relative to the accumulated page rotations triggered by the current pinch
1795             // We actually turn at 80 degrees rather than at 90 degrees.  That's less strain on the hands.
1796             const qreal relativeAngle = pinch->rotationAngle() - rotations * 90;
1797             if (relativeAngle > 80) {
1798                 slotRotateClockwise();
1799                 rotations++;
1800             }
1801             if (relativeAngle < -80) {
1802                 slotRotateCounterClockwise();
1803                 rotations--;
1804             }
1805         }
1806 
1807         if (pinch->state() == Qt::GestureFinished || pinch->state() == Qt::GestureCanceled) {
1808             rotations = 0;
1809             d->pinchZoomActive = false;
1810             d->remainingScroll = QPointF(0.0, 0.0);
1811         }
1812 
1813         return true;
1814     }
1815 
1816     return false;
1817 }
1818 
1819 void PageView::paintEvent(QPaintEvent *pe)
1820 {
1821     const QPoint areaPos = contentAreaPosition();
1822     // create the rect into contents from the clipped screen rect
1823     QRect viewportRect = viewport()->rect();
1824     viewportRect.translate(areaPos);
1825     QRect contentsRect = pe->rect().translated(areaPos).intersected(viewportRect);
1826     if (!contentsRect.isValid()) {
1827         return;
1828     }
1829 
1830 #ifdef PAGEVIEW_DEBUG
1831     qCDebug(OkularUiDebug) << "paintevent" << contentsRect;
1832 #endif
1833 
1834     // create the screen painter. a pixel painted at contentsX,contentsY
1835     // appears to the top-left corner of the scrollview.
1836     QPainter screenPainter(viewport());
1837     // translate to simulate the scrolled content widget
1838     screenPainter.translate(-areaPos);
1839 
1840     // selectionRect is the normalized mouse selection rect
1841     QRect selectionRect = d->mouseSelectionRect;
1842     if (!selectionRect.isNull()) {
1843         selectionRect = selectionRect.normalized();
1844     }
1845     // selectionRectInternal without the border
1846     QRect selectionRectInternal = selectionRect;
1847     selectionRectInternal.adjust(1, 1, -1, -1);
1848     // color for blending
1849     QColor selBlendColor = (selectionRect.width() > 8 || selectionRect.height() > 8) ? d->mouseSelectionColor : Qt::red;
1850 
1851     // subdivide region into rects
1852     QRegion rgn = pe->region();
1853     // preprocess rects area to see if it worths or not using subdivision
1854     uint summedArea = 0;
1855     for (const QRect &r : rgn) {
1856         summedArea += r.width() * r.height();
1857     }
1858     // very elementary check: SUMj(Region[j].area) is less than boundingRect.area
1859     const bool useSubdivision = summedArea < (0.6 * contentsRect.width() * contentsRect.height());
1860     if (!useSubdivision) {
1861         rgn = contentsRect;
1862     }
1863 
1864     // iterate over the rects (only one loop if not using subdivision)
1865     for (const QRect &r : rgn) {
1866         if (useSubdivision) {
1867             // set 'contentsRect' to a part of the sub-divided region
1868             contentsRect = r.translated(areaPos).intersected(viewportRect);
1869             if (!contentsRect.isValid()) {
1870                 continue;
1871             }
1872         }
1873 #ifdef PAGEVIEW_DEBUG
1874         qCDebug(OkularUiDebug) << contentsRect;
1875 #endif
1876 
1877         // note: this check will take care of all things requiring alpha blending (not only selection)
1878         bool wantCompositing = !selectionRect.isNull() && contentsRect.intersects(selectionRect);
1879         // also alpha-blend when there is a table selection...
1880         wantCompositing |= !d->tableSelectionParts.isEmpty();
1881 
1882         if (wantCompositing && Okular::Settings::enableCompositing()) {
1883             // create pixmap and open a painter over it (contents{left,top} becomes pixmap {0,0})
1884             QPixmap doubleBuffer(contentsRect.size() * devicePixelRatioF());
1885             doubleBuffer.setDevicePixelRatio(devicePixelRatioF());
1886             QPainter pixmapPainter(&doubleBuffer);
1887 
1888             pixmapPainter.translate(-contentsRect.left(), -contentsRect.top());
1889 
1890             // 1) Layer 0: paint items and clear bg on unpainted rects
1891             drawDocumentOnPainter(contentsRect, &pixmapPainter);
1892             // 2a) Layer 1a: paint (blend) transparent selection (rectangle)
1893             if (!selectionRect.isNull() && selectionRect.intersects(contentsRect) && !selectionRectInternal.contains(contentsRect)) {
1894                 QRect blendRect = selectionRectInternal.intersected(contentsRect);
1895                 // skip rectangles covered by the selection's border
1896                 if (blendRect.isValid()) {
1897                     // grab current pixmap into a new one to colorize contents
1898                     QPixmap blendedPixmap(blendRect.width() * devicePixelRatioF(), blendRect.height() * devicePixelRatioF());
1899                     blendedPixmap.setDevicePixelRatio(devicePixelRatioF());
1900                     QPainter p(&blendedPixmap);
1901 
1902                     p.drawPixmap(0,
1903                                  0,
1904                                  doubleBuffer,
1905                                  (blendRect.left() - contentsRect.left()) * devicePixelRatioF(),
1906                                  (blendRect.top() - contentsRect.top()) * devicePixelRatioF(),
1907                                  blendRect.width() * devicePixelRatioF(),
1908                                  blendRect.height() * devicePixelRatioF());
1909 
1910                     QColor blCol = selBlendColor.darker(140);
1911                     blCol.setAlphaF(0.2);
1912                     p.fillRect(blendedPixmap.rect(), blCol);
1913                     p.end();
1914                     // copy the blended pixmap back to its place
1915                     pixmapPainter.drawPixmap(blendRect.left(), blendRect.top(), blendedPixmap);
1916                 }
1917                 // draw border (red if the selection is too small)
1918                 pixmapPainter.setPen(selBlendColor);
1919                 pixmapPainter.drawRect(selectionRect.adjusted(0, 0, -1, -1));
1920             }
1921             // 2b) Layer 1b: paint (blend) transparent selection (table)
1922             for (const TableSelectionPart &tsp : std::as_const(d->tableSelectionParts)) {
1923                 QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight());
1924                 selectionPartRect.translate(tsp.item->uncroppedGeometry().topLeft());
1925                 QRect selectionPartRectInternal = selectionPartRect;
1926                 selectionPartRectInternal.adjust(1, 1, -1, -1);
1927                 if (!selectionPartRect.isNull() && selectionPartRect.intersects(contentsRect) && !selectionPartRectInternal.contains(contentsRect)) {
1928                     QRect blendRect = selectionPartRectInternal.intersected(contentsRect);
1929                     // skip rectangles covered by the selection's border
1930                     if (blendRect.isValid()) {
1931                         // grab current pixmap into a new one to colorize contents
1932                         QPixmap blendedPixmap(blendRect.width() * devicePixelRatioF(), blendRect.height() * devicePixelRatioF());
1933                         blendedPixmap.setDevicePixelRatio(devicePixelRatioF());
1934                         QPainter p(&blendedPixmap);
1935                         p.drawPixmap(0,
1936                                      0,
1937                                      doubleBuffer,
1938                                      (blendRect.left() - contentsRect.left()) * devicePixelRatioF(),
1939                                      (blendRect.top() - contentsRect.top()) * devicePixelRatioF(),
1940                                      blendRect.width() * devicePixelRatioF(),
1941                                      blendRect.height() * devicePixelRatioF());
1942 
1943                         QColor blCol = d->mouseSelectionColor.darker(140);
1944                         blCol.setAlphaF(0.2);
1945                         p.fillRect(blendedPixmap.rect(), blCol);
1946                         p.end();
1947                         // copy the blended pixmap back to its place
1948                         pixmapPainter.drawPixmap(blendRect.left(), blendRect.top(), blendedPixmap);
1949                     }
1950                     // draw border (red if the selection is too small)
1951                     pixmapPainter.setPen(d->mouseSelectionColor);
1952                     pixmapPainter.drawRect(selectionPartRect.adjusted(0, 0, -1, -1));
1953                 }
1954             }
1955             drawTableDividers(&pixmapPainter);
1956             // 3a) Layer 1: give annotator painting control
1957             if (d->annotator && d->annotator->routePaints(contentsRect)) {
1958                 d->annotator->routePaint(&pixmapPainter, contentsRect);
1959             }
1960             // 3b) Layer 1: give mouseAnnotation painting control
1961             d->mouseAnnotation->routePaint(&pixmapPainter, contentsRect);
1962 
1963             // 4) Layer 2: overlays
1964             if (Okular::Settings::debugDrawBoundaries()) {
1965                 pixmapPainter.setPen(Qt::blue);
1966                 pixmapPainter.drawRect(contentsRect);
1967             }
1968 
1969             // finish painting and draw contents
1970             pixmapPainter.end();
1971             screenPainter.drawPixmap(contentsRect.left(), contentsRect.top(), doubleBuffer);
1972         } else {
1973             // 1) Layer 0: paint items and clear bg on unpainted rects
1974             drawDocumentOnPainter(contentsRect, &screenPainter);
1975             // 2a) Layer 1a: paint opaque selection (rectangle)
1976             if (!selectionRect.isNull() && selectionRect.intersects(contentsRect) && !selectionRectInternal.contains(contentsRect)) {
1977                 screenPainter.setPen(palette().color(QPalette::Active, QPalette::Highlight).darker(110));
1978                 screenPainter.drawRect(selectionRect);
1979             }
1980             // 2b) Layer 1b: paint opaque selection (table)
1981             for (const TableSelectionPart &tsp : std::as_const(d->tableSelectionParts)) {
1982                 QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight());
1983                 selectionPartRect.translate(tsp.item->uncroppedGeometry().topLeft());
1984                 QRect selectionPartRectInternal = selectionPartRect;
1985                 selectionPartRectInternal.adjust(1, 1, -1, -1);
1986                 if (!selectionPartRect.isNull() && selectionPartRect.intersects(contentsRect) && !selectionPartRectInternal.contains(contentsRect)) {
1987                     screenPainter.setPen(palette().color(QPalette::Active, QPalette::Highlight).darker(110));
1988                     screenPainter.drawRect(selectionPartRect);
1989                 }
1990             }
1991             drawTableDividers(&screenPainter);
1992             // 3a) Layer 1: give annotator painting control
1993             if (d->annotator && d->annotator->routePaints(contentsRect)) {
1994                 d->annotator->routePaint(&screenPainter, contentsRect);
1995             }
1996             // 3b) Layer 1: give mouseAnnotation painting control
1997             d->mouseAnnotation->routePaint(&screenPainter, contentsRect);
1998 
1999             // 4) Layer 2: overlays
2000             if (Okular::Settings::debugDrawBoundaries()) {
2001                 screenPainter.setPen(Qt::red);
2002                 screenPainter.drawRect(contentsRect);
2003             }
2004         }
2005     }
2006 }
2007 
2008 void PageView::drawTableDividers(QPainter *screenPainter)
2009 {
2010     if (!d->tableSelectionParts.isEmpty()) {
2011         screenPainter->setPen(d->mouseSelectionColor.darker());
2012         if (d->tableDividersGuessed) {
2013             QPen p = screenPainter->pen();
2014             p.setStyle(Qt::DashLine);
2015             screenPainter->setPen(p);
2016         }
2017         for (const TableSelectionPart &tsp : std::as_const(d->tableSelectionParts)) {
2018             QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight());
2019             selectionPartRect.translate(tsp.item->uncroppedGeometry().topLeft());
2020             QRect selectionPartRectInternal = selectionPartRect;
2021             selectionPartRectInternal.adjust(1, 1, -1, -1);
2022             for (double col : std::as_const(d->tableSelectionCols)) {
2023                 if (col >= tsp.rectInSelection.left && col <= tsp.rectInSelection.right) {
2024                     col = (col - tsp.rectInSelection.left) / (tsp.rectInSelection.right - tsp.rectInSelection.left);
2025                     const int x = selectionPartRect.left() + col * selectionPartRect.width() + 0.5;
2026                     screenPainter->drawLine(x, selectionPartRectInternal.top(), x, selectionPartRectInternal.top() + selectionPartRectInternal.height());
2027                 }
2028             }
2029             for (double row : std::as_const(d->tableSelectionRows)) {
2030                 if (row >= tsp.rectInSelection.top && row <= tsp.rectInSelection.bottom) {
2031                     row = (row - tsp.rectInSelection.top) / (tsp.rectInSelection.bottom - tsp.rectInSelection.top);
2032                     const int y = selectionPartRect.top() + row * selectionPartRect.height() + 0.5;
2033                     screenPainter->drawLine(selectionPartRectInternal.left(), y, selectionPartRectInternal.left() + selectionPartRectInternal.width(), y);
2034                 }
2035             }
2036         }
2037     }
2038 }
2039 
2040 void PageView::resizeEvent(QResizeEvent *e)
2041 {
2042     if (d->items.isEmpty()) {
2043         resizeContentArea(e->size());
2044         return;
2045     }
2046 
2047     if ((d->zoomMode == ZoomFitWidth || d->zoomMode == ZoomFitAuto) && !verticalScrollBar()->isVisible() && qAbs(e->oldSize().height() - e->size().height()) < verticalScrollBar()->width() && d->verticalScrollBarVisible) {
2048         // this saves us from infinite resizing loop because of scrollbars appearing and disappearing
2049         // see bug 160628 for more info
2050         // TODO looks are still a bit ugly because things are left uncentered
2051         // but better a bit ugly than unusable
2052         d->verticalScrollBarVisible = false;
2053         resizeContentArea(e->size());
2054         return;
2055     } else if (d->zoomMode == ZoomFitAuto && !horizontalScrollBar()->isVisible() && qAbs(e->oldSize().width() - e->size().width()) < horizontalScrollBar()->height() && d->horizontalScrollBarVisible) {
2056         // this saves us from infinite resizing loop because of scrollbars appearing and disappearing
2057         // TODO looks are still a bit ugly because things are left uncentered
2058         // but better a bit ugly than unusable
2059         d->horizontalScrollBarVisible = false;
2060         resizeContentArea(e->size());
2061         return;
2062     }
2063 
2064     if (d->pinchZoomActive) {
2065         // if we make a continuous zooming with pinch gesture or mouse, we call delayedResizeEvent() directly.
2066         delayedResizeEvent();
2067     } else {
2068         // start a timer that will refresh the pixmap after 0.2s
2069         d->delayResizeEventTimer->start(200);
2070     }
2071 
2072     d->verticalScrollBarVisible = verticalScrollBar()->isVisible();
2073     d->horizontalScrollBarVisible = horizontalScrollBar()->isVisible();
2074 }
2075 
2076 void PageView::keyPressEvent(QKeyEvent *e)
2077 {
2078     // Ignore ESC key press to send to shell.cpp
2079     if (e->key() != Qt::Key_Escape) {
2080         e->accept();
2081     } else {
2082         e->ignore();
2083     }
2084 
2085     // if performing a selection or dyn zooming, disable keys handling
2086     if ((d->mouseSelecting && e->key() != Qt::Key_Escape) || (QApplication::mouseButtons() & Qt::MiddleButton)) {
2087         return;
2088     }
2089 
2090     // move/scroll page by using keys
2091     switch (e->key()) {
2092     case Qt::Key_J:
2093     case Qt::Key_Down:
2094         slotScrollDown(1 /* go down 1 step */);
2095         break;
2096 
2097     case Qt::Key_PageDown:
2098         slotScrollDown();
2099         break;
2100 
2101     case Qt::Key_K:
2102     case Qt::Key_Up:
2103         slotScrollUp(1 /* go up 1 step */);
2104         break;
2105 
2106     case Qt::Key_PageUp:
2107     case Qt::Key_Backspace:
2108         slotScrollUp();
2109         break;
2110 
2111     case Qt::Key_Left:
2112     case Qt::Key_H:
2113         if (horizontalScrollBar()->maximum() == 0) {
2114             // if we cannot scroll we go to the previous page vertically
2115             int next_page = d->document->currentPage() - viewColumns();
2116             d->document->setViewportPage(next_page);
2117         } else {
2118             d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(-horizontalScrollBar()->singleStep(), 0), d->currentShortScrollDuration);
2119         }
2120         break;
2121     case Qt::Key_Right:
2122     case Qt::Key_L:
2123         if (horizontalScrollBar()->maximum() == 0) {
2124             // if we cannot scroll we advance the page vertically
2125             int next_page = d->document->currentPage() + viewColumns();
2126             d->document->setViewportPage(next_page);
2127         } else {
2128             d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(horizontalScrollBar()->singleStep(), 0), d->currentShortScrollDuration);
2129         }
2130         break;
2131     case Qt::Key_Escape:
2132         Q_EMIT escPressed();
2133         selectionClear(d->tableDividersGuessed ? ClearOnlyDividers : ClearAllSelection);
2134         d->mousePressPos = QPointF();
2135         if (d->aPrevAction) {
2136             d->aPrevAction->trigger();
2137             d->aPrevAction = nullptr;
2138         }
2139         d->mouseAnnotation->routeKeyPressEvent(e);
2140         break;
2141     case Qt::Key_Delete:
2142         d->mouseAnnotation->routeKeyPressEvent(e);
2143         break;
2144     case Qt::Key_Shift:
2145     case Qt::Key_Control:
2146         if (d->autoScrollTimer) {
2147             if (d->autoScrollTimer->isActive()) {
2148                 d->autoScrollTimer->stop();
2149             } else {
2150                 slotAutoScroll();
2151             }
2152             return;
2153         }
2154         // fallthrough
2155     default:
2156         e->ignore();
2157         return;
2158     }
2159     // if a known key has been pressed, stop scrolling the page
2160     if (d->autoScrollTimer) {
2161         d->scrollIncrement = 0;
2162         d->autoScrollTimer->stop();
2163     }
2164 }
2165 
2166 void PageView::keyReleaseEvent(QKeyEvent *e)
2167 {
2168     e->accept();
2169 
2170     if (d->annotator && d->annotator->active()) {
2171         if (d->annotator->routeKeyEvent(e)) {
2172             return;
2173         }
2174     }
2175 
2176     if (e->key() == Qt::Key_Escape && d->autoScrollTimer) {
2177         d->scrollIncrement = 0;
2178         d->autoScrollTimer->stop();
2179     }
2180 
2181     if (e->key() == Qt::Key_Control) {
2182         continuousZoomEnd();
2183     }
2184 }
2185 
2186 void PageView::inputMethodEvent(QInputMethodEvent *e)
2187 {
2188     Q_UNUSED(e)
2189 }
2190 
2191 void PageView::tabletEvent(QTabletEvent *e)
2192 {
2193     // Ignore tablet events that we don't care about
2194     if (!(e->type() == QEvent::TabletPress || e->type() == QEvent::TabletRelease || e->type() == QEvent::TabletMove)) {
2195         e->ignore();
2196         return;
2197     }
2198 
2199     // Determine pen state
2200     bool penReleased = false;
2201     if (e->type() == QEvent::TabletPress) {
2202         d->penDown = true;
2203     }
2204     if (e->type() == QEvent::TabletRelease) {
2205         d->penDown = false;
2206         penReleased = true;
2207     }
2208 
2209     // If we're editing an annotation and the tablet pen is either down or just released
2210     // then dispatch event to annotator
2211     if (d->annotator && d->annotator->active() && (d->penDown || penReleased)) {
2212         // accept the event, otherwise it comes back as a mouse event
2213         e->accept();
2214 
2215         const QPointF eventPos = contentAreaPoint(e->position());
2216         PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2217         const QPoint localOriginInGlobal = mapToGlobal(QPoint(0, 0));
2218 
2219         // routeTabletEvent will accept or ignore event as appropriate
2220         d->annotator->routeTabletEvent(e, pageItem, localOriginInGlobal);
2221     } else {
2222         e->ignore();
2223     }
2224 }
2225 
2226 void PageView::continuousZoom(double delta)
2227 {
2228     if (delta) {
2229         d->zoomFactor *= (1.0 + (delta / 500.0));
2230         d->blockPixmapsRequest = true;
2231         updateZoom(ZoomRefreshCurrent);
2232         d->blockPixmapsRequest = false;
2233         viewport()->update();
2234     }
2235 }
2236 
2237 void PageView::continuousZoomEnd()
2238 {
2239     // request pixmaps since it was disabled during drag
2240     slotRequestVisiblePixmaps();
2241 
2242     // the cursor may now be over a link.. update it
2243     updateCursor();
2244 }
2245 
2246 void PageView::mouseMoveEvent(QMouseEvent *e)
2247 {
2248     d->previousMouseMovePos = e->globalPosition();
2249 
2250     // don't perform any mouse action when no document is shown
2251     if (d->items.isEmpty()) {
2252         return;
2253     }
2254 
2255     // if holding mouse mid button, perform zoom
2256     if (e->buttons() & Qt::MiddleButton) {
2257         int deltaY = d->mouseMidLastY - e->globalPosition().y();
2258         d->mouseMidLastY = e->globalPosition().y();
2259 
2260         const float upperZoomLimit = d->document->supportsTiles() ? 99.99 : 3.99;
2261 
2262         // Wrap mouse cursor
2263         if (Okular::Settings::dragBeyondScreenEdges()) {
2264             Qt::Edges wrapEdges;
2265             wrapEdges.setFlag(Qt::TopEdge, d->zoomFactor < upperZoomLimit);
2266             wrapEdges.setFlag(Qt::BottomEdge, d->zoomFactor > 0.101);
2267 
2268             deltaY += CursorWrapHelper::wrapCursor(e->globalPosition().toPoint(), wrapEdges).y();
2269         }
2270 
2271         // update zoom level, perform zoom and redraw
2272         continuousZoom(deltaY);
2273         return;
2274     }
2275 
2276     const QPoint eventPos = contentAreaPoint(e->pos());
2277 
2278     // if we're editing an annotation, dispatch event to it
2279     if (d->annotator && d->annotator->active()) {
2280         PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2281         updateCursor(eventPos);
2282         d->annotator->routeMouseEvent(e, pageItem);
2283         return;
2284     }
2285 
2286     bool leftButton = (e->buttons() == Qt::LeftButton);
2287     bool rightButton = (e->buttons() == Qt::RightButton);
2288 
2289     switch (d->mouseMode) {
2290     case Okular::Settings::EnumMouseMode::Browse: {
2291         PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2292         if (leftButton) {
2293             d->leftClickTimer.stop();
2294             if (d->mouseAnnotation->isActive()) {
2295                 // if left button pressed and annotation is focused, forward move event
2296                 d->mouseAnnotation->routeMouseMoveEvent(pageItem, eventPos, leftButton);
2297             }
2298             // drag page
2299             else {
2300                 if (d->scroller->state() == QScroller::Inactive || d->scroller->state() == QScroller::Scrolling) {
2301                     d->mouseGrabOffset = QPoint(0, 0);
2302 
2303                     if (!d->pinchZoomActive) {
2304                         d->scroller->handleInput(QScroller::InputPress, e->pos(), e->timestamp() - 1);
2305                     }
2306                 }
2307 
2308                 setCursor(Qt::ClosedHandCursor);
2309 
2310                 // Wrap mouse cursor
2311                 if (Okular::Settings::dragBeyondScreenEdges()) {
2312                     Qt::Edges wrapEdges;
2313                     wrapEdges.setFlag(Qt::TopEdge, verticalScrollBar()->value() < verticalScrollBar()->maximum());
2314                     wrapEdges.setFlag(Qt::BottomEdge, verticalScrollBar()->value() > verticalScrollBar()->minimum());
2315                     wrapEdges.setFlag(Qt::LeftEdge, horizontalScrollBar()->value() < horizontalScrollBar()->maximum());
2316                     wrapEdges.setFlag(Qt::RightEdge, horizontalScrollBar()->value() > horizontalScrollBar()->minimum());
2317 
2318                     d->mouseGrabOffset -= CursorWrapHelper::wrapCursor(e->pos(), wrapEdges);
2319                 }
2320 
2321                 if (!d->pinchZoomActive) {
2322                     d->scroller->handleInput(QScroller::InputMove, e->pos() + d->mouseGrabOffset, e->timestamp());
2323                 }
2324             }
2325         } else if (rightButton && !d->mousePressPos.isNull() && d->aMouseSelect) {
2326             // if mouse moves 5 px away from the press point, switch to 'selection'
2327             qreal deltaX = d->mousePressPos.x() - e->globalPosition().x(), deltaY = d->mousePressPos.y() - e->globalPosition().y();
2328             if (deltaX > 5 || deltaX < -5 || deltaY > 5 || deltaY < -5) {
2329                 d->aPrevAction = d->aMouseNormal;
2330                 d->aMouseSelect->trigger();
2331                 QPoint newPos = eventPos + QPoint(deltaX, deltaY);
2332                 selectionStart(newPos, palette().color(QPalette::Active, QPalette::Highlight).lighter(120), false);
2333                 updateSelection(eventPos);
2334                 break;
2335             }
2336         } else {
2337             /* Forward move events which are still not yet consumed by "mouse grab" or aMouseSelect */
2338             d->mouseAnnotation->routeMouseMoveEvent(pageItem, eventPos, leftButton);
2339             updateCursor();
2340         }
2341     } break;
2342 
2343     case Okular::Settings::EnumMouseMode::Zoom:
2344     case Okular::Settings::EnumMouseMode::RectSelect:
2345     case Okular::Settings::EnumMouseMode::TableSelect:
2346     case Okular::Settings::EnumMouseMode::TrimSelect:
2347         // set second corner of selection
2348         if (d->mouseSelecting) {
2349             updateSelection(eventPos);
2350             d->mouseOverLinkObject = nullptr;
2351         }
2352         updateCursor();
2353         break;
2354 
2355     case Okular::Settings::EnumMouseMode::Magnifier:
2356         if (e->buttons()) // if any button is pressed at all
2357         {
2358             moveMagnifier(e->pos());
2359             updateMagnifier(eventPos);
2360         }
2361         break;
2362 
2363     case Okular::Settings::EnumMouseMode::TextSelect:
2364         // if mouse moves 5 px away from the press point and the document supports text extraction, do 'textselection'
2365         if (!d->mouseTextSelecting && !d->mousePressPos.isNull() && d->document->supportsSearching() && ((eventPos - d->mouseSelectPos).manhattanLength() > 5)) {
2366             d->mouseTextSelecting = true;
2367         }
2368         updateSelection(eventPos);
2369         updateCursor();
2370         break;
2371     }
2372 }
2373 
2374 void PageView::mousePressEvent(QMouseEvent *e)
2375 {
2376     // don't perform any mouse action when no document is shown
2377     if (d->items.isEmpty()) {
2378         return;
2379     }
2380 
2381     // if performing a selection or dyn zooming, disable mouse press
2382     if (d->mouseSelecting || (e->button() != Qt::MiddleButton && (e->buttons() & Qt::MiddleButton))) {
2383         return;
2384     }
2385 
2386     // if the page is scrolling, stop it
2387     if (d->autoScrollTimer) {
2388         d->scrollIncrement = 0;
2389         d->autoScrollTimer->stop();
2390     }
2391 
2392     // if pressing mid mouse button while not doing other things, begin 'continuous zoom' mode
2393     if (e->button() == Qt::MiddleButton) {
2394         d->mouseMidLastY = e->globalPosition().y();
2395         setCursor(Qt::SizeVerCursor);
2396         CursorWrapHelper::startDrag();
2397         return;
2398     }
2399 
2400     const QPoint eventPos = contentAreaPoint(e->pos());
2401 
2402     // if we're editing an annotation, dispatch event to it
2403     if (d->annotator && d->annotator->active()) {
2404         d->scroller->stop();
2405         PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2406         d->annotator->routeMouseEvent(e, pageItem);
2407         return;
2408     }
2409 
2410     // trigger history navigation for additional mouse buttons
2411     if (e->button() == Qt::XButton1) {
2412         Q_EMIT mouseBackButtonClick();
2413         return;
2414     }
2415     if (e->button() == Qt::XButton2) {
2416         Q_EMIT mouseForwardButtonClick();
2417         return;
2418     }
2419 
2420     // update press / 'start drag' mouse position
2421     d->mousePressPos = e->globalPosition();
2422     CursorWrapHelper::startDrag();
2423 
2424     // handle mode dependent mouse press actions
2425     bool leftButton = e->button() == Qt::LeftButton, rightButton = e->button() == Qt::RightButton;
2426 
2427     //   Not sure we should erase the selection when clicking with left.
2428     if (d->mouseMode != Okular::Settings::EnumMouseMode::TextSelect) {
2429         textSelectionClear();
2430     }
2431 
2432     switch (d->mouseMode) {
2433     case Okular::Settings::EnumMouseMode::Browse: // drag start / click / link following
2434     {
2435         PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2436         if (leftButton) {
2437             if (pageItem) {
2438                 d->mouseAnnotation->routeMousePressEvent(pageItem, eventPos);
2439             }
2440 
2441             if (!d->mouseOnRect) {
2442                 d->mouseGrabOffset = QPoint(0, 0);
2443                 if (!d->pinchZoomActive) {
2444                     d->scroller->handleInput(QScroller::InputPress, e->pos(), e->timestamp());
2445                 }
2446                 d->leftClickTimer.start(QApplication::doubleClickInterval() + 10);
2447             }
2448         }
2449     } break;
2450 
2451     case Okular::Settings::EnumMouseMode::Zoom: // set first corner of the zoom rect
2452         if (leftButton) {
2453             selectionStart(eventPos, palette().color(QPalette::Active, QPalette::Highlight), false);
2454         } else if (rightButton) {
2455             updateZoom(ZoomOut);
2456         }
2457         break;
2458 
2459     case Okular::Settings::EnumMouseMode::Magnifier:
2460         moveMagnifier(e->pos());
2461         d->magnifierView->show();
2462         updateMagnifier(eventPos);
2463         break;
2464 
2465     case Okular::Settings::EnumMouseMode::RectSelect: // set first corner of the selection rect
2466     case Okular::Settings::EnumMouseMode::TrimSelect:
2467         if (leftButton) {
2468             selectionStart(eventPos, palette().color(QPalette::Active, QPalette::Highlight).lighter(120), false);
2469         }
2470         break;
2471     case Okular::Settings::EnumMouseMode::TableSelect:
2472         if (leftButton) {
2473             if (d->tableSelectionParts.isEmpty()) {
2474                 selectionStart(eventPos, palette().color(QPalette::Active, QPalette::Highlight).lighter(120), false);
2475             } else {
2476                 QRect updatedRect;
2477                 for (const TableSelectionPart &tsp : std::as_const(d->tableSelectionParts)) {
2478                     QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight());
2479                     selectionPartRect.translate(tsp.item->uncroppedGeometry().topLeft());
2480 
2481                     // This will update the whole table rather than just the added/removed divider
2482                     // (which can span more than one part).
2483                     updatedRect = updatedRect.united(selectionPartRect);
2484 
2485                     if (!selectionPartRect.contains(eventPos)) {
2486                         continue;
2487                     }
2488 
2489                     // At this point it's clear we're either adding or removing a divider manually, so obviously the user is happy with the guess (if any).
2490                     d->tableDividersGuessed = false;
2491 
2492                     // There's probably a neat trick to finding which edge it's closest to,
2493                     // but this way has the advantage of simplicity.
2494                     const int fromLeft = abs(selectionPartRect.left() - eventPos.x());
2495                     const int fromRight = abs(selectionPartRect.left() + selectionPartRect.width() - eventPos.x());
2496                     const int fromTop = abs(selectionPartRect.top() - eventPos.y());
2497                     const int fromBottom = abs(selectionPartRect.top() + selectionPartRect.height() - eventPos.y());
2498                     const int colScore = fromTop < fromBottom ? fromTop : fromBottom;
2499                     const int rowScore = fromLeft < fromRight ? fromLeft : fromRight;
2500 
2501                     if (colScore < rowScore) {
2502                         bool deleted = false;
2503                         for (int i = 0; i < d->tableSelectionCols.length(); i++) {
2504                             const double col = (d->tableSelectionCols[i] - tsp.rectInSelection.left) / (tsp.rectInSelection.right - tsp.rectInSelection.left);
2505                             const int colX = selectionPartRect.left() + col * selectionPartRect.width() + 0.5;
2506                             if (abs(colX - eventPos.x()) <= 3) {
2507                                 d->tableSelectionCols.removeAt(i);
2508                                 deleted = true;
2509 
2510                                 break;
2511                             }
2512                         }
2513                         if (!deleted) {
2514                             double col = eventPos.x() - selectionPartRect.left();
2515                             col /= selectionPartRect.width(); // at this point, it's normalised within the part
2516                             col *= (tsp.rectInSelection.right - tsp.rectInSelection.left);
2517                             col += tsp.rectInSelection.left; // at this point, it's normalised within the whole table
2518 
2519                             d->tableSelectionCols.append(col);
2520                             std::sort(d->tableSelectionCols.begin(), d->tableSelectionCols.end());
2521                         }
2522                     } else {
2523                         bool deleted = false;
2524                         for (int i = 0; i < d->tableSelectionRows.length(); i++) {
2525                             const double row = (d->tableSelectionRows[i] - tsp.rectInSelection.top) / (tsp.rectInSelection.bottom - tsp.rectInSelection.top);
2526                             const int rowY = selectionPartRect.top() + row * selectionPartRect.height() + 0.5;
2527                             if (abs(rowY - eventPos.y()) <= 3) {
2528                                 d->tableSelectionRows.removeAt(i);
2529                                 deleted = true;
2530 
2531                                 break;
2532                             }
2533                         }
2534                         if (!deleted) {
2535                             double row = eventPos.y() - selectionPartRect.top();
2536                             row /= selectionPartRect.height(); // at this point, it's normalised within the part
2537                             row *= (tsp.rectInSelection.bottom - tsp.rectInSelection.top);
2538                             row += tsp.rectInSelection.top; // at this point, it's normalised within the whole table
2539 
2540                             d->tableSelectionRows.append(row);
2541                             std::sort(d->tableSelectionRows.begin(), d->tableSelectionRows.end());
2542                         }
2543                     }
2544                 }
2545                 updatedRect.translate(-contentAreaPosition());
2546                 viewport()->update(updatedRect);
2547             }
2548         } else if (rightButton && !d->tableSelectionParts.isEmpty()) {
2549             QMenu menu(this);
2550             QAction *copyToClipboard = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy Table Contents to Clipboard"));
2551             const bool copyAllowed = d->document->isAllowed(Okular::AllowCopy);
2552 
2553             if (!copyAllowed) {
2554                 copyToClipboard->setEnabled(false);
2555                 copyToClipboard->setText(i18n("Copy forbidden by DRM"));
2556             }
2557 
2558             QAction *choice = menu.exec(e->globalPosition().toPoint());
2559             if (choice == copyToClipboard) {
2560                 copyTextSelection();
2561             }
2562         }
2563         break;
2564     case Okular::Settings::EnumMouseMode::TextSelect:
2565         d->mouseSelectPos = eventPos;
2566         if (!rightButton) {
2567             textSelectionClear();
2568         }
2569         break;
2570     }
2571 }
2572 
2573 void PageView::mouseReleaseEvent(QMouseEvent *e)
2574 {
2575     // stop the drag scrolling
2576     d->dragScrollTimer.stop();
2577 
2578     d->leftClickTimer.stop();
2579 
2580     const bool leftButton = e->button() == Qt::LeftButton;
2581     const bool rightButton = e->button() == Qt::RightButton;
2582 
2583     if (d->mouseAnnotation->isActive() && leftButton) {
2584         // Just finished to move the annotation
2585         d->mouseAnnotation->routeMouseReleaseEvent();
2586     }
2587 
2588     // don't perform any mouse action when no document is shown..
2589     if (d->items.isEmpty()) {
2590         // ..except for right Clicks (emitted even it viewport is empty)
2591         if (e->button() == Qt::RightButton) {
2592             Q_EMIT rightClick(nullptr, e->globalPosition().toPoint());
2593         }
2594         return;
2595     }
2596 
2597     const QPoint eventPos = contentAreaPoint(e->pos());
2598 
2599     // handle mode independent mid bottom zoom
2600     if (e->button() == Qt::MiddleButton) {
2601         continuousZoomEnd();
2602         return;
2603     }
2604 
2605     // if we're editing an annotation, dispatch event to it
2606     if (d->annotator && d->annotator->active()) {
2607         PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2608         d->annotator->routeMouseEvent(e, pageItem);
2609         return;
2610     }
2611 
2612     switch (d->mouseMode) {
2613     case Okular::Settings::EnumMouseMode::Browse: {
2614         if (!d->pinchZoomActive) {
2615             d->scroller->handleInput(QScroller::InputRelease, e->pos() + d->mouseGrabOffset, e->timestamp());
2616         }
2617 
2618         // return the cursor to its normal state after dragging
2619         if (cursor().shape() == Qt::ClosedHandCursor) {
2620             updateCursor(eventPos);
2621         }
2622 
2623         PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2624         const QPointF pressPos = contentAreaPoint(mapFromGlobal(d->mousePressPos));
2625         const PageViewItem *pageItemPressPos = pickItemOnPoint(pressPos.x(), pressPos.y());
2626 
2627         // if the mouse has not moved since the press, that's a -click-
2628         if (leftButton && pageItem && pageItem == pageItemPressPos && ((d->mousePressPos - e->globalPosition()).manhattanLength() < QApplication::startDragDistance())) {
2629             if (!mouseReleaseOverLink(d->mouseOverLinkObject) && (e->modifiers() == Qt::ShiftModifier)) {
2630                 const double nX = pageItem->absToPageX(eventPos.x());
2631                 const double nY = pageItem->absToPageY(eventPos.y());
2632                 const Okular::ObjectRect *rect;
2633                 // TODO: find a better way to activate the source reference "links"
2634                 // for the moment they are activated with Shift + left click
2635                 // Search the nearest source reference.
2636                 rect = pageItem->page()->objectRect(Okular::ObjectRect::SourceRef, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight());
2637                 if (!rect) {
2638                     static const double s_minDistance = 0.025; // FIXME?: empirical value?
2639                     double distance = 0.0;
2640                     rect = pageItem->page()->nearestObjectRect(Okular::ObjectRect::SourceRef, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight(), &distance);
2641                     // distance is distanceSqr, adapt it to a normalized value
2642                     distance = distance / (pow(pageItem->uncroppedWidth(), 2) + pow(pageItem->uncroppedHeight(), 2));
2643                     if (rect && (distance > s_minDistance)) {
2644                         rect = nullptr;
2645                     }
2646                 }
2647                 if (rect) {
2648                     const Okular::SourceReference *ref = static_cast<const Okular::SourceReference *>(rect->object());
2649                     d->document->processSourceReference(ref);
2650                 } else {
2651                     const Okular::SourceReference *ref = d->document->dynamicSourceReference(pageItem->pageNumber(), nX * pageItem->page()->width(), nY * pageItem->page()->height());
2652                     if (ref) {
2653                         d->document->processSourceReference(ref);
2654                         delete ref;
2655                     }
2656                 }
2657             }
2658         } else if (rightButton && !d->mouseAnnotation->isModified()) {
2659             if (pageItem && pageItem == pageItemPressPos && ((d->mousePressPos - e->globalPosition()).manhattanLength() < QApplication::startDragDistance())) {
2660                 QMenu *menu = createProcessLinkMenu(pageItem, eventPos);
2661 
2662                 const QRect &itemRect = pageItem->uncroppedGeometry();
2663                 const double nX = pageItem->absToPageX(eventPos.x());
2664                 const double nY = pageItem->absToPageY(eventPos.y());
2665 
2666                 const QList<const Okular::ObjectRect *> annotRects = pageItem->page()->objectRects(Okular::ObjectRect::OAnnotation, nX, nY, itemRect.width(), itemRect.height());
2667 
2668                 AnnotationPopup annotPopup(d->document, AnnotationPopup::MultiAnnotationMode, this);
2669                 // Do not move annotPopup inside the if, it needs to live until menu->exec()
2670                 if (!annotRects.isEmpty()) {
2671                     for (const Okular::ObjectRect *annotRect : annotRects) {
2672                         Okular::Annotation *ann = ((Okular::AnnotationObjectRect *)annotRect)->annotation();
2673                         if (ann && (ann->subType() != Okular::Annotation::AWidget)) {
2674                             annotPopup.addAnnotation(ann, pageItem->pageNumber());
2675                         }
2676                     }
2677 
2678                     connect(&annotPopup, &AnnotationPopup::openAnnotationWindow, this, &PageView::openAnnotationWindow);
2679 
2680                     if (!menu) {
2681                         menu = new QMenu(this);
2682                     }
2683                     annotPopup.addActionsToMenu(menu);
2684                 }
2685 
2686                 if (menu) {
2687                     menu->exec(e->globalPosition().toPoint());
2688                     menu->deleteLater();
2689                 } else {
2690                     // a link can move us to another page or even to another document, there's no point in trying to
2691                     //  process the click on the image once we have processes the click on the link
2692                     const Okular::ObjectRect *rect = pageItem->page()->objectRect(Okular::ObjectRect::Image, nX, nY, itemRect.width(), itemRect.height());
2693                     if (rect) {
2694                         // handle right click over a image
2695                     } else {
2696                         // right click (if not within 5 px of the press point, the mode
2697                         // had been already changed to 'Selection' instead of 'Normal')
2698                         Q_EMIT rightClick(pageItem->page(), e->globalPosition().toPoint());
2699                     }
2700                 }
2701             } else {
2702                 // right click (if not within 5 px of the press point, the mode
2703                 // had been already changed to 'Selection' instead of 'Normal')
2704                 Q_EMIT rightClick(pageItem ? pageItem->page() : nullptr, e->globalPosition().toPoint());
2705             }
2706         }
2707     } break;
2708 
2709     case Okular::Settings::EnumMouseMode::Zoom:
2710         // if a selection rect has been defined, zoom into it
2711         if (leftButton && d->mouseSelecting) {
2712             QRect selRect = d->mouseSelectionRect.normalized();
2713             if (selRect.width() <= 8 && selRect.height() <= 8) {
2714                 selectionClear();
2715                 break;
2716             }
2717 
2718             // find out new zoom ratio and normalized view center (relative to the contentsRect)
2719             double zoom = qMin((double)viewport()->width() / (double)selRect.width(), (double)viewport()->height() / (double)selRect.height());
2720             double nX = (double)(selRect.left() + selRect.right()) / (2.0 * (double)contentAreaWidth());
2721             double nY = (double)(selRect.top() + selRect.bottom()) / (2.0 * (double)contentAreaHeight());
2722 
2723             const float upperZoomLimit = d->document->supportsTiles() ? 100.0 : 4.0;
2724             if (d->zoomFactor <= upperZoomLimit || zoom <= 1.0) {
2725                 d->zoomFactor *= zoom;
2726                 viewport()->setUpdatesEnabled(false);
2727                 updateZoom(ZoomRefreshCurrent);
2728                 viewport()->setUpdatesEnabled(true);
2729             }
2730 
2731             // recenter view and update the viewport
2732             center((int)(nX * contentAreaWidth()), (int)(nY * contentAreaHeight()));
2733             viewport()->update();
2734 
2735             // hide message box and delete overlay window
2736             selectionClear();
2737         }
2738         break;
2739 
2740     case Okular::Settings::EnumMouseMode::Magnifier:
2741         d->magnifierView->hide();
2742         break;
2743 
2744     case Okular::Settings::EnumMouseMode::TrimSelect: {
2745         // if it is a left release checks if is over a previous link press
2746         if (leftButton && mouseReleaseOverLink(d->mouseOverLinkObject)) {
2747             selectionClear();
2748             break;
2749         }
2750 
2751         // if mouse is released and selection is null this is a rightClick
2752         if (rightButton && !d->mouseSelecting) {
2753             break;
2754         }
2755         PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2756         // ensure end point rests within a page, or ignore
2757         if (!pageItem) {
2758             break;
2759         }
2760         QRect selectionRect = d->mouseSelectionRect.normalized();
2761 
2762         double nLeft = pageItem->absToPageX(selectionRect.left());
2763         double nRight = pageItem->absToPageX(selectionRect.right());
2764         double nTop = pageItem->absToPageY(selectionRect.top());
2765         double nBottom = pageItem->absToPageY(selectionRect.bottom());
2766         if (nLeft < 0) {
2767             nLeft = 0;
2768         }
2769         if (nTop < 0) {
2770             nTop = 0;
2771         }
2772         if (nRight > 1) {
2773             nRight = 1;
2774         }
2775         if (nBottom > 1) {
2776             nBottom = 1;
2777         }
2778         d->trimBoundingBox = Okular::NormalizedRect(nLeft, nTop, nRight, nBottom);
2779 
2780         // Trim Selection successfully done, hide prompt
2781         d->messageWindow->hide();
2782 
2783         // clear widget selection and invalidate rect
2784         selectionClear();
2785 
2786         // When Trim selection bbox interaction is over, we should switch to another mousemode.
2787         if (d->aPrevAction) {
2788             d->aPrevAction->trigger();
2789             d->aPrevAction = nullptr;
2790         } else {
2791             d->aMouseNormal->trigger();
2792         }
2793 
2794         // with d->trimBoundingBox defined, redraw for trim to take visual effect
2795         if (d->document->pages() > 0) {
2796             slotRelayoutPages();
2797             slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already!
2798         }
2799 
2800         break;
2801     }
2802     case Okular::Settings::EnumMouseMode::RectSelect: {
2803         // if it is a left release checks if is over a previous link press
2804         if (leftButton && mouseReleaseOverLink(d->mouseOverLinkObject)) {
2805             selectionClear();
2806             break;
2807         }
2808 
2809         // if mouse is released and selection is null this is a rightClick
2810         if (rightButton && !d->mouseSelecting) {
2811             PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2812             Q_EMIT rightClick(pageItem ? pageItem->page() : nullptr, e->globalPosition().toPoint());
2813             break;
2814         }
2815 
2816         // if a selection is defined, display a popup
2817         if ((!leftButton && !d->aPrevAction) || (!rightButton && d->aPrevAction) || !d->mouseSelecting) {
2818             break;
2819         }
2820 
2821         QRect selectionRect = d->mouseSelectionRect.normalized();
2822         if (selectionRect.width() <= 8 && selectionRect.height() <= 8) {
2823             selectionClear();
2824             if (d->aPrevAction) {
2825                 d->aPrevAction->trigger();
2826                 d->aPrevAction = nullptr;
2827             }
2828             break;
2829         }
2830 
2831         // if we support text generation
2832         QString selectedText;
2833         if (d->document->supportsSearching()) {
2834             // grab text in selection by extracting it from all intersected pages
2835             const Okular::Page *okularPage = nullptr;
2836             for (const PageViewItem *item : std::as_const(d->items)) {
2837                 if (!item->isVisible()) {
2838                     continue;
2839                 }
2840 
2841                 const QRect &itemRect = item->croppedGeometry();
2842                 if (selectionRect.intersects(itemRect)) {
2843                     // request the textpage if there isn't one
2844                     okularPage = item->page();
2845                     qCDebug(OkularUiDebug) << "checking if page" << item->pageNumber() << "has text:" << okularPage->hasTextPage();
2846                     if (!okularPage->hasTextPage()) {
2847                         d->document->requestTextPage(okularPage->number());
2848                     }
2849                     // grab text in the rect that intersects itemRect
2850                     QRect relativeRect = selectionRect.intersected(itemRect);
2851                     relativeRect.translate(-item->uncroppedGeometry().topLeft());
2852                     Okular::RegularAreaRect rects;
2853                     rects.append(Okular::NormalizedRect(relativeRect, item->uncroppedWidth(), item->uncroppedHeight()));
2854                     selectedText += okularPage->text(&rects);
2855                 }
2856             }
2857         }
2858 
2859         // popup that ask to copy:text and copy/save:image
2860         QMenu menu(this);
2861         menu.setObjectName(QStringLiteral("PopupMenu"));
2862         QAction *textToClipboard = nullptr;
2863 #if HAVE_SPEECH
2864         QAction *speakText = nullptr;
2865 #endif
2866         QAction *imageToClipboard = nullptr;
2867         QAction *imageToFile = nullptr;
2868         if (d->document->supportsSearching() && !selectedText.isEmpty()) {
2869             menu.addAction(new OKMenuTitle(&menu, i18np("Text (1 character)", "Text (%1 characters)", selectedText.length())));
2870             textToClipboard = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy to Clipboard"));
2871             textToClipboard->setObjectName(QStringLiteral("CopyTextToClipboard"));
2872             bool copyAllowed = d->document->isAllowed(Okular::AllowCopy);
2873             if (!copyAllowed) {
2874                 textToClipboard->setEnabled(false);
2875                 textToClipboard->setText(i18n("Copy forbidden by DRM"));
2876             }
2877 #if HAVE_SPEECH
2878             if (Okular::Settings::useTTS()) {
2879                 speakText = menu.addAction(QIcon::fromTheme(QStringLiteral("text-speak")), i18n("Speak Text"));
2880             }
2881 #endif
2882             if (copyAllowed) {
2883                 addSearchWithinDocumentAction(&menu, selectedText);
2884                 addWebShortcutsMenu(&menu, selectedText);
2885             }
2886         }
2887         menu.addAction(new OKMenuTitle(&menu, i18n("Image (%1 by %2 pixels)", selectionRect.width(), selectionRect.height())));
2888         imageToClipboard = menu.addAction(QIcon::fromTheme(QStringLiteral("image-x-generic")), i18n("Copy to Clipboard"));
2889         imageToFile = menu.addAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save to File..."));
2890         QAction *choice = menu.exec(e->globalPosition().toPoint());
2891         // check if the user really selected an action
2892         if (choice) {
2893             // IMAGE operation chosen
2894             if (choice == imageToClipboard || choice == imageToFile) {
2895                 // renders page into a pixmap
2896                 QPixmap copyPix(selectionRect.width(), selectionRect.height());
2897                 QPainter copyPainter(&copyPix);
2898                 copyPainter.translate(-selectionRect.left(), -selectionRect.top());
2899                 drawDocumentOnPainter(selectionRect, &copyPainter);
2900                 copyPainter.end();
2901 
2902                 if (choice == imageToClipboard) {
2903                     // [2] copy pixmap to clipboard
2904                     QClipboard *cb = QApplication::clipboard();
2905                     cb->setPixmap(copyPix, QClipboard::Clipboard);
2906                     if (cb->supportsSelection()) {
2907                         cb->setPixmap(copyPix, QClipboard::Selection);
2908                     }
2909                     d->messageWindow->display(i18n("Image [%1x%2] copied to clipboard.", copyPix.width(), copyPix.height()));
2910                 } else if (choice == imageToFile) {
2911                     // [3] save pixmap to file
2912                     QString fileName = QFileDialog::getSaveFileName(this, i18n("Save file"), QString(), i18n("Images (*.png *.jpeg)"));
2913                     if (fileName.isEmpty()) {
2914                         d->messageWindow->display(i18n("File not saved."), QString(), PageViewMessage::Warning);
2915                     } else {
2916                         QMimeDatabase db;
2917                         QMimeType mime = db.mimeTypeForUrl(QUrl::fromLocalFile(fileName));
2918                         QString type;
2919                         if (!mime.isDefault()) {
2920                             type = QStringLiteral("PNG");
2921                         } else {
2922                             type = mime.name().section(QLatin1Char('/'), -1).toUpper();
2923                         }
2924                         copyPix.save(fileName, qPrintable(type));
2925                         d->messageWindow->display(i18n("Image [%1x%2] saved to %3 file.", copyPix.width(), copyPix.height(), type));
2926                     }
2927                 }
2928             }
2929             // TEXT operation chosen
2930             else {
2931                 if (choice == textToClipboard) {
2932                     // [1] copy text to clipboard
2933                     QClipboard *cb = QApplication::clipboard();
2934                     cb->setText(selectedText, QClipboard::Clipboard);
2935                     if (cb->supportsSelection()) {
2936                         cb->setText(selectedText, QClipboard::Selection);
2937                     }
2938                 }
2939 #if HAVE_SPEECH
2940                 else if (choice == speakText) {
2941                     // [2] speech selection using TTS
2942                     d->tts()->say(selectedText);
2943                 }
2944 #endif
2945             }
2946         }
2947         // clear widget selection and invalidate rect
2948         selectionClear();
2949 
2950         // restore previous action if came from it using right button
2951         if (d->aPrevAction) {
2952             d->aPrevAction->trigger();
2953             d->aPrevAction = nullptr;
2954         }
2955     } break;
2956 
2957     case Okular::Settings::EnumMouseMode::TableSelect: {
2958         // if it is a left release checks if is over a previous link press
2959         if (leftButton && mouseReleaseOverLink(d->mouseOverLinkObject)) {
2960             selectionClear();
2961             break;
2962         }
2963 
2964         // if mouse is released and selection is null this is a rightClick
2965         if (rightButton && !d->mouseSelecting) {
2966             PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2967             Q_EMIT rightClick(pageItem ? pageItem->page() : nullptr, e->globalPosition().toPoint());
2968             break;
2969         }
2970 
2971         QRect selectionRect = d->mouseSelectionRect.normalized();
2972         if (selectionRect.width() <= 8 && selectionRect.height() <= 8 && d->tableSelectionParts.isEmpty()) {
2973             selectionClear();
2974             if (d->aPrevAction) {
2975                 d->aPrevAction->trigger();
2976                 d->aPrevAction = nullptr;
2977             }
2978             break;
2979         }
2980 
2981         if (d->mouseSelecting) {
2982             // break up the selection into page-relative pieces
2983             d->tableSelectionParts.clear();
2984             const Okular::Page *okularPage = nullptr;
2985             for (PageViewItem *item : std::as_const(d->items)) {
2986                 if (!item->isVisible()) {
2987                     continue;
2988                 }
2989 
2990                 const QRect &itemRect = item->croppedGeometry();
2991                 if (selectionRect.intersects(itemRect)) {
2992                     // request the textpage if there isn't one
2993                     okularPage = item->page();
2994                     qCDebug(OkularUiDebug) << "checking if page" << item->pageNumber() << "has text:" << okularPage->hasTextPage();
2995                     if (!okularPage->hasTextPage()) {
2996                         d->document->requestTextPage(okularPage->number());
2997                     }
2998                     // grab text in the rect that intersects itemRect
2999                     QRect rectInItem = selectionRect.intersected(itemRect);
3000                     rectInItem.translate(-item->uncroppedGeometry().topLeft());
3001                     QRect rectInSelection = selectionRect.intersected(itemRect);
3002                     rectInSelection.translate(-selectionRect.topLeft());
3003                     d->tableSelectionParts.append(
3004                         TableSelectionPart(item, Okular::NormalizedRect(rectInItem, item->uncroppedWidth(), item->uncroppedHeight()), Okular::NormalizedRect(rectInSelection, selectionRect.width(), selectionRect.height())));
3005                 }
3006             }
3007 
3008             QRect updatedRect = d->mouseSelectionRect.normalized().adjusted(0, 0, 1, 1);
3009             updatedRect.translate(-contentAreaPosition());
3010             d->mouseSelecting = false;
3011             d->mouseSelectionRect.setCoords(0, 0, 0, 0);
3012             d->tableSelectionCols.clear();
3013             d->tableSelectionRows.clear();
3014             guessTableDividers();
3015             viewport()->update(updatedRect);
3016         }
3017 
3018         if (!d->document->isAllowed(Okular::AllowCopy)) {
3019             d->messageWindow->display(i18n("Copy forbidden by DRM"), QString(), PageViewMessage::Info, -1);
3020             break;
3021         }
3022 
3023         QClipboard *cb = QApplication::clipboard();
3024         if (cb->supportsSelection()) {
3025             cb->setMimeData(getTableContents(), QClipboard::Selection);
3026         }
3027 
3028     } break;
3029 
3030     case Okular::Settings::EnumMouseMode::TextSelect:
3031         // if it is a left release checks if is over a previous link press
3032         if (leftButton && mouseReleaseOverLink(d->mouseOverLinkObject)) {
3033             selectionClear();
3034             break;
3035         }
3036 
3037         if (d->mouseTextSelecting) {
3038             d->mouseTextSelecting = false;
3039             //                    textSelectionClear();
3040             if (d->document->isAllowed(Okular::AllowCopy)) {
3041                 const QString text = d->selectedText();
3042                 if (!text.isEmpty()) {
3043                     QClipboard *cb = QApplication::clipboard();
3044                     if (cb->supportsSelection()) {
3045                         cb->setText(text, QClipboard::Selection);
3046                     }
3047                 }
3048             }
3049         } else if (!d->mousePressPos.isNull() && rightButton) {
3050             PageViewItem *item = pickItemOnPoint(eventPos.x(), eventPos.y());
3051             const Okular::Page *page;
3052             // if there is text selected in the page
3053             if (item) {
3054                 QAction *httpLink = nullptr;
3055                 QAction *textToClipboard = nullptr;
3056                 QString url;
3057 
3058                 QMenu *menu = createProcessLinkMenu(item, eventPos);
3059                 const bool mouseClickOverLink = (menu != nullptr);
3060 #if HAVE_SPEECH
3061                 QAction *speakText = nullptr;
3062 #endif
3063                 if ((page = item->page())->textSelection()) {
3064                     if (!menu) {
3065                         menu = new QMenu(this);
3066                     }
3067                     textToClipboard = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy Text"));
3068 
3069 #if HAVE_SPEECH
3070                     if (Okular::Settings::useTTS()) {
3071                         speakText = menu->addAction(QIcon::fromTheme(QStringLiteral("text-speak")), i18n("Speak Text"));
3072                     }
3073 #endif
3074                     if (!d->document->isAllowed(Okular::AllowCopy)) {
3075                         textToClipboard->setEnabled(false);
3076                         textToClipboard->setText(i18n("Copy forbidden by DRM"));
3077                     } else {
3078                         addSearchWithinDocumentAction(menu, d->selectedText());
3079                         addWebShortcutsMenu(menu, d->selectedText());
3080                     }
3081 
3082                     // if the right-click was over a link add "Follow This link" instead of "Go to"
3083                     if (!mouseClickOverLink) {
3084                         url = UrlUtils::getUrl(d->selectedText());
3085                         if (!url.isEmpty()) {
3086                             const QString squeezedText = KStringHandler::rsqueeze(url, linkTextPreviewLength);
3087                             httpLink = menu->addAction(i18n("Go to '%1'", squeezedText));
3088                             httpLink->setObjectName(QStringLiteral("GoToAction"));
3089                         }
3090                     }
3091                 }
3092 
3093                 if (menu) {
3094                     menu->setObjectName(QStringLiteral("PopupMenu"));
3095 
3096                     QAction *choice = menu->exec(e->globalPosition().toPoint());
3097                     // check if the user really selected an action
3098                     if (choice) {
3099                         if (choice == textToClipboard) {
3100                             copyTextSelection();
3101 #if HAVE_SPEECH
3102                         } else if (choice == speakText) {
3103                             const QString text = d->selectedText();
3104                             d->tts()->say(text);
3105 #endif
3106                         } else if (choice == httpLink) {
3107                             auto *job = new KIO::OpenUrlJob(QUrl(url));
3108                             job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this));
3109                             job->start();
3110                         }
3111                     }
3112 
3113                     menu->deleteLater();
3114                 }
3115             }
3116         }
3117         break;
3118     }
3119 
3120     // reset mouse press / 'drag start' position
3121     d->mousePressPos = QPointF();
3122 }
3123 
3124 void PageView::guessTableDividers()
3125 {
3126     QList<QPair<double, int>> colTicks, rowTicks, colSelectionTicks, rowSelectionTicks;
3127 
3128     for (const TableSelectionPart &tsp : std::as_const(d->tableSelectionParts)) {
3129         // add ticks for the edges of this area...
3130         colSelectionTicks.append(qMakePair(tsp.rectInSelection.left, +1));
3131         colSelectionTicks.append(qMakePair(tsp.rectInSelection.right, -1));
3132         rowSelectionTicks.append(qMakePair(tsp.rectInSelection.top, +1));
3133         rowSelectionTicks.append(qMakePair(tsp.rectInSelection.bottom, -1));
3134 
3135         // get the words in this part
3136         Okular::RegularAreaRect rects;
3137         rects.append(tsp.rectInItem);
3138         const Okular::TextEntity::List words = tsp.item->page()->words(&rects, Okular::TextPage::CentralPixelTextAreaInclusionBehaviour);
3139 
3140         for (const Okular::TextEntity &te : words) {
3141             if (te.text().isEmpty()) {
3142                 continue;
3143             }
3144 
3145             Okular::NormalizedRect wordArea = te.area();
3146 
3147             // convert it from item coordinates to part coordinates
3148             wordArea.left -= tsp.rectInItem.left;
3149             wordArea.left /= (tsp.rectInItem.right - tsp.rectInItem.left);
3150             wordArea.right -= tsp.rectInItem.left;
3151             wordArea.right /= (tsp.rectInItem.right - tsp.rectInItem.left);
3152             wordArea.top -= tsp.rectInItem.top;
3153             wordArea.top /= (tsp.rectInItem.bottom - tsp.rectInItem.top);
3154             wordArea.bottom -= tsp.rectInItem.top;
3155             wordArea.bottom /= (tsp.rectInItem.bottom - tsp.rectInItem.top);
3156 
3157             // convert from part coordinates to table coordinates
3158             wordArea.left *= (tsp.rectInSelection.right - tsp.rectInSelection.left);
3159             wordArea.left += tsp.rectInSelection.left;
3160             wordArea.right *= (tsp.rectInSelection.right - tsp.rectInSelection.left);
3161             wordArea.right += tsp.rectInSelection.left;
3162             wordArea.top *= (tsp.rectInSelection.bottom - tsp.rectInSelection.top);
3163             wordArea.top += tsp.rectInSelection.top;
3164             wordArea.bottom *= (tsp.rectInSelection.bottom - tsp.rectInSelection.top);
3165             wordArea.bottom += tsp.rectInSelection.top;
3166 
3167             // add to the ticks arrays...
3168             colTicks.append(qMakePair(wordArea.left, +1));
3169             colTicks.append(qMakePair(wordArea.right, -1));
3170             rowTicks.append(qMakePair(wordArea.top, +1));
3171             rowTicks.append(qMakePair(wordArea.bottom, -1));
3172         }
3173     }
3174 
3175     int tally = 0;
3176 
3177     std::sort(colSelectionTicks.begin(), colSelectionTicks.end());
3178     std::sort(rowSelectionTicks.begin(), rowSelectionTicks.end());
3179 
3180     for (int i = 0; i < colSelectionTicks.length(); ++i) {
3181         tally += colSelectionTicks[i].second;
3182         if (tally == 0 && i + 1 < colSelectionTicks.length() && colSelectionTicks[i + 1].first != colSelectionTicks[i].first) {
3183             colTicks.append(qMakePair(colSelectionTicks[i].first, +1));
3184             colTicks.append(qMakePair(colSelectionTicks[i + 1].first, -1));
3185         }
3186     }
3187     Q_ASSERT(tally == 0);
3188 
3189     for (int i = 0; i < rowSelectionTicks.length(); ++i) {
3190         tally += rowSelectionTicks[i].second;
3191         if (tally == 0 && i + 1 < rowSelectionTicks.length() && rowSelectionTicks[i + 1].first != rowSelectionTicks[i].first) {
3192             rowTicks.append(qMakePair(rowSelectionTicks[i].first, +1));
3193             rowTicks.append(qMakePair(rowSelectionTicks[i + 1].first, -1));
3194         }
3195     }
3196     Q_ASSERT(tally == 0);
3197 
3198     std::sort(colTicks.begin(), colTicks.end());
3199     std::sort(rowTicks.begin(), rowTicks.end());
3200 
3201     for (int i = 0; i < colTicks.length(); ++i) {
3202         tally += colTicks[i].second;
3203         if (tally == 0 && i + 1 < colTicks.length() && colTicks[i + 1].first != colTicks[i].first) {
3204             d->tableSelectionCols.append((colTicks[i].first + colTicks[i + 1].first) / 2);
3205             d->tableDividersGuessed = true;
3206         }
3207     }
3208     Q_ASSERT(tally == 0);
3209 
3210     for (int i = 0; i < rowTicks.length(); ++i) {
3211         tally += rowTicks[i].second;
3212         if (tally == 0 && i + 1 < rowTicks.length() && rowTicks[i + 1].first != rowTicks[i].first) {
3213             d->tableSelectionRows.append((rowTicks[i].first + rowTicks[i + 1].first) / 2);
3214             d->tableDividersGuessed = true;
3215         }
3216     }
3217     Q_ASSERT(tally == 0);
3218 }
3219 
3220 void PageView::mouseDoubleClickEvent(QMouseEvent *e)
3221 {
3222     if (e->button() == Qt::LeftButton) {
3223         const QPoint eventPos = contentAreaPoint(e->pos());
3224         PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
3225         if (pageItem) {
3226             // find out normalized mouse coords inside current item
3227             double nX = pageItem->absToPageX(eventPos.x());
3228             double nY = pageItem->absToPageY(eventPos.y());
3229 
3230             if (d->mouseMode == Okular::Settings::EnumMouseMode::TextSelect) {
3231                 textSelectionClear();
3232 
3233                 Okular::RegularAreaRect *wordRect = pageItem->page()->wordAt(Okular::NormalizedPoint(nX, nY));
3234                 if (wordRect) {
3235                     // TODO words with hyphens across pages
3236                     d->document->setPageTextSelection(pageItem->pageNumber(), wordRect, palette().color(QPalette::Active, QPalette::Highlight));
3237                     d->pagesWithTextSelection << pageItem->pageNumber();
3238                     if (d->document->isAllowed(Okular::AllowCopy)) {
3239                         const QString text = d->selectedText();
3240                         if (!text.isEmpty()) {
3241                             QClipboard *cb = QApplication::clipboard();
3242                             if (cb->supportsSelection()) {
3243                                 cb->setText(text, QClipboard::Selection);
3244                             }
3245                         }
3246                     }
3247                     return;
3248                 }
3249             }
3250 
3251             const QRect &itemRect = pageItem->uncroppedGeometry();
3252             Okular::Annotation *ann = nullptr;
3253 
3254             const Okular::ObjectRect *orect = pageItem->page()->objectRect(Okular::ObjectRect::OAnnotation, nX, nY, itemRect.width(), itemRect.height());
3255             if (orect) {
3256                 ann = ((Okular::AnnotationObjectRect *)orect)->annotation();
3257             }
3258             if (ann && ann->subType() != Okular::Annotation::AWidget) {
3259                 openAnnotationWindow(ann, pageItem->pageNumber());
3260             }
3261         }
3262     }
3263 }
3264 
3265 void PageView::wheelEvent(QWheelEvent *e)
3266 {
3267     if (!d->document->isOpened()) {
3268         QAbstractScrollArea::wheelEvent(e);
3269         return;
3270     }
3271 
3272     int delta = e->angleDelta().y(), vScroll = verticalScrollBar()->value();
3273     e->accept();
3274     if ((e->modifiers() & Qt::ControlModifier) == Qt::ControlModifier) {
3275         continuousZoom(delta);
3276     } else {
3277         if (delta <= -QWheelEvent::DefaultDeltasPerStep && !getContinuousMode() && vScroll == verticalScrollBar()->maximum()) {
3278             // go to next page
3279             if ((int)d->document->currentPage() < d->items.count() - 1) {
3280                 // more optimized than document->setNextPage and then move view to top
3281                 Okular::DocumentViewport newViewport = d->document->viewport();
3282                 newViewport.pageNumber += viewColumns();
3283                 if (newViewport.pageNumber >= (int)d->items.count()) {
3284                     newViewport.pageNumber = d->items.count() - 1;
3285                 }
3286                 newViewport.rePos.enabled = true;
3287                 newViewport.rePos.normalizedY = 0.0;
3288                 d->document->setViewport(newViewport);
3289                 d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); // sync scroller with scrollbar
3290             }
3291         } else if (delta >= QWheelEvent::DefaultDeltasPerStep && !getContinuousMode() && vScroll == verticalScrollBar()->minimum()) {
3292             // go to prev page
3293             if (d->document->currentPage() > 0) {
3294                 // more optimized than document->setPrevPage and then move view to bottom
3295                 Okular::DocumentViewport newViewport = d->document->viewport();
3296                 newViewport.pageNumber -= viewColumns();
3297                 if (newViewport.pageNumber < 0) {
3298                     newViewport.pageNumber = 0;
3299                 }
3300                 newViewport.rePos.enabled = true;
3301                 newViewport.rePos.normalizedY = 1.0;
3302                 d->document->setViewport(newViewport);
3303                 d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); // sync scroller with scrollbar
3304             }
3305         } else {
3306             // When the shift key is held down, scroll ten times faster
3307             int multiplier = e->modifiers() & Qt::ShiftModifier ? 10 : 1;
3308 
3309             if (delta != 0 && delta % QWheelEvent::DefaultDeltasPerStep == 0) {
3310                 // number of scroll wheel steps Qt gives to us at the same time
3311                 int count = abs(delta / QWheelEvent::DefaultDeltasPerStep) * multiplier;
3312                 if (delta < 0) {
3313                     slotScrollDown(count);
3314                 } else {
3315                     slotScrollUp(count);
3316                 }
3317             } else {
3318                 d->scroller->scrollTo(d->scroller->finalPosition() - e->angleDelta() * multiplier, 0);
3319             }
3320         }
3321     }
3322 }
3323 
3324 bool PageView::viewportEvent(QEvent *e)
3325 {
3326     if (e->type() == QEvent::ToolTip
3327         // Show tool tips only for those modes that change the cursor
3328         // to a hand when hovering over the link.
3329         && (d->mouseMode == Okular::Settings::EnumMouseMode::Browse || d->mouseMode == Okular::Settings::EnumMouseMode::RectSelect || d->mouseMode == Okular::Settings::EnumMouseMode::TextSelect ||
3330             d->mouseMode == Okular::Settings::EnumMouseMode::TrimSelect)) {
3331         QHelpEvent *he = static_cast<QHelpEvent *>(e);
3332         if (d->mouseAnnotation->isMouseOver()) {
3333             d->mouseAnnotation->routeTooltipEvent(he);
3334         } else {
3335             const QPoint eventPos = contentAreaPoint(he->pos());
3336             PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
3337             const Okular::ObjectRect *rect = nullptr;
3338             const Okular::Action *link = nullptr;
3339             if (pageItem) {
3340                 double nX = pageItem->absToPageX(eventPos.x());
3341                 double nY = pageItem->absToPageY(eventPos.y());
3342                 rect = pageItem->page()->objectRect(Okular::ObjectRect::Action, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight());
3343                 if (rect) {
3344                     link = static_cast<const Okular::Action *>(rect->object());
3345                 }
3346             }
3347 
3348             if (link) {
3349                 QRect r = rect->boundingRect(pageItem->uncroppedWidth(), pageItem->uncroppedHeight());
3350                 r.translate(pageItem->uncroppedGeometry().topLeft());
3351                 r.translate(-contentAreaPosition());
3352                 QString tip = link->actionTip();
3353                 if (!tip.isEmpty()) {
3354                     QToolTip::showText(he->globalPos(), tip, viewport(), r);
3355                 }
3356             }
3357         }
3358         e->accept();
3359         return true;
3360     } else {
3361         // do not stop the event
3362         return QAbstractScrollArea::viewportEvent(e);
3363     }
3364 }
3365 
3366 void PageView::scrollContentsBy(int dx, int dy)
3367 {
3368     const QRect r = viewport()->rect();
3369     viewport()->scroll(dx, dy, r);
3370     // HACK manually repaint the damaged regions, as it seems some updates are missed
3371     // thus leaving artifacts around
3372     QRegion rgn(r);
3373     rgn -= rgn & r.translated(dx, dy);
3374 
3375     for (const QRect &rect : rgn) {
3376         viewport()->update(rect);
3377     }
3378 
3379     updateCursor();
3380 }
3381 // END widget events
3382 
3383 QList<Okular::RegularAreaRect *> PageView::textSelections(const QPoint start, const QPoint end, int &firstpage)
3384 {
3385     firstpage = -1;
3386     QList<Okular::RegularAreaRect *> ret;
3387     QSet<int> affectedItemsSet;
3388     QRect selectionRect = QRect(start, end).normalized();
3389     for (const PageViewItem *item : std::as_const(d->items)) {
3390         if (item->isVisible() && selectionRect.intersects(item->croppedGeometry())) {
3391             affectedItemsSet.insert(item->pageNumber());
3392         }
3393     }
3394 #ifdef PAGEVIEW_DEBUG
3395     qCDebug(OkularUiDebug) << ">>>> item selected by mouse:" << affectedItemsSet.count();
3396 #endif
3397 
3398     if (!affectedItemsSet.isEmpty()) {
3399         // is the mouse drag line the ne-sw diagonal of the selection rect?
3400         bool direction_ne_sw = start == selectionRect.topRight() || start == selectionRect.bottomLeft();
3401 
3402         int tmpmin = d->document->pages();
3403         int tmpmax = 0;
3404         for (const int p : std::as_const(affectedItemsSet)) {
3405             if (p < tmpmin) {
3406                 tmpmin = p;
3407             }
3408             if (p > tmpmax) {
3409                 tmpmax = p;
3410             }
3411         }
3412 
3413         PageViewItem *a = pickItemOnPoint((int)(direction_ne_sw ? selectionRect.right() : selectionRect.left()), (int)selectionRect.top());
3414         int min = a && (a->pageNumber() != tmpmax) ? a->pageNumber() : tmpmin;
3415         PageViewItem *b = pickItemOnPoint((int)(direction_ne_sw ? selectionRect.left() : selectionRect.right()), (int)selectionRect.bottom());
3416         int max = b && (b->pageNumber() != tmpmin) ? b->pageNumber() : tmpmax;
3417 
3418         QList<int> affectedItemsIds;
3419         for (int i = min; i <= max; ++i) {
3420             affectedItemsIds.append(i);
3421         }
3422 #ifdef PAGEVIEW_DEBUG
3423         qCDebug(OkularUiDebug) << ">>>> pages:" << affectedItemsIds;
3424 #endif
3425         firstpage = affectedItemsIds.first();
3426 
3427         if (affectedItemsIds.count() == 1) {
3428             PageViewItem *item = d->items[affectedItemsIds.first()];
3429             selectionRect.translate(-item->uncroppedGeometry().topLeft());
3430             ret.append(textSelectionForItem(item, direction_ne_sw ? selectionRect.topRight() : selectionRect.topLeft(), direction_ne_sw ? selectionRect.bottomLeft() : selectionRect.bottomRight()));
3431         } else if (affectedItemsIds.count() > 1) {
3432             // first item
3433             PageViewItem *first = d->items[affectedItemsIds.first()];
3434             QRect geom = first->croppedGeometry().intersected(selectionRect).translated(-first->uncroppedGeometry().topLeft());
3435             ret.append(textSelectionForItem(first, selectionRect.bottom() > geom.height() ? (direction_ne_sw ? geom.topRight() : geom.topLeft()) : (direction_ne_sw ? geom.bottomRight() : geom.bottomLeft()), QPoint()));
3436             // last item
3437             PageViewItem *last = d->items[affectedItemsIds.last()];
3438             geom = last->croppedGeometry().intersected(selectionRect).translated(-last->uncroppedGeometry().topLeft());
3439             // the last item needs to appended at last...
3440             Okular::RegularAreaRect *lastArea =
3441                 textSelectionForItem(last, QPoint(), selectionRect.bottom() > geom.height() ? (direction_ne_sw ? geom.bottomLeft() : geom.bottomRight()) : (direction_ne_sw ? geom.topLeft() : geom.topRight()));
3442             affectedItemsIds.removeFirst();
3443             affectedItemsIds.removeLast();
3444             // item between the two above
3445             for (const int page : std::as_const(affectedItemsIds)) {
3446                 ret.append(textSelectionForItem(d->items[page]));
3447             }
3448             ret.append(lastArea);
3449         }
3450     }
3451     return ret;
3452 }
3453 
3454 void PageView::drawDocumentOnPainter(const QRect contentsRect, QPainter *p)
3455 {
3456     QColor backColor;
3457 
3458     if (Okular::Settings::useCustomBackgroundColor()) {
3459         backColor = Okular::Settings::backgroundColor();
3460     } else {
3461         backColor = viewport()->palette().color(QPalette::Dark);
3462     }
3463 
3464     // create a region from which we'll subtract painted rects
3465     QRegion remainingArea(contentsRect);
3466 
3467     // This loop draws the actual pages
3468     // iterate over all items painting the ones intersecting contentsRect
3469     for (const PageViewItem *item : std::as_const(d->items)) {
3470         // check if a piece of the page intersects the contents rect
3471         if (!item->isVisible() || !item->croppedGeometry().intersects(contentsRect)) {
3472             continue;
3473         }
3474 
3475         // get item and item's outline geometries
3476         QRect itemGeometry = item->croppedGeometry();
3477 
3478         // move the painter to the top-left corner of the real page
3479         p->save();
3480         p->translate(itemGeometry.left(), itemGeometry.top());
3481 
3482         // draw the page using the PagePainter with all flags active
3483         if (contentsRect.intersects(itemGeometry)) {
3484             Okular::NormalizedPoint *viewPortPoint = nullptr;
3485             Okular::NormalizedPoint point(d->lastSourceLocationViewportNormalizedX, d->lastSourceLocationViewportNormalizedY);
3486             if (Okular::Settings::showSourceLocationsGraphically() && item->pageNumber() == d->lastSourceLocationViewportPageNumber) {
3487                 viewPortPoint = &point;
3488             }
3489             QRect pixmapRect = contentsRect.intersected(itemGeometry);
3490             pixmapRect.translate(-item->croppedGeometry().topLeft());
3491             PagePainter::paintCroppedPageOnPainter(p, item->page(), this, pageflags, item->uncroppedWidth(), item->uncroppedHeight(), pixmapRect, item->crop(), viewPortPoint);
3492         }
3493 
3494         // remove painted area from 'remainingArea' and restore painter
3495         remainingArea -= itemGeometry;
3496         p->restore();
3497     }
3498 
3499     // fill the visible area around the page with the background color
3500     for (const QRect &backRect : remainingArea) {
3501         p->fillRect(backRect, backColor);
3502     }
3503 
3504     // take outline and shadow into account when testing whether a repaint is necessary
3505     auto dpr = devicePixelRatioF();
3506     QRect checkRect = contentsRect;
3507     checkRect.adjust(-3, -3, 1, 1);
3508 
3509     // Method to linearly interpolate between black (=(0,0,0), omitted) and the background color
3510     auto interpolateColor = [&backColor](double t) { return QColor(t * backColor.red(), t * backColor.green(), t * backColor.blue()); };
3511 
3512     // width of the shadow in device pixels
3513     static const int shadowWidth = 2 * dpr;
3514 
3515     // iterate over all items painting a black outline and a simple bottom/right gradient
3516     for (const PageViewItem *item : std::as_const(d->items)) {
3517         // check if a piece of the page intersects the contents rect
3518         if (!item->isVisible() || !item->croppedGeometry().intersects(checkRect)) {
3519             continue;
3520         }
3521 
3522         // get item and item's outline geometries
3523         QRect itemGeometry = item->croppedGeometry();
3524 
3525         // move the painter to the top-left corner of the real page
3526         p->save();
3527         p->translate(itemGeometry.left(), itemGeometry.top());
3528 
3529         // draw the page outline (black border and bottom-right shadow)
3530         if (!itemGeometry.contains(contentsRect)) {
3531             int itemWidth = itemGeometry.width();
3532             int itemHeight = itemGeometry.height();
3533             // draw simple outline
3534             QPen pen(Qt::black);
3535             pen.setWidth(0);
3536             p->setPen(pen);
3537 
3538             QRectF outline(-1.0 / dpr, -1.0 / dpr, itemWidth + 1.0 / dpr, itemHeight + 1.0 / dpr);
3539             p->drawRect(outline);
3540 
3541             // draw bottom/right gradient
3542             for (int i = 1; i <= shadowWidth; i++) {
3543                 pen.setColor(interpolateColor(double(i) / (shadowWidth + 1)));
3544                 p->setPen(pen);
3545                 QPointF left((i - 1) / dpr, itemHeight + i / dpr);
3546                 QPointF up(itemWidth + i / dpr, (i - 1) / dpr);
3547                 QPointF corner(itemWidth + i / dpr, itemHeight + i / dpr);
3548                 p->drawLine(left, corner);
3549                 p->drawLine(up, corner);
3550             }
3551         }
3552 
3553         p->restore();
3554     }
3555 }
3556 
3557 void PageView::updateItemSize(PageViewItem *item, int colWidth, int rowHeight)
3558 {
3559     const Okular::Page *okularPage = item->page();
3560     double width = okularPage->width(), height = okularPage->height(), zoom = d->zoomFactor;
3561     Okular::NormalizedRect crop(0., 0., 1., 1.);
3562 
3563     // Handle cropping, due to either "Trim Margin" or "Trim to Selection" cases
3564     if ((Okular::Settings::trimMargins() && okularPage->isBoundingBoxKnown() && !okularPage->boundingBox().isNull()) || (d->aTrimToSelection && d->aTrimToSelection->isChecked() && !d->trimBoundingBox.isNull())) {
3565         crop = Okular::Settings::trimMargins() ? okularPage->boundingBox() : d->trimBoundingBox;
3566 
3567         // Rotate the bounding box
3568         for (int i = okularPage->rotation(); i > 0; --i) {
3569             Okular::NormalizedRect rot = crop;
3570             crop.left = 1 - rot.bottom;
3571             crop.top = rot.left;
3572             crop.right = 1 - rot.top;
3573             crop.bottom = rot.right;
3574         }
3575 
3576         // Expand the crop slightly beyond the bounding box (for Trim Margins only)
3577         if (Okular::Settings::trimMargins()) {
3578             static const double cropExpandRatio = 0.04;
3579             const double cropExpand = cropExpandRatio * ((crop.right - crop.left) + (crop.bottom - crop.top)) / 2;
3580             crop = Okular::NormalizedRect(crop.left - cropExpand, crop.top - cropExpand, crop.right + cropExpand, crop.bottom + cropExpand) & Okular::NormalizedRect(0, 0, 1, 1);
3581         }
3582 
3583         // We currently generate a larger image and then crop it, so if the
3584         // crop rect is very small the generated image is huge. Hence, we shouldn't
3585         // let the crop rect become too small.
3586         static double minCropRatio;
3587         if (Okular::Settings::trimMargins()) {
3588             // Make sure we crop by at most 50% in either dimension:
3589             minCropRatio = 0.5;
3590         } else {
3591             // Looser Constraint for "Trim Selection"
3592             minCropRatio = 0.20;
3593         }
3594         if ((crop.right - crop.left) < minCropRatio) {
3595             const double newLeft = (crop.left + crop.right) / 2 - minCropRatio / 2;
3596             crop.left = qMax(0.0, qMin(1.0 - minCropRatio, newLeft));
3597             crop.right = crop.left + minCropRatio;
3598         }
3599         if ((crop.bottom - crop.top) < minCropRatio) {
3600             const double newTop = (crop.top + crop.bottom) / 2 - minCropRatio / 2;
3601             crop.top = qMax(0.0, qMin(1.0 - minCropRatio, newTop));
3602             crop.bottom = crop.top + minCropRatio;
3603         }
3604 
3605         width *= (crop.right - crop.left);
3606         height *= (crop.bottom - crop.top);
3607 #ifdef PAGEVIEW_DEBUG
3608         qCDebug(OkularUiDebug) << "Cropped page" << okularPage->number() << "to" << crop << "width" << width << "height" << height << "by bbox" << okularPage->boundingBox();
3609 #endif
3610     }
3611 
3612     if (d->zoomMode == ZoomFixed) {
3613         width *= zoom;
3614         height *= zoom;
3615         item->setWHZC((int)width, (int)height, d->zoomFactor, crop);
3616     } else if (d->zoomMode == ZoomFitWidth) {
3617         height = (height / width) * colWidth;
3618         zoom = (double)colWidth / width;
3619         item->setWHZC(colWidth, (int)height, zoom, crop);
3620         if ((uint)item->pageNumber() == d->document->currentPage()) {
3621             d->zoomFactor = zoom;
3622         }
3623     } else if (d->zoomMode == ZoomFitPage) {
3624         const double scaleW = (double)colWidth / (double)width;
3625         const double scaleH = (double)rowHeight / (double)height;
3626         zoom = qMin(scaleW, scaleH);
3627         item->setWHZC((int)(zoom * width), (int)(zoom * height), zoom, crop);
3628         if ((uint)item->pageNumber() == d->document->currentPage()) {
3629             d->zoomFactor = zoom;
3630         }
3631     } else if (d->zoomMode == ZoomFitAuto) {
3632         const double aspectRatioRelation = 1.25; // relation between aspect ratios for "auto fit"
3633         const double uiAspect = (double)rowHeight / (double)colWidth;
3634         const double pageAspect = (double)height / (double)width;
3635         const double rel = uiAspect / pageAspect;
3636 
3637         if (!getContinuousMode() && rel > aspectRatioRelation) {
3638             // UI space is relatively much higher than the page
3639             zoom = (double)rowHeight / (double)height;
3640         } else if (rel < 1.0 / aspectRatioRelation) {
3641             // UI space is relatively much wider than the page in relation
3642             zoom = (double)colWidth / (double)width;
3643         } else {
3644             // aspect ratios of page and UI space are very similar
3645             const double scaleW = (double)colWidth / (double)width;
3646             const double scaleH = (double)rowHeight / (double)height;
3647             zoom = qMin(scaleW, scaleH);
3648         }
3649         item->setWHZC((int)(zoom * width), (int)(zoom * height), zoom, crop);
3650         if ((uint)item->pageNumber() == d->document->currentPage()) {
3651             d->zoomFactor = zoom;
3652         }
3653     }
3654 #ifndef NDEBUG
3655     else {
3656         qCDebug(OkularUiDebug) << "calling updateItemSize with unrecognized d->zoomMode!";
3657     }
3658 #endif
3659 }
3660 
3661 PageViewItem *PageView::pickItemOnPoint(int x, int y)
3662 {
3663     PageViewItem *item = nullptr;
3664     for (PageViewItem *i : std::as_const(d->visibleItems)) {
3665         const QRect &r = i->croppedGeometry();
3666         if (x < r.right() && x > r.left() && y < r.bottom()) {
3667             if (y > r.top()) {
3668                 item = i;
3669             }
3670             break;
3671         }
3672     }
3673     return item;
3674 }
3675 
3676 void PageView::textSelectionClear()
3677 {
3678     // something to clear
3679     if (!d->pagesWithTextSelection.isEmpty()) {
3680         for (const int page : std::as_const(d->pagesWithTextSelection)) {
3681             d->document->setPageTextSelection(page, nullptr, QColor());
3682         }
3683         d->pagesWithTextSelection.clear();
3684     }
3685 }
3686 
3687 void PageView::selectionStart(const QPoint pos, const QColor &color, bool /*aboveAll*/)
3688 {
3689     selectionClear();
3690     d->mouseSelecting = true;
3691     d->mouseSelectionRect.setRect(pos.x(), pos.y(), 1, 1);
3692     d->mouseSelectionColor = color;
3693     // ensures page doesn't scroll
3694     if (d->autoScrollTimer) {
3695         d->scrollIncrement = 0;
3696         d->autoScrollTimer->stop();
3697     }
3698 }
3699 
3700 void PageView::scrollPosIntoView(const QPoint pos)
3701 {
3702     // this number slows the speed of the page by its value, chosen not to be too fast or too slow, the actual speed is determined from the mouse position, not critical
3703     const int damping = 6;
3704 
3705     if (pos.x() < horizontalScrollBar()->value()) {
3706         d->dragScrollVector.setX((pos.x() - horizontalScrollBar()->value()) / damping);
3707     } else if (horizontalScrollBar()->value() + viewport()->width() < pos.x()) {
3708         d->dragScrollVector.setX((pos.x() - horizontalScrollBar()->value() - viewport()->width()) / damping);
3709     } else {
3710         d->dragScrollVector.setX(0);
3711     }
3712 
3713     if (pos.y() < verticalScrollBar()->value()) {
3714         d->dragScrollVector.setY((pos.y() - verticalScrollBar()->value()) / damping);
3715     } else if (verticalScrollBar()->value() + viewport()->height() < pos.y()) {
3716         d->dragScrollVector.setY((pos.y() - verticalScrollBar()->value() - viewport()->height()) / damping);
3717     } else {
3718         d->dragScrollVector.setY(0);
3719     }
3720 
3721     if (d->dragScrollVector != QPoint(0, 0)) {
3722         if (!d->dragScrollTimer.isActive()) {
3723             d->dragScrollTimer.start(1000 / 60); // 60 fps
3724         }
3725     } else {
3726         d->dragScrollTimer.stop();
3727     }
3728 }
3729 
3730 QPoint PageView::viewportToContentArea(const Okular::DocumentViewport &vp) const
3731 {
3732     Q_ASSERT(vp.pageNumber >= 0);
3733 
3734     const QRect &r = d->items[vp.pageNumber]->croppedGeometry();
3735     QPoint c {r.left(), r.top()};
3736 
3737     if (vp.rePos.enabled) {
3738         // Convert the coordinates of vp to normalized coordinates on the cropped page.
3739         // This is a no-op if the page isn't cropped.
3740         const Okular::NormalizedRect &crop = d->items[vp.pageNumber]->crop();
3741         const double normalized_on_crop_x = (vp.rePos.normalizedX - crop.left) / (crop.right - crop.left);
3742         const double normalized_on_crop_y = (vp.rePos.normalizedY - crop.top) / (crop.bottom - crop.top);
3743 
3744         if (vp.rePos.pos == Okular::DocumentViewport::Center) {
3745             c.rx() += qRound(normClamp(normalized_on_crop_x, 0.5) * (double)r.width());
3746             c.ry() += qRound(normClamp(normalized_on_crop_y, 0.0) * (double)r.height());
3747         } else {
3748             // TopLeft
3749             c.rx() += qRound(normClamp(normalized_on_crop_x, 0.0) * (double)r.width() + viewport()->width() / 2.0);
3750             c.ry() += qRound(normClamp(normalized_on_crop_y, 0.0) * (double)r.height() + viewport()->height() / 2.0);
3751         }
3752     } else {
3753         // exact repositioning disabled, align page top margin with viewport top border by default
3754         c.rx() += r.width() / 2;
3755         c.ry() += viewport()->height() / 2 - 10;
3756     }
3757     return c;
3758 }
3759 
3760 void PageView::updateSelection(const QPoint pos)
3761 {
3762     if (d->mouseSelecting) {
3763         scrollPosIntoView(pos);
3764         // update the selection rect
3765         QRect updateRect = d->mouseSelectionRect;
3766         d->mouseSelectionRect.setBottomLeft(pos);
3767         updateRect |= d->mouseSelectionRect;
3768         updateRect.translate(-contentAreaPosition());
3769         viewport()->update(updateRect.adjusted(-1, -2, 2, 1));
3770     } else if (d->mouseTextSelecting) {
3771         scrollPosIntoView(pos);
3772         int first = -1;
3773         const QList<Okular::RegularAreaRect *> selections = textSelections(pos, d->mouseSelectPos.toPoint(), first);
3774         QSet<int> pagesWithSelectionSet;
3775         for (int i = 0; i < selections.count(); ++i) {
3776             pagesWithSelectionSet.insert(i + first);
3777         }
3778 
3779         const QSet<int> noMoreSelectedPages = d->pagesWithTextSelection - pagesWithSelectionSet;
3780         // clear the selection from pages not selected anymore
3781         for (int p : noMoreSelectedPages) {
3782             d->document->setPageTextSelection(p, nullptr, QColor());
3783         }
3784         // set the new selection for the selected pages
3785         for (int p : std::as_const(pagesWithSelectionSet)) {
3786             d->document->setPageTextSelection(p, selections[p - first], palette().color(QPalette::Active, QPalette::Highlight));
3787         }
3788         d->pagesWithTextSelection = pagesWithSelectionSet;
3789     }
3790 }
3791 
3792 static Okular::NormalizedPoint rotateInNormRect(const QPoint rotated, const QRect rect, Okular::Rotation rotation)
3793 {
3794     Okular::NormalizedPoint ret;
3795 
3796     switch (rotation) {
3797     case Okular::Rotation0:
3798         ret = Okular::NormalizedPoint(rotated.x(), rotated.y(), rect.width(), rect.height());
3799         break;
3800     case Okular::Rotation90:
3801         ret = Okular::NormalizedPoint(rotated.y(), rect.width() - rotated.x(), rect.height(), rect.width());
3802         break;
3803     case Okular::Rotation180:
3804         ret = Okular::NormalizedPoint(rect.width() - rotated.x(), rect.height() - rotated.y(), rect.width(), rect.height());
3805         break;
3806     case Okular::Rotation270:
3807         ret = Okular::NormalizedPoint(rect.height() - rotated.y(), rotated.x(), rect.height(), rect.width());
3808         break;
3809     }
3810 
3811     return ret;
3812 }
3813 
3814 Okular::RegularAreaRect *PageView::textSelectionForItem(const PageViewItem *item, const QPoint startPoint, const QPoint endPoint)
3815 {
3816     const QRect &geometry = item->uncroppedGeometry();
3817     Okular::NormalizedPoint startCursor(0.0, 0.0);
3818     if (!startPoint.isNull()) {
3819         startCursor = rotateInNormRect(startPoint, geometry, item->page()->rotation());
3820     }
3821     Okular::NormalizedPoint endCursor(1.0, 1.0);
3822     if (!endPoint.isNull()) {
3823         endCursor = rotateInNormRect(endPoint, geometry, item->page()->rotation());
3824     }
3825     Okular::TextSelection mouseTextSelectionInfo(startCursor, endCursor);
3826 
3827     const Okular::Page *okularPage = item->page();
3828 
3829     if (!okularPage->hasTextPage()) {
3830         d->document->requestTextPage(okularPage->number());
3831     }
3832 
3833     Okular::RegularAreaRect *selectionArea = okularPage->textArea(&mouseTextSelectionInfo);
3834 #ifdef PAGEVIEW_DEBUG
3835     qCDebug(OkularUiDebug).nospace() << "text areas (" << okularPage->number() << "): " << (selectionArea ? QString::number(selectionArea->count()) : "(none)");
3836 #endif
3837     return selectionArea;
3838 }
3839 
3840 void PageView::selectionClear(const ClearMode mode)
3841 {
3842     QRect updatedRect = d->mouseSelectionRect.normalized().adjusted(-2, -2, 2, 2);
3843     d->mouseSelecting = false;
3844     d->mouseSelectionRect.setCoords(0, 0, 0, 0);
3845     d->tableSelectionCols.clear();
3846     d->tableSelectionRows.clear();
3847     d->tableDividersGuessed = false;
3848     for (const TableSelectionPart &tsp : std::as_const(d->tableSelectionParts)) {
3849         QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight());
3850         selectionPartRect.translate(tsp.item->uncroppedGeometry().topLeft());
3851         // should check whether this is on-screen here?
3852         updatedRect = updatedRect.united(selectionPartRect);
3853     }
3854     if (mode != ClearOnlyDividers) {
3855         d->tableSelectionParts.clear();
3856     }
3857     d->tableSelectionParts.clear();
3858     updatedRect.translate(-contentAreaPosition());
3859     viewport()->update(updatedRect);
3860 }
3861 
3862 // const to be used for both zoomFactorFitMode function and slotRelayoutPages.
3863 static const int kcolWidthMargin = 6;
3864 static const int krowHeightMargin = 12;
3865 
3866 double PageView::zoomFactorFitMode(ZoomMode mode)
3867 {
3868     const int pageCount = d->items.count();
3869     if (pageCount == 0) {
3870         return 0;
3871     }
3872     const bool facingCentered = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered || (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount == 1);
3873     const bool overrideCentering = facingCentered && pageCount < 3;
3874     const int nCols = overrideCentering ? 1 : viewColumns();
3875     const int colWidth = viewport()->width() / nCols - kcolWidthMargin;
3876     const double rowHeight = viewport()->height() - krowHeightMargin;
3877     const PageViewItem *currentItem = d->items[qMax(0, (int)d->document->currentPage())];
3878     // prevent segmentation fault when opening a new document;
3879     if (!currentItem) {
3880         return 0;
3881     }
3882 
3883     // We need the real width/height of the cropped page.
3884     const Okular::Page *okularPage = currentItem->page();
3885     const double width = okularPage->width() * currentItem->crop().width();
3886     const double height = okularPage->height() * currentItem->crop().height();
3887 
3888     if (mode == ZoomFitWidth) {
3889         return (double)colWidth / width;
3890     }
3891     if (mode == ZoomFitPage) {
3892         const double scaleW = (double)colWidth / (double)width;
3893         const double scaleH = (double)rowHeight / (double)height;
3894         return qMin(scaleW, scaleH);
3895     }
3896     return 0;
3897 }
3898 
3899 static double parseZoomString(QString z)
3900 {
3901     // kdelibs4 sometimes adds accelerators to actions' text directly :(
3902     z.remove(QLatin1Char('&'));
3903     z.remove(QLatin1Char('%'));
3904     return QLocale().toDouble(z) / 100.0;
3905 }
3906 
3907 static QString makePrettyZoomString(double value)
3908 {
3909     // we do not need to display 2-digit precision
3910     QString localValue(QLocale().toString(value * 100.0, 'f', 1));
3911     localValue.remove(QLocale().decimalPoint() + QLatin1Char('0'));
3912     // remove a trailing zero in numbers like 66.70
3913     if (localValue.right(1) == QLatin1String("0") && localValue.indexOf(QLocale().decimalPoint()) > -1) {
3914         localValue.chop(1);
3915     }
3916     return localValue;
3917 }
3918 
3919 void PageView::updateZoom(ZoomMode newZoomMode)
3920 {
3921     if (newZoomMode == ZoomFixed) {
3922         if (d->aZoom->currentItem() == 0) {
3923             newZoomMode = ZoomFitWidth;
3924         } else if (d->aZoom->currentItem() == 1) {
3925             newZoomMode = ZoomFitPage;
3926         } else if (d->aZoom->currentItem() == 2) {
3927             newZoomMode = ZoomFitAuto;
3928         }
3929     }
3930 
3931     float newFactor = d->zoomFactor;
3932     QAction *checkedZoomAction = nullptr;
3933     switch (newZoomMode) {
3934     case ZoomFixed: { // ZoomFixed case
3935         newFactor = parseZoomString(d->aZoom->currentText());
3936     } break;
3937     case ZoomIn:
3938     case ZoomOut: {
3939         const float zoomFactorFitWidth = zoomFactorFitMode(ZoomFitWidth);
3940         const float zoomFactorFitPage = zoomFactorFitMode(ZoomFitPage);
3941 
3942         QVector<float> zoomValue(kZoomValues.size());
3943 
3944         std::copy(kZoomValues.begin(), kZoomValues.end(), zoomValue.begin());
3945         zoomValue.append(zoomFactorFitWidth);
3946         zoomValue.append(zoomFactorFitPage);
3947         std::sort(zoomValue.begin(), zoomValue.end());
3948 
3949         QVector<float>::iterator i;
3950         if (newZoomMode == ZoomOut) {
3951             if (newFactor <= zoomValue.first()) {
3952                 return;
3953             }
3954             i = std::lower_bound(zoomValue.begin(), zoomValue.end(), newFactor) - 1;
3955         } else {
3956             if (newFactor >= zoomValue.last()) {
3957                 return;
3958             }
3959             i = std::upper_bound(zoomValue.begin(), zoomValue.end(), newFactor);
3960         }
3961         const float tmpFactor = *i;
3962         if (tmpFactor == zoomFactorFitWidth) {
3963             newZoomMode = ZoomFitWidth;
3964             checkedZoomAction = d->aZoomFitWidth;
3965         } else if (tmpFactor == zoomFactorFitPage) {
3966             newZoomMode = ZoomFitPage;
3967             checkedZoomAction = d->aZoomFitPage;
3968         } else {
3969             newFactor = tmpFactor;
3970             newZoomMode = ZoomFixed;
3971         }
3972     } break;
3973     case ZoomActual:
3974         newZoomMode = ZoomFixed;
3975         newFactor = 1.0;
3976         break;
3977     case ZoomFitWidth:
3978         checkedZoomAction = d->aZoomFitWidth;
3979         break;
3980     case ZoomFitPage:
3981         checkedZoomAction = d->aZoomFitPage;
3982         break;
3983     case ZoomFitAuto:
3984         checkedZoomAction = d->aZoomAutoFit;
3985         break;
3986     case ZoomRefreshCurrent:
3987         newZoomMode = ZoomFixed;
3988         d->zoomFactor = -1;
3989         break;
3990     }
3991     const float upperZoomLimit = d->document->supportsTiles() ? 100.0 : 4.0;
3992     if (newFactor > upperZoomLimit) {
3993         newFactor = upperZoomLimit;
3994     }
3995     if (newFactor < kZoomValues[0]) {
3996         newFactor = kZoomValues[0];
3997     }
3998 
3999     if (newZoomMode != d->zoomMode || (newZoomMode == ZoomFixed && newFactor != d->zoomFactor)) {
4000         // rebuild layout and update the whole viewport
4001         d->zoomMode = newZoomMode;
4002         d->zoomFactor = newFactor;
4003         // be sure to block updates to document's viewport
4004         bool prevState = d->blockViewport;
4005         d->blockViewport = true;
4006         slotRelayoutPages();
4007         d->blockViewport = prevState;
4008         // request pixmaps
4009         slotRequestVisiblePixmaps();
4010         // update zoom text
4011         updateZoomText();
4012         // update actions checked state
4013         if (d->aZoomFitWidth) {
4014             d->aZoomFitWidth->setChecked(checkedZoomAction == d->aZoomFitWidth);
4015             d->aZoomFitPage->setChecked(checkedZoomAction == d->aZoomFitPage);
4016             d->aZoomAutoFit->setChecked(checkedZoomAction == d->aZoomAutoFit);
4017         }
4018     } else if (newZoomMode == ZoomFixed && newFactor == d->zoomFactor) {
4019         updateZoomText();
4020     }
4021 
4022     updateZoomActionsEnabledStatus();
4023 }
4024 
4025 void PageView::updateZoomActionsEnabledStatus()
4026 {
4027     const float upperZoomLimit = d->document->supportsTiles() ? kZoomValues.back() : 4.0;
4028     const bool hasPages = d->document && d->document->pages() > 0;
4029 
4030     if (d->aZoomFitWidth) {
4031         d->aZoomFitWidth->setEnabled(hasPages);
4032     }
4033     if (d->aZoomFitPage) {
4034         d->aZoomFitPage->setEnabled(hasPages);
4035     }
4036     if (d->aZoomAutoFit) {
4037         d->aZoomAutoFit->setEnabled(hasPages);
4038     }
4039     if (d->aZoom) {
4040         d->aZoom->selectableActionGroup()->setEnabled(hasPages);
4041         d->aZoom->setEnabled(hasPages);
4042     }
4043     if (d->aZoomIn) {
4044         d->aZoomIn->setEnabled(hasPages && d->zoomFactor < upperZoomLimit - 0.001);
4045     }
4046     if (d->aZoomOut) {
4047         d->aZoomOut->setEnabled(hasPages && d->zoomFactor > (kZoomValues[0] + 0.001));
4048     }
4049     if (d->aZoomActual) {
4050         d->aZoomActual->setEnabled(hasPages && d->zoomFactor != 1.0);
4051     }
4052 }
4053 
4054 void PageView::updateZoomText()
4055 {
4056     // use current page zoom as zoomFactor if in ZoomFit/* mode
4057     if (d->zoomMode != ZoomFixed && d->items.count() > 0) {
4058         d->zoomFactor = d->items[qMax(0, (int)d->document->currentPage())]->zoomFactor();
4059     }
4060     float newFactor = d->zoomFactor;
4061     d->aZoom->removeAllActions();
4062 
4063     // add items that describe fit actions
4064     QStringList translated;
4065     translated << i18n("Fit Width") << i18n("Fit Page") << i18n("Auto Fit");
4066 
4067     // add percent items
4068     int idx = 0, selIdx = 3;
4069     bool inserted = false; // use: "d->zoomMode != ZoomFixed" to hide Fit/* zoom ratio
4070     int zoomValueCount = 11;
4071     if (d->document->supportsTiles()) {
4072         zoomValueCount = kZoomValues.size();
4073     }
4074     while (idx < zoomValueCount || !inserted) {
4075         float value = idx < zoomValueCount ? kZoomValues[idx] : newFactor;
4076         if (!inserted && newFactor < (value - 0.0001)) {
4077             value = newFactor;
4078         } else {
4079             idx++;
4080         }
4081         if (value > (newFactor - 0.0001) && value < (newFactor + 0.0001)) {
4082             inserted = true;
4083         }
4084         if (!inserted) {
4085             selIdx++;
4086         }
4087         const QString localizedValue = makePrettyZoomString(value);
4088         const QString i18nZoomName = i18nc("Zoom percentage value %1 will be replaced by the actual zoom factor value, so make sure you include it in your translation in order to not to break anything", "%1%", localizedValue);
4089         if (makePrettyZoomString(parseZoomString(i18nZoomName)) == localizedValue) {
4090             translated << i18nZoomName;
4091         } else {
4092             qWarning() << "Wrong translation of zoom percentage. Please file a bug";
4093             translated << QStringLiteral("%1%").arg(localizedValue);
4094         }
4095     }
4096     d->aZoom->setItems(translated);
4097 
4098     // select current item in list
4099     if (d->zoomMode == ZoomFitWidth) {
4100         selIdx = 0;
4101     } else if (d->zoomMode == ZoomFitPage) {
4102         selIdx = 1;
4103     } else if (d->zoomMode == ZoomFitAuto) {
4104         selIdx = 2;
4105     }
4106     // we have to temporarily enable the actions as otherwise we can't set a new current item
4107     d->aZoom->setEnabled(true);
4108     d->aZoom->selectableActionGroup()->setEnabled(true);
4109     d->aZoom->setCurrentItem(selIdx);
4110     d->aZoom->setEnabled(d->items.size() > 0);
4111     d->aZoom->selectableActionGroup()->setEnabled(d->items.size() > 0);
4112 }
4113 
4114 void PageView::updateViewMode(const int nr)
4115 {
4116     const QList<QAction *> actions = d->viewModeActionGroup->actions();
4117     for (QAction *action : actions) {
4118         QVariant mode_id = action->data();
4119         if (mode_id.toInt() == nr) {
4120             action->trigger();
4121         }
4122     }
4123 }
4124 
4125 void PageView::updateCursor()
4126 {
4127     const QPoint p = contentAreaPosition() + viewport()->mapFromGlobal(QCursor::pos());
4128     updateCursor(p);
4129 }
4130 
4131 void PageView::updateCursor(const QPoint p)
4132 {
4133     // reset mouse over link it will be re-set if that still valid
4134     d->mouseOverLinkObject = nullptr;
4135 
4136     // detect the underlaying page (if present)
4137     PageViewItem *pageItem = pickItemOnPoint(p.x(), p.y());
4138     QScroller::State scrollerState = d->scroller->state();
4139 
4140     if (d->annotator && d->annotator->active()) {
4141         if (pageItem || d->annotator->annotating()) {
4142             setCursor(d->annotator->cursor());
4143         } else {
4144             setCursor(Qt::ForbiddenCursor);
4145         }
4146     } else if (scrollerState == QScroller::Pressed || scrollerState == QScroller::Dragging) {
4147         setCursor(Qt::ClosedHandCursor);
4148     } else if (pageItem) {
4149         double nX = pageItem->absToPageX(p.x());
4150         double nY = pageItem->absToPageY(p.y());
4151         Qt::CursorShape cursorShapeFallback;
4152 
4153         // if over a ObjectRect (of type Link) change cursor to hand
4154         switch (d->mouseMode) {
4155         case Okular::Settings::EnumMouseMode::TextSelect:
4156             if (d->mouseTextSelecting) {
4157                 setCursor(Qt::IBeamCursor);
4158                 return;
4159             }
4160             cursorShapeFallback = Qt::IBeamCursor;
4161             break;
4162         case Okular::Settings::EnumMouseMode::Magnifier:
4163             setCursor(Qt::CrossCursor);
4164             return;
4165         case Okular::Settings::EnumMouseMode::RectSelect:
4166         case Okular::Settings::EnumMouseMode::TrimSelect:
4167             if (d->mouseSelecting) {
4168                 setCursor(Qt::CrossCursor);
4169                 return;
4170             }
4171             cursorShapeFallback = Qt::CrossCursor;
4172             break;
4173         case Okular::Settings::EnumMouseMode::Browse:
4174             d->mouseOnRect = false;
4175             if (d->mouseAnnotation->isMouseOver()) {
4176                 d->mouseOnRect = true;
4177                 setCursor(d->mouseAnnotation->cursor());
4178                 return;
4179             } else {
4180                 cursorShapeFallback = Qt::OpenHandCursor;
4181             }
4182             break;
4183         default:
4184             setCursor(Qt::ArrowCursor);
4185             return;
4186         }
4187 
4188         const Okular::ObjectRect *linkobj = pageItem->page()->objectRect(Okular::ObjectRect::Action, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight());
4189         if (linkobj) {
4190             d->mouseOverLinkObject = linkobj;
4191             d->mouseOnRect = true;
4192             setCursor(Qt::PointingHandCursor);
4193         } else {
4194             setCursor(cursorShapeFallback);
4195         }
4196     } else {
4197         // if there's no page over the cursor and we were showing the pointingHandCursor
4198         // go back to the normal one
4199         d->mouseOnRect = false;
4200         setCursor(Qt::ArrowCursor);
4201     }
4202 }
4203 
4204 void PageView::reloadForms()
4205 {
4206     if (d->m_formsVisible) {
4207         for (PageViewItem *item : std::as_const(d->visibleItems)) {
4208             item->reloadFormWidgetsState();
4209         }
4210     }
4211 }
4212 
4213 void PageView::moveMagnifier(const QPoint p) // non scaled point
4214 {
4215     const int w = d->magnifierView->width() * 0.5;
4216     const int h = d->magnifierView->height() * 0.5;
4217 
4218     int x = p.x() - w;
4219     int y = p.y() - h;
4220 
4221     const int max_x = viewport()->width();
4222     const int max_y = viewport()->height();
4223 
4224     QPoint scroll(0, 0);
4225 
4226     if (x < 0) {
4227         if (horizontalScrollBar()->value() > 0) {
4228             scroll.setX(x - w);
4229         }
4230         x = 0;
4231     }
4232 
4233     if (y < 0) {
4234         if (verticalScrollBar()->value() > 0) {
4235             scroll.setY(y - h);
4236         }
4237         y = 0;
4238     }
4239 
4240     if (p.x() + w > max_x) {
4241         if (horizontalScrollBar()->value() < horizontalScrollBar()->maximum()) {
4242             scroll.setX(p.x() + 2 * w - max_x);
4243         }
4244         x = max_x - d->magnifierView->width() - 1;
4245     }
4246 
4247     if (p.y() + h > max_y) {
4248         if (verticalScrollBar()->value() < verticalScrollBar()->maximum()) {
4249             scroll.setY(p.y() + 2 * h - max_y);
4250         }
4251         y = max_y - d->magnifierView->height() - 1;
4252     }
4253 
4254     if (!scroll.isNull()) {
4255         scrollPosIntoView(contentAreaPoint(p + scroll));
4256     }
4257 
4258     d->magnifierView->move(x, y);
4259 }
4260 
4261 void PageView::updateMagnifier(const QPoint p) // scaled point
4262 {
4263     /* translate mouse coordinates to page coordinates and inform the magnifier of the situation */
4264     PageViewItem *item = pickItemOnPoint(p.x(), p.y());
4265     if (item) {
4266         Okular::NormalizedPoint np(item->absToPageX(p.x()), item->absToPageY(p.y()));
4267         d->magnifierView->updateView(np, item->page());
4268     }
4269 }
4270 
4271 int PageView::viewColumns() const
4272 {
4273     int vm = Okular::Settings::viewMode();
4274     if (vm == Okular::Settings::EnumViewMode::Single) {
4275         return 1;
4276     } else if (vm == Okular::Settings::EnumViewMode::Facing || vm == Okular::Settings::EnumViewMode::FacingFirstCentered) {
4277         return 2;
4278     } else if (vm == Okular::Settings::EnumViewMode::Summary && d->document->pages() < Okular::Settings::viewColumns()) {
4279         return d->document->pages();
4280     } else {
4281         return Okular::Settings::viewColumns();
4282     }
4283 }
4284 
4285 void PageView::center(int cx, int cy, bool smoothMove)
4286 {
4287     scrollTo(cx - viewport()->width() / 2, cy - viewport()->height() / 2, smoothMove);
4288 }
4289 
4290 void PageView::scrollTo(int x, int y, bool smoothMove)
4291 {
4292     bool prevState = d->blockPixmapsRequest;
4293 
4294     int newValue = -1;
4295     if (x != horizontalScrollBar()->value() || y != verticalScrollBar()->value()) {
4296         newValue = 1; // Pretend this call is the result of a scrollbar event
4297     }
4298 
4299     d->blockPixmapsRequest = true;
4300 
4301     if (smoothMove) {
4302         d->scroller->scrollTo(QPoint(x, y), d->currentLongScrollDuration);
4303     } else {
4304         d->scroller->scrollTo(QPoint(x, y), 0);
4305     }
4306 
4307     d->blockPixmapsRequest = prevState;
4308 
4309     slotRequestVisiblePixmaps(newValue);
4310 }
4311 
4312 void PageView::toggleFormWidgets(bool on)
4313 {
4314     bool somehadfocus = false;
4315     for (PageViewItem *item : std::as_const(d->items)) {
4316         const bool hadfocus = item->setFormWidgetsVisible(on);
4317         somehadfocus = somehadfocus || hadfocus;
4318     }
4319     if (somehadfocus) {
4320         setFocus();
4321     }
4322     d->m_formsVisible = on;
4323 }
4324 
4325 void PageView::resizeContentArea(const QSize newSize)
4326 {
4327     const QSize vs = viewport()->size();
4328     int hRange = newSize.width() - vs.width();
4329     int vRange = newSize.height() - vs.height();
4330     if (horizontalScrollBar()->isVisible() && hRange == verticalScrollBar()->width() && verticalScrollBar()->isVisible() && vRange == horizontalScrollBar()->height() && Okular::Settings::showScrollBars()) {
4331         hRange = 0;
4332         vRange = 0;
4333     }
4334     horizontalScrollBar()->setRange(0, hRange);
4335     verticalScrollBar()->setRange(0, vRange);
4336     updatePageStep();
4337 }
4338 
4339 void PageView::updatePageStep()
4340 {
4341     const QSize vs = viewport()->size();
4342     horizontalScrollBar()->setPageStep(vs.width());
4343     verticalScrollBar()->setPageStep(vs.height() * (100 - Okular::Settings::scrollOverlap()) / 100);
4344 }
4345 
4346 void PageView::addWebShortcutsMenu(QMenu *menu, const QString &text)
4347 {
4348     if (text.isEmpty()) {
4349         return;
4350     }
4351 
4352     QString searchText = text;
4353     searchText = searchText.replace(QLatin1Char('\n'), QLatin1Char(' ')).replace(QLatin1Char('\r'), QLatin1Char(' ')).simplified();
4354 
4355     if (searchText.isEmpty()) {
4356         return;
4357     }
4358 
4359     KUriFilterData filterData(searchText);
4360 
4361     filterData.setSearchFilteringOptions(KUriFilterData::RetrievePreferredSearchProvidersOnly);
4362 
4363     if (KUriFilter::self()->filterSearchUri(filterData, KUriFilter::NormalTextFilter)) {
4364         const QStringList searchProviders = filterData.preferredSearchProviders();
4365 
4366         if (!searchProviders.isEmpty()) {
4367             QMenu *webShortcutsMenu = new QMenu(menu);
4368             webShortcutsMenu->setIcon(QIcon::fromTheme(QStringLiteral("preferences-web-browser-shortcuts")));
4369 
4370             const QString squeezedText = KStringHandler::rsqueeze(searchText, searchTextPreviewLength);
4371             webShortcutsMenu->setTitle(i18n("Search for '%1' with", squeezedText));
4372 
4373             QAction *action = nullptr;
4374 
4375             for (const QString &searchProvider : searchProviders) {
4376                 action = new QAction(searchProvider, webShortcutsMenu);
4377                 action->setIcon(QIcon::fromTheme(filterData.iconNameForPreferredSearchProvider(searchProvider)));
4378                 action->setData(filterData.queryForPreferredSearchProvider(searchProvider));
4379                 connect(action, &QAction::triggered, this, &PageView::slotHandleWebShortcutAction);
4380                 webShortcutsMenu->addAction(action);
4381             }
4382 
4383             webShortcutsMenu->addSeparator();
4384 
4385             action = new QAction(i18n("Configure Web Shortcuts..."), webShortcutsMenu);
4386             action->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
4387             connect(action, &QAction::triggered, this, &PageView::slotConfigureWebShortcuts);
4388             webShortcutsMenu->addAction(action);
4389 
4390             menu->addMenu(webShortcutsMenu);
4391         }
4392     }
4393 }
4394 
4395 QMenu *PageView::createProcessLinkMenu(PageViewItem *item, const QPoint eventPos)
4396 {
4397     // check if the right-click was over a link
4398     const double nX = item->absToPageX(eventPos.x());
4399     const double nY = item->absToPageY(eventPos.y());
4400     const Okular::ObjectRect *rect = item->page()->objectRect(Okular::ObjectRect::Action, nX, nY, item->uncroppedWidth(), item->uncroppedHeight());
4401     if (rect) {
4402         const Okular::Action *link = static_cast<const Okular::Action *>(rect->object());
4403 
4404         if (!link) {
4405             return nullptr;
4406         }
4407 
4408         QMenu *menu = new QMenu(this);
4409 
4410         // creating the menu and its actions
4411         QAction *processLink = menu->addAction(i18n("Follow This Link"));
4412         processLink->setObjectName(QStringLiteral("ProcessLinkAction"));
4413         if (link->actionType() == Okular::Action::Sound) {
4414             processLink->setText(i18n("Play this Sound"));
4415             if (Okular::AudioPlayer::instance()->state() == Okular::AudioPlayer::PlayingState) {
4416                 QAction *actStopSound = menu->addAction(i18n("Stop Sound"));
4417                 connect(actStopSound, &QAction::triggered, []() { Okular::AudioPlayer::instance()->stopPlaybacks(); });
4418             }
4419         }
4420 
4421         if (dynamic_cast<const Okular::BrowseAction *>(link)) {
4422             QAction *actCopyLinkLocation = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy Link Address"));
4423             actCopyLinkLocation->setObjectName(QStringLiteral("CopyLinkLocationAction"));
4424             connect(actCopyLinkLocation, &QAction::triggered, menu, [link]() {
4425                 const Okular::BrowseAction *browseLink = static_cast<const Okular::BrowseAction *>(link);
4426                 QClipboard *cb = QApplication::clipboard();
4427                 cb->setText(browseLink->url().toDisplayString(), QClipboard::Clipboard);
4428                 if (cb->supportsSelection()) {
4429                     cb->setText(browseLink->url().toDisplayString(), QClipboard::Selection);
4430                 }
4431             });
4432         }
4433 
4434         connect(processLink, &QAction::triggered, this, [this, link]() { d->document->processAction(link); });
4435         return menu;
4436     }
4437     return nullptr;
4438 }
4439 
4440 void PageView::addSearchWithinDocumentAction(QMenu *menu, const QString &searchText)
4441 {
4442     const QString squeezedText = KStringHandler::rsqueeze(searchText, searchTextPreviewLength);
4443     QAction *action = new QAction(i18n("Search for '%1' in this document", squeezedText.simplified()), menu);
4444     action->setIcon(QIcon::fromTheme(QStringLiteral("document-preview")));
4445     connect(action, &QAction::triggered, this, [this, searchText] { Q_EMIT triggerSearch(searchText); });
4446     menu->addAction(action);
4447 }
4448 
4449 void PageView::updateSmoothScrollAnimationSpeed()
4450 {
4451     // If it's turned off in Okular's own settings, don't bother to look at the
4452     // global settings
4453     if (!Okular::Settings::smoothScrolling()) {
4454         d->currentShortScrollDuration = 0;
4455         d->currentLongScrollDuration = 0;
4456         return;
4457     }
4458 
4459     // If we are using smooth scrolling, scale the speed of the animated
4460     // transitions according to the global animation speed setting
4461     KConfigGroup kdeglobalsConfig = KConfigGroup(KSharedConfig::openConfig(), QStringLiteral("KDE"));
4462     const qreal globalAnimationScale = qMax(0.0, kdeglobalsConfig.readEntry("AnimationDurationFactor", 1.0));
4463     d->currentShortScrollDuration = d->baseShortScrollDuration * globalAnimationScale;
4464     d->currentLongScrollDuration = d->baseLongScrollDuration * globalAnimationScale;
4465 }
4466 
4467 bool PageView::getContinuousMode() const
4468 {
4469     return d->aViewContinuous ? d->aViewContinuous->isChecked() : Okular::Settings::viewContinuous();
4470 }
4471 
4472 void PageView::zoomWithFixedCenter(PageView::ZoomMode newZoomMode, QPointF zoomCenter, float newZoom)
4473 {
4474     const Okular::DocumentViewport &vp = d->document->viewport();
4475     Q_ASSERT(vp.pageNumber >= 0);
4476 
4477     // determine the page below zoom center
4478     const QPoint contentPos = contentAreaPoint(zoomCenter.toPoint());
4479     const PageViewItem *page = pickItemOnPoint(contentPos.x(), contentPos.y());
4480     const int hScrollBarMaximum = horizontalScrollBar()->maximum();
4481     const int vScrollBarMaximum = verticalScrollBar()->maximum();
4482 
4483     // if the zoom center is not over a page, use viewport page number
4484     if (!page) {
4485         page = d->items[vp.pageNumber];
4486     }
4487 
4488     const QRect beginGeometry = page->croppedGeometry();
4489 
4490     QPoint offset {beginGeometry.left(), beginGeometry.top()};
4491 
4492     const QPointF oldScroll = contentAreaPosition() - offset;
4493 
4494     d->blockPixmapsRequest = true;
4495     if (newZoom) {
4496         d->zoomFactor = newZoom;
4497     }
4498 
4499     updateZoom(newZoomMode);
4500     d->blockPixmapsRequest = false;
4501 
4502     const QRect afterGeometry = page->croppedGeometry();
4503     const double vpZoomY = (double)afterGeometry.height() / (double)beginGeometry.height();
4504     const double vpZoomX = (double)afterGeometry.width() / (double)beginGeometry.width();
4505 
4506     QPointF newScroll;
4507     // The calculation for newScroll is taken from Gwenview class Abstractimageview::setZoom
4508     newScroll.setY(vpZoomY * (oldScroll.y() + zoomCenter.y()) - (zoomCenter.y()));
4509     newScroll.setX(vpZoomX * (oldScroll.x() + zoomCenter.x()) - (zoomCenter.x()));
4510 
4511     // add the remaining scroll from the previous zoom event
4512     newScroll.setY(newScroll.y() + d->remainingScroll.y() * vpZoomY);
4513     newScroll.setX(newScroll.x() + d->remainingScroll.x() * vpZoomX);
4514 
4515     // adjust newScroll to the new margins after zooming
4516     offset = QPoint {afterGeometry.left(), afterGeometry.top()};
4517     newScroll += offset;
4518 
4519     // adjust newScroll for appear and disappear of the scrollbars
4520     if (Okular::Settings::showScrollBars()) {
4521         if (hScrollBarMaximum == 0 && horizontalScrollBar()->maximum() > 0) {
4522             newScroll.setY(newScroll.y() - (horizontalScrollBar()->height() / 2.0));
4523         }
4524 
4525         if (hScrollBarMaximum > 0 && horizontalScrollBar()->maximum() == 0) {
4526             newScroll.setY(newScroll.y() + (horizontalScrollBar()->height() / 2.0));
4527         }
4528 
4529         if (vScrollBarMaximum == 0 && verticalScrollBar()->maximum() > 0) {
4530             newScroll.setX(newScroll.x() - (verticalScrollBar()->width() / 2.0));
4531         }
4532 
4533         if (vScrollBarMaximum > 0 && verticalScrollBar()->maximum() == 0) {
4534             newScroll.setX(newScroll.x() + (verticalScrollBar()->width() / 2.0));
4535         }
4536     }
4537 
4538     const int newScrollX = std::round(newScroll.x());
4539     const int newScrollY = std::round(newScroll.y());
4540     scrollTo(newScrollX, newScrollY, false);
4541 
4542     viewport()->setUpdatesEnabled(true);
4543     viewport()->update();
4544 
4545     // test if target scroll position was reached, if not save
4546     // the difference in d->remainingScroll for later use
4547     const QPointF diffF = newScroll - contentAreaPosition();
4548     if (abs(diffF.x()) < 0.5 && abs(diffF.y()) < 0.5) {
4549         // scroll target reached set d->remainingScroll to 0.0
4550         d->remainingScroll = QPointF(0.0, 0.0);
4551     } else {
4552         d->remainingScroll = diffF;
4553     }
4554 }
4555 
4556 // BEGIN private SLOTS
4557 void PageView::slotRelayoutPages()
4558 // called by: notifySetup, viewportResizeEvent, slotViewMode, slotContinuousToggled, updateZoom
4559 {
4560     // set an empty container if we have no pages
4561     const int pageCount = d->items.count();
4562     if (pageCount < 1) {
4563         return;
4564     }
4565 
4566     int viewportWidth = viewport()->width(), viewportHeight = viewport()->height(), fullWidth = 0, fullHeight = 0;
4567 
4568     // handle the 'center first page in row' stuff
4569     const bool facing = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount > 1;
4570     const bool facingCentered = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered || (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount == 1);
4571     const bool overrideCentering = facingCentered && pageCount < 3;
4572     const bool centerFirstPage = facingCentered && !overrideCentering;
4573     const bool facingPages = facing || centerFirstPage;
4574     const bool centerLastPage = centerFirstPage && pageCount % 2 == 0;
4575     const bool continuousView = getContinuousMode();
4576     const int nCols = overrideCentering ? 1 : viewColumns();
4577     const bool singlePageViewMode = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Single;
4578 
4579     if (d->aFitWindowToPage) {
4580         d->aFitWindowToPage->setEnabled(!continuousView && singlePageViewMode);
4581     }
4582 
4583     // set all items geometry and resize contents. handle 'continuous' and 'single' modes separately
4584 
4585     PageViewItem *currentItem = d->items[qMax(0, (int)d->document->currentPage())];
4586 
4587     // Here we find out column's width and row's height to compute a table
4588     // so we can place widgets 'centered in virtual cells'.
4589     const int nRows = (int)ceil((float)(centerFirstPage ? (pageCount + nCols - 1) : pageCount) / (float)nCols);
4590 
4591     int *colWidth = new int[nCols], *rowHeight = new int[nRows], cIdx = 0, rIdx = 0;
4592     for (int i = 0; i < nCols; i++) {
4593         colWidth[i] = viewportWidth / nCols;
4594     }
4595     for (int i = 0; i < nRows; i++) {
4596         rowHeight[i] = 0;
4597     }
4598     // handle the 'centering on first row' stuff
4599     if (centerFirstPage) {
4600         cIdx += nCols - 1;
4601     }
4602 
4603     // 1) find the maximum columns width and rows height for a grid in
4604     // which each page must well-fit inside a cell
4605     for (PageViewItem *item : std::as_const(d->items)) {
4606         // update internal page size (leaving a little margin in case of Fit* modes)
4607         updateItemSize(item, colWidth[cIdx] - kcolWidthMargin, viewportHeight - krowHeightMargin);
4608         // find row's maximum height and column's max width
4609         if (item->croppedWidth() + kcolWidthMargin > colWidth[cIdx]) {
4610             colWidth[cIdx] = item->croppedWidth() + kcolWidthMargin;
4611         }
4612         if (item->croppedHeight() + krowHeightMargin > rowHeight[rIdx]) {
4613             rowHeight[rIdx] = item->croppedHeight() + krowHeightMargin;
4614         }
4615         // handle the 'centering on first row' stuff
4616         // update col/row indices
4617         if (++cIdx == nCols) {
4618             cIdx = 0;
4619             rIdx++;
4620         }
4621     }
4622 
4623     const int pageRowIdx = ((centerFirstPage ? nCols - 1 : 0) + currentItem->pageNumber()) / nCols;
4624 
4625     // 2) compute full size
4626     for (int i = 0; i < nCols; i++) {
4627         fullWidth += colWidth[i];
4628     }
4629     if (continuousView) {
4630         for (int i = 0; i < nRows; i++) {
4631             fullHeight += rowHeight[i];
4632         }
4633     } else {
4634         fullHeight = rowHeight[pageRowIdx];
4635     }
4636 
4637     // 3) arrange widgets inside cells (and refine fullHeight if needed)
4638     int insertX = 0, insertY = fullHeight < viewportHeight ? (viewportHeight - fullHeight) / 2 : 0;
4639     const int origInsertY = insertY;
4640     cIdx = 0;
4641     rIdx = 0;
4642     if (centerFirstPage) {
4643         cIdx += nCols - 1;
4644         for (int i = 0; i < cIdx; ++i) {
4645             insertX += colWidth[i];
4646         }
4647     }
4648     for (PageViewItem *item : std::as_const(d->items)) {
4649         int cWidth = colWidth[cIdx], rHeight = rowHeight[rIdx];
4650         if (continuousView || rIdx == pageRowIdx) {
4651             const bool reallyDoCenterFirst = item->pageNumber() == 0 && centerFirstPage;
4652             const bool reallyDoCenterLast = item->pageNumber() == pageCount - 1 && centerLastPage;
4653             int actualX = 0;
4654             if (reallyDoCenterFirst || reallyDoCenterLast) {
4655                 // page is centered across entire viewport
4656                 actualX = (fullWidth - item->croppedWidth()) / 2;
4657             } else if (facingPages) {
4658                 if (Okular::Settings::rtlReadingDirection()) {
4659                     // RTL reading mode
4660                     actualX = ((centerFirstPage && item->pageNumber() % 2 == 0) || (!centerFirstPage && item->pageNumber() % 2 == 1)) ? (fullWidth / 2) - item->croppedWidth() - 1 : (fullWidth / 2) + 1;
4661                 } else {
4662                     // page edges 'touch' the center of the viewport
4663                     actualX = ((centerFirstPage && item->pageNumber() % 2 == 1) || (!centerFirstPage && item->pageNumber() % 2 == 0)) ? (fullWidth / 2) - item->croppedWidth() - 1 : (fullWidth / 2) + 1;
4664                 }
4665             } else {
4666                 // page is centered within its virtual column
4667                 // actualX = insertX + (cWidth - item->croppedWidth()) / 2;
4668                 if (Okular::Settings::rtlReadingDirection()) {
4669                     actualX = fullWidth - insertX - cWidth + ((cWidth - item->croppedWidth()) / 2);
4670                 } else {
4671                     actualX = insertX + (cWidth - item->croppedWidth()) / 2;
4672                 }
4673             }
4674             item->moveTo(actualX, (continuousView ? insertY : origInsertY) + (rHeight - item->croppedHeight()) / 2);
4675             item->setVisible(true);
4676         } else {
4677             item->moveTo(0, 0);
4678             item->setVisible(false);
4679         }
4680         item->setFormWidgetsVisible(d->m_formsVisible);
4681         // advance col/row index
4682         insertX += cWidth;
4683         if (++cIdx == nCols) {
4684             cIdx = 0;
4685             rIdx++;
4686             insertX = 0;
4687             insertY += rHeight;
4688         }
4689 #ifdef PAGEVIEW_DEBUG
4690         qWarning() << "updating size for pageno" << item->pageNumber() << "cropped" << item->croppedGeometry() << "uncropped" << item->uncroppedGeometry();
4691 #endif
4692     }
4693 
4694     delete[] colWidth;
4695     delete[] rowHeight;
4696 
4697     // 3) reset dirty state
4698     d->dirtyLayout = false;
4699 
4700     // 4) update scrollview's contents size and recenter view
4701     bool wasUpdatesEnabled = viewport()->updatesEnabled();
4702     if (fullWidth != contentAreaWidth() || fullHeight != contentAreaHeight()) {
4703         const Okular::DocumentViewport vp = d->document->viewport();
4704         // disable updates and resize the viewportContents
4705         if (wasUpdatesEnabled) {
4706             viewport()->setUpdatesEnabled(false);
4707         }
4708         resizeContentArea(QSize(fullWidth, fullHeight));
4709         // restore previous viewport if defined and updates enabled
4710         if (wasUpdatesEnabled && !d->pinchZoomActive) {
4711             if (vp.pageNumber >= 0) {
4712                 int prevX = horizontalScrollBar()->value(), prevY = verticalScrollBar()->value();
4713 
4714                 const QPoint centerPos = viewportToContentArea(vp);
4715                 center(centerPos.x(), centerPos.y());
4716 
4717                 // center() usually moves the viewport, that requests pixmaps too.
4718                 // if that doesn't happen we have to request them by hand
4719                 if (prevX == horizontalScrollBar()->value() && prevY == verticalScrollBar()->value()) {
4720                     slotRequestVisiblePixmaps();
4721                 }
4722             }
4723             // or else go to center page
4724             else {
4725                 center(fullWidth / 2, 0);
4726             }
4727             viewport()->setUpdatesEnabled(true);
4728         }
4729     } else {
4730         slotRequestVisiblePixmaps();
4731     }
4732 
4733     // 5) update the whole viewport if updated enabled
4734     if (wasUpdatesEnabled && !d->pinchZoomActive) {
4735         viewport()->update();
4736     }
4737 }
4738 
4739 void PageView::delayedResizeEvent()
4740 {
4741     // If we already got here we don't need to execute the timer slot again
4742     d->delayResizeEventTimer->stop();
4743     slotRelayoutPages();
4744     slotRequestVisiblePixmaps();
4745 }
4746 
4747 static void slotRequestPreloadPixmap(PageView *pageView, const PageViewItem *i, const QRect expandedViewportRect, QList<Okular::PixmapRequest *> *requestedPixmaps)
4748 {
4749     Okular::NormalizedRect preRenderRegion;
4750     const QRect intersectionRect = expandedViewportRect.intersected(i->croppedGeometry());
4751     if (!intersectionRect.isEmpty()) {
4752         preRenderRegion = Okular::NormalizedRect(intersectionRect.translated(-i->uncroppedGeometry().topLeft()), i->uncroppedWidth(), i->uncroppedHeight());
4753     }
4754 
4755     // request the pixmap if not already present
4756     if (!i->page()->hasPixmap(pageView, i->uncroppedWidth(), i->uncroppedHeight(), preRenderRegion) && i->uncroppedWidth() > 0) {
4757         Okular::PixmapRequest::PixmapRequestFeatures requestFeatures = Okular::PixmapRequest::Preload;
4758         requestFeatures |= Okular::PixmapRequest::Asynchronous;
4759         const bool pageHasTilesManager = i->page()->hasTilesManager(pageView);
4760         if (pageHasTilesManager && !preRenderRegion.isNull()) {
4761             Okular::PixmapRequest *p = new Okular::PixmapRequest(pageView, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), pageView->devicePixelRatioF(), PAGEVIEW_PRELOAD_PRIO, requestFeatures);
4762             requestedPixmaps->push_back(p);
4763 
4764             p->setNormalizedRect(preRenderRegion);
4765             p->setTile(true);
4766         } else if (!pageHasTilesManager) {
4767             Okular::PixmapRequest *p = new Okular::PixmapRequest(pageView, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), pageView->devicePixelRatioF(), PAGEVIEW_PRELOAD_PRIO, requestFeatures);
4768             requestedPixmaps->push_back(p);
4769             p->setNormalizedRect(preRenderRegion);
4770         }
4771     }
4772 }
4773 
4774 void PageView::slotRequestVisiblePixmaps(int newValue)
4775 {
4776     // if requests are blocked (because raised by an unwanted event), exit
4777     if (d->blockPixmapsRequest) {
4778         return;
4779     }
4780 
4781     // precalc view limits for intersecting with page coords inside the loop
4782     const bool isEvent = newValue != -1 && !d->blockViewport;
4783     const QRectF viewportRect(horizontalScrollBar()->value(), verticalScrollBar()->value(), viewport()->width(), viewport()->height());
4784     const QRectF viewportRectAtZeroZero(0, 0, viewport()->width(), viewport()->height());
4785 
4786     // some variables used to determine the viewport
4787     int nearPageNumber = -1;
4788     const double viewportCenterX = (viewportRect.left() + viewportRect.right()) / 2.0;
4789     const double viewportCenterY = (viewportRect.top() + viewportRect.bottom()) / 2.0;
4790     double focusedX = 0.5, focusedY = 0.0, minDistance = -1.0;
4791     // Margin (in pixels) around the viewport to preload
4792     const int pixelsToExpand = 512;
4793 
4794     // iterate over all items
4795     d->visibleItems.clear();
4796     QList<Okular::PixmapRequest *> requestedPixmaps;
4797     QVector<Okular::VisiblePageRect *> visibleRects;
4798     for (PageViewItem *i : std::as_const(d->items)) {
4799         const QSet<FormWidgetIface *> formWidgetsList = i->formWidgets();
4800         for (FormWidgetIface *fwi : formWidgetsList) {
4801             Okular::NormalizedRect r = fwi->rect();
4802             fwi->moveTo(qRound(i->uncroppedGeometry().left() + i->uncroppedWidth() * r.left) + 1 - viewportRect.left(), qRound(i->uncroppedGeometry().top() + i->uncroppedHeight() * r.top) + 1 - viewportRect.top());
4803         }
4804         const QHash<Okular::Movie *, VideoWidget *> videoWidgets = i->videoWidgets();
4805         for (VideoWidget *vw : videoWidgets) {
4806             const Okular::NormalizedRect r = vw->normGeometry();
4807             vw->move(qRound(i->uncroppedGeometry().left() + i->uncroppedWidth() * r.left) + 1 - viewportRect.left(), qRound(i->uncroppedGeometry().top() + i->uncroppedHeight() * r.top) + 1 - viewportRect.top());
4808 
4809             if (vw->isPlaying() && viewportRectAtZeroZero.intersected(vw->geometry()).isEmpty()) {
4810                 vw->stop();
4811                 vw->pageLeft();
4812             }
4813         }
4814 
4815         if (!i->isVisible()) {
4816             continue;
4817         }
4818 #ifdef PAGEVIEW_DEBUG
4819         qWarning() << "checking page" << i->pageNumber();
4820         qWarning().nospace() << "viewportRect is " << viewportRect << ", page item is " << i->croppedGeometry() << " intersect : " << viewportRect.intersects(i->croppedGeometry());
4821 #endif
4822         // if the item doesn't intersect the viewport, skip it
4823         QRectF intersectionRect = viewportRect.intersected(i->croppedGeometry());
4824         if (intersectionRect.isEmpty()) {
4825             continue;
4826         }
4827 
4828         // add the item to the 'visible list'
4829         d->visibleItems.push_back(i);
4830 
4831         intersectionRect.translate(-i->uncroppedGeometry().topLeft());
4832         const Okular::NormalizedRect normRect(intersectionRect.left() / i->uncroppedWidth(), intersectionRect.top() / i->uncroppedHeight(), intersectionRect.right() / i->uncroppedWidth(), intersectionRect.bottom() / i->uncroppedHeight());
4833 
4834         Okular::VisiblePageRect *vItem = new Okular::VisiblePageRect(i->pageNumber(), normRect);
4835         visibleRects.push_back(vItem);
4836 #ifdef PAGEVIEW_DEBUG
4837         qWarning() << "checking for pixmap for page" << i->pageNumber() << "=" << i->page()->hasPixmap(this, i->uncroppedWidth(), i->uncroppedHeight());
4838         qWarning() << "checking for text for page" << i->pageNumber() << "=" << i->page()->hasTextPage();
4839 #endif
4840 
4841         Okular::NormalizedRect expandedVisibleRect = vItem->rect;
4842         if (i->page()->hasTilesManager(this) && Okular::Settings::memoryLevel() != Okular::Settings::EnumMemoryLevel::Low) {
4843             double rectMargin = pixelsToExpand / (double)i->uncroppedHeight();
4844             expandedVisibleRect.left = qMax(0.0, vItem->rect.left - rectMargin);
4845             expandedVisibleRect.top = qMax(0.0, vItem->rect.top - rectMargin);
4846             expandedVisibleRect.right = qMin(1.0, vItem->rect.right + rectMargin);
4847             expandedVisibleRect.bottom = qMin(1.0, vItem->rect.bottom + rectMargin);
4848         }
4849 
4850         // if the item has not the right pixmap, add a request for it
4851         if (!i->page()->hasPixmap(this, i->uncroppedWidth(), i->uncroppedHeight(), expandedVisibleRect)) {
4852 #ifdef PAGEVIEW_DEBUG
4853             qWarning() << "rerequesting visible pixmaps for page" << i->pageNumber() << "!";
4854 #endif
4855             Okular::PixmapRequest *p = new Okular::PixmapRequest(this, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), devicePixelRatioF(), PAGEVIEW_PRIO, Okular::PixmapRequest::Asynchronous);
4856             requestedPixmaps.push_back(p);
4857 
4858             if (i->page()->hasTilesManager(this)) {
4859                 p->setNormalizedRect(expandedVisibleRect);
4860                 p->setTile(true);
4861             } else {
4862                 p->setNormalizedRect(vItem->rect);
4863             }
4864         }
4865 
4866         // look for the item closest to viewport center and the relative
4867         // position between the item and the viewport center
4868         if (isEvent) {
4869             const QRect &geometry = i->croppedGeometry();
4870             // compute distance between item center and viewport center (slightly moved left)
4871             const double distance = hypot((geometry.left() + geometry.right()) / 2.0 - (viewportCenterX - 4), (geometry.top() + geometry.bottom()) / 2.0 - viewportCenterY);
4872             if (distance >= minDistance && nearPageNumber != -1) {
4873                 continue;
4874             }
4875             nearPageNumber = i->pageNumber();
4876             minDistance = distance;
4877             if (geometry.height() > 0 && geometry.width() > 0) {
4878                 // Compute normalized coordinates w.r.t. cropped page
4879                 focusedX = (viewportCenterX - (double)geometry.left()) / (double)geometry.width();
4880                 focusedY = (viewportCenterY - (double)geometry.top()) / (double)geometry.height();
4881                 // Convert to normalized coordinates w.r.t. full page (no-op if not cropped)
4882                 focusedX = i->crop().left + focusedX * i->crop().width();
4883                 focusedY = i->crop().top + focusedY * i->crop().height();
4884             }
4885         }
4886     }
4887 
4888     // if preloading is enabled, add the pages before and after in preloading
4889     if (!d->visibleItems.isEmpty() && Okular::SettingsCore::memoryLevel() != Okular::SettingsCore::EnumMemoryLevel::Low) {
4890         // as the requests are done in the order as they appear in the list,
4891         // request first the next page and then the previous
4892 
4893         int pagesToPreload = viewColumns();
4894 
4895         // if the greedy option is set, preload all pages
4896         if (Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Greedy) {
4897             pagesToPreload = d->items.count();
4898         }
4899 
4900         const QRectF adjustedViewportRect = viewportRect.adjusted(0, -pixelsToExpand, 0, pixelsToExpand);
4901         const QRect expandedViewportRect(adjustedViewportRect.x(), adjustedViewportRect.y(), adjustedViewportRect.width(), adjustedViewportRect.height());
4902 
4903         for (int j = 1; j <= pagesToPreload; j++) {
4904             // add the page after the 'visible series' in preload
4905             const int tailRequest = d->visibleItems.last()->pageNumber() + j;
4906             if (tailRequest < (int)d->items.count()) {
4907                 slotRequestPreloadPixmap(this, d->items[tailRequest], expandedViewportRect, &requestedPixmaps);
4908             }
4909 
4910             // add the page before the 'visible series' in preload
4911             const int headRequest = d->visibleItems.first()->pageNumber() - j;
4912             if (headRequest >= 0) {
4913                 slotRequestPreloadPixmap(this, d->items[headRequest], expandedViewportRect, &requestedPixmaps);
4914             }
4915 
4916             // stop if we've already reached both ends of the document
4917             if (headRequest < 0 && tailRequest >= (int)d->items.count()) {
4918                 break;
4919             }
4920         }
4921     }
4922 
4923     // send requests to the document
4924     if (!requestedPixmaps.isEmpty()) {
4925         d->document->requestPixmaps(requestedPixmaps);
4926     }
4927     // if this functions was invoked by viewport events, send update to document
4928     if (isEvent && nearPageNumber != -1) {
4929         // determine the document viewport
4930         Okular::DocumentViewport newViewport(nearPageNumber);
4931         newViewport.rePos.enabled = true;
4932         newViewport.rePos.normalizedX = focusedX;
4933         newViewport.rePos.normalizedY = focusedY;
4934         // set the viewport to other observers
4935         // do not update history if the viewport is autoscrolling
4936         d->document->setViewport(newViewport, this, false, d->scroller->state() != QScroller::Scrolling);
4937     }
4938     d->document->setVisiblePageRects(visibleRects, this);
4939 }
4940 
4941 void PageView::slotAutoScroll()
4942 {
4943     // the first time create the timer
4944     if (!d->autoScrollTimer) {
4945         d->autoScrollTimer = new QTimer(this);
4946         d->autoScrollTimer->setSingleShot(true);
4947         connect(d->autoScrollTimer, &QTimer::timeout, this, &PageView::slotAutoScroll);
4948     }
4949 
4950     // if scrollIncrement is zero, stop the timer
4951     if (!d->scrollIncrement) {
4952         d->autoScrollTimer->stop();
4953         return;
4954     }
4955 
4956     // compute delay between timer ticks and scroll amount per tick
4957     int index = abs(d->scrollIncrement) - 1; // 0..9
4958     const int scrollDelay[10] = {200, 100, 50, 30, 20, 30, 25, 20, 30, 20};
4959     const int scrollOffset[10] = {1, 1, 1, 1, 1, 2, 2, 2, 4, 4};
4960     d->autoScrollTimer->start(scrollDelay[index]);
4961     int delta = d->scrollIncrement > 0 ? scrollOffset[index] : -scrollOffset[index];
4962     d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, delta), scrollDelay[index]);
4963 }
4964 
4965 void PageView::slotDragScroll()
4966 {
4967     scrollTo(horizontalScrollBar()->value() + d->dragScrollVector.x(), verticalScrollBar()->value() + d->dragScrollVector.y());
4968     QPoint p = contentAreaPosition() + viewport()->mapFromGlobal(QCursor::pos());
4969     updateSelection(p);
4970 }
4971 
4972 void PageView::slotShowWelcome()
4973 {
4974     // show initial welcome text
4975     d->messageWindow->display(i18n("Welcome"), QString(), PageViewMessage::Info, 2000);
4976 }
4977 
4978 void PageView::slotShowSizeAllCursor()
4979 {
4980     setCursor(Qt::SizeAllCursor);
4981 }
4982 
4983 void PageView::slotHandleWebShortcutAction()
4984 {
4985     QAction *action = qobject_cast<QAction *>(sender());
4986 
4987     if (action) {
4988         KUriFilterData filterData(action->data().toString());
4989 
4990         if (KUriFilter::self()->filterSearchUri(filterData, KUriFilter::WebShortcutFilter)) {
4991             QDesktopServices::openUrl(filterData.uri());
4992         }
4993     }
4994 }
4995 
4996 void PageView::slotConfigureWebShortcuts()
4997 {
4998     auto *job = new KIO::CommandLauncherJob(QStringLiteral("kcmshell5"), QStringList() << QStringLiteral("webshortcuts"));
4999     job->start();
5000 }
5001 
5002 void PageView::slotZoom()
5003 {
5004     if (!d->aZoom->selectableActionGroup()->isEnabled()) {
5005         return;
5006     }
5007 
5008     setFocus();
5009     updateZoom(ZoomFixed);
5010 }
5011 
5012 void PageView::slotZoomIn()
5013 {
5014     updateZoom(ZoomIn);
5015 }
5016 
5017 void PageView::slotZoomOut()
5018 {
5019     updateZoom(ZoomOut);
5020 }
5021 
5022 void PageView::slotZoomActual()
5023 {
5024     updateZoom(ZoomActual);
5025 }
5026 
5027 void PageView::slotFitToWidthToggled(bool on)
5028 {
5029     if (on) {
5030         updateZoom(ZoomFitWidth);
5031     }
5032 }
5033 
5034 void PageView::slotFitToPageToggled(bool on)
5035 {
5036     if (on) {
5037         updateZoom(ZoomFitPage);
5038     }
5039 }
5040 
5041 void PageView::slotAutoFitToggled(bool on)
5042 {
5043     if (on) {
5044         updateZoom(ZoomFitAuto);
5045     }
5046 }
5047 
5048 void PageView::slotViewMode(QAction *action)
5049 {
5050     const int nr = action->data().toInt();
5051     if ((int)Okular::Settings::viewMode() != nr) {
5052         Okular::Settings::setViewMode(nr);
5053         Okular::Settings::self()->save();
5054         if (d->document->pages() > 0) {
5055             slotRelayoutPages();
5056         }
5057     }
5058 }
5059 
5060 void PageView::slotContinuousToggled()
5061 {
5062     if (d->document->pages() > 0) {
5063         slotRelayoutPages();
5064     }
5065 }
5066 
5067 void PageView::slotReadingDirectionToggled(bool leftToRight)
5068 {
5069     Okular::Settings::setRtlReadingDirection(leftToRight);
5070     Okular::Settings::self()->save();
5071 }
5072 
5073 void PageView::slotUpdateReadingDirectionAction()
5074 {
5075     d->aReadingDirection->setChecked(Okular::Settings::rtlReadingDirection());
5076 }
5077 
5078 void PageView::slotSetMouseNormal()
5079 {
5080     d->mouseMode = Okular::Settings::EnumMouseMode::Browse;
5081     Okular::Settings::setMouseMode(d->mouseMode);
5082     // hide the messageWindow
5083     d->messageWindow->hide();
5084     // force an update of the cursor
5085     updateCursor();
5086     Okular::Settings::self()->save();
5087     d->annotator->detachAnnotation();
5088 }
5089 
5090 void PageView::slotSetMouseZoom()
5091 {
5092     d->mouseMode = Okular::Settings::EnumMouseMode::Zoom;
5093     Okular::Settings::setMouseMode(d->mouseMode);
5094     // change the text in messageWindow (and show it if hidden)
5095     d->messageWindow->display(i18n("Select zooming area. Right-click to zoom out."), QString(), PageViewMessage::Info, -1);
5096     // force an update of the cursor
5097     updateCursor();
5098     Okular::Settings::self()->save();
5099     d->annotator->detachAnnotation();
5100 }
5101 
5102 void PageView::slotSetMouseMagnifier()
5103 {
5104     d->mouseMode = Okular::Settings::EnumMouseMode::Magnifier;
5105     Okular::Settings::setMouseMode(d->mouseMode);
5106     d->messageWindow->display(i18n("Click to see the magnified view."), QString());
5107 
5108     // force an update of the cursor
5109     updateCursor();
5110     Okular::Settings::self()->save();
5111     d->annotator->detachAnnotation();
5112 }
5113 
5114 void PageView::slotSetMouseSelect()
5115 {
5116     d->mouseMode = Okular::Settings::EnumMouseMode::RectSelect;
5117     Okular::Settings::setMouseMode(d->mouseMode);
5118     // change the text in messageWindow (and show it if hidden)
5119     d->messageWindow->display(i18n("Draw a rectangle around the text/graphics to copy."), QString(), PageViewMessage::Info, -1);
5120     // force an update of the cursor
5121     updateCursor();
5122     Okular::Settings::self()->save();
5123     d->annotator->detachAnnotation();
5124 }
5125 
5126 void PageView::slotSetMouseTextSelect()
5127 {
5128     d->mouseMode = Okular::Settings::EnumMouseMode::TextSelect;
5129     Okular::Settings::setMouseMode(d->mouseMode);
5130     // change the text in messageWindow (and show it if hidden)
5131     d->messageWindow->display(i18n("Select text"), QString(), PageViewMessage::Info, -1);
5132     // force an update of the cursor
5133     updateCursor();
5134     Okular::Settings::self()->save();
5135     d->annotator->detachAnnotation();
5136 }
5137 
5138 void PageView::slotSetMouseTableSelect()
5139 {
5140     d->mouseMode = Okular::Settings::EnumMouseMode::TableSelect;
5141     Okular::Settings::setMouseMode(d->mouseMode);
5142     // change the text in messageWindow (and show it if hidden)
5143     d->messageWindow->display(i18n("Draw a rectangle around the table, then click near edges to divide up; press Esc to clear."), QString(), PageViewMessage::Info, -1);
5144     // force an update of the cursor
5145     updateCursor();
5146     Okular::Settings::self()->save();
5147     d->annotator->detachAnnotation();
5148 }
5149 
5150 void PageView::showNoSigningCertificatesDialog(bool nonDateValidCerts)
5151 {
5152     if (nonDateValidCerts) {
5153         KMessageBox::information(this, i18n("All your signing certificates are either not valid yet or are past their validity date."));
5154     } else {
5155         KMessageBox::information(this,
5156                                  i18n("There are no available signing certificates.<br/>For more information, please see the section about <a href=\"%1\">Adding Digital Signatures</a> in the manual.",
5157                                       QStringLiteral("help:/okular/signatures.html#adding_digital_signatures")),
5158                                  QString(),
5159                                  QString(),
5160                                  KMessageBox::Notify | KMessageBox::AllowLink);
5161     }
5162 }
5163 
5164 Okular::Document *PageView::document() const
5165 {
5166     return d->document;
5167 }
5168 
5169 void PageView::slotSignature()
5170 {
5171     if (!d->document->isHistoryClean()) {
5172         KMessageBox::information(this, i18n("You have unsaved changes. Please save the document before signing it."));
5173         return;
5174     }
5175 
5176     const Okular::CertificateStore *certStore = d->document->certificateStore();
5177     bool userCancelled, nonDateValidCerts;
5178     const QList<Okular::CertificateInfo> &certs = certStore->signingCertificatesForNow(&userCancelled, &nonDateValidCerts);
5179     if (userCancelled) {
5180         return;
5181     }
5182 
5183     if (certs.isEmpty()) {
5184         showNoSigningCertificatesDialog(nonDateValidCerts);
5185         return;
5186     }
5187 
5188     d->messageWindow->display(i18n("Draw a rectangle to insert the signature field"), QString(), PageViewMessage::Info, -1);
5189 
5190     d->annotator->setSignatureMode(true);
5191 
5192     // force an update of the cursor
5193     updateCursor();
5194     Okular::Settings::self()->save();
5195 }
5196 
5197 void PageView::slotAutoScrollUp()
5198 {
5199     if (d->scrollIncrement < -9) {
5200         return;
5201     }
5202     d->scrollIncrement--;
5203     slotAutoScroll();
5204     setFocus();
5205 }
5206 
5207 void PageView::slotAutoScrollDown()
5208 {
5209     if (d->scrollIncrement > 9) {
5210         return;
5211     }
5212     d->scrollIncrement++;
5213     slotAutoScroll();
5214     setFocus();
5215 }
5216 
5217 void PageView::slotScrollUp(int nSteps)
5218 {
5219     if (verticalScrollBar()->value() > verticalScrollBar()->minimum()) {
5220         if (nSteps) {
5221             d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, -100 * nSteps), d->currentShortScrollDuration);
5222         } else {
5223             if (d->scroller->finalPosition().y() > verticalScrollBar()->minimum()) {
5224                 d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, -(1 - Okular::Settings::scrollOverlap() / 100.0) * viewport()->height()), d->currentLongScrollDuration);
5225             }
5226         }
5227     } else if (!getContinuousMode() && d->document->currentPage() > 0) {
5228         // Since we are in single page mode and at the top of the page, go to previous page.
5229         // setViewport() is more optimized than document->setPrevPage and then move view to bottom.
5230         Okular::DocumentViewport newViewport = d->document->viewport();
5231         newViewport.pageNumber -= viewColumns();
5232         if (newViewport.pageNumber < 0) {
5233             newViewport.pageNumber = 0;
5234         }
5235         newViewport.rePos.enabled = true;
5236         newViewport.rePos.normalizedY = 1.0;
5237         d->document->setViewport(newViewport);
5238     }
5239 }
5240 
5241 void PageView::slotScrollDown(int nSteps)
5242 {
5243     if (verticalScrollBar()->value() < verticalScrollBar()->maximum()) {
5244         if (nSteps) {
5245             d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, 100 * nSteps), d->currentShortScrollDuration);
5246         } else {
5247             if (d->scroller->finalPosition().y() < verticalScrollBar()->maximum()) {
5248                 d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, (1 - Okular::Settings::scrollOverlap() / 100.0) * viewport()->height()), d->currentLongScrollDuration);
5249             }
5250         }
5251     } else if (!getContinuousMode() && (int)d->document->currentPage() < d->items.count() - 1) {
5252         // Since we are in single page mode and at the bottom of the page, go to next page.
5253         // setViewport() is more optimized than document->setNextPage and then move view to top
5254         Okular::DocumentViewport newViewport = d->document->viewport();
5255         newViewport.pageNumber += viewColumns();
5256         if (newViewport.pageNumber >= (int)d->items.count()) {
5257             newViewport.pageNumber = d->items.count() - 1;
5258         }
5259         newViewport.rePos.enabled = true;
5260         newViewport.rePos.normalizedY = 0.0;
5261         d->document->setViewport(newViewport);
5262     }
5263 }
5264 
5265 void PageView::slotRotateClockwise()
5266 {
5267     int id = ((int)d->document->rotation() + 1) % 4;
5268     d->document->setRotation(id);
5269 }
5270 
5271 void PageView::slotRotateCounterClockwise()
5272 {
5273     int id = ((int)d->document->rotation() + 3) % 4;
5274     d->document->setRotation(id);
5275 }
5276 
5277 void PageView::slotRotateOriginal()
5278 {
5279     d->document->setRotation(0);
5280 }
5281 
5282 // Enforce mutual-exclusion between trim modes
5283 // Each mode is uniquely identified by a single value
5284 // From Okular::Settings::EnumTrimMode
5285 void PageView::updateTrimMode(int except_id)
5286 {
5287     const QList<QAction *> trimModeActions = d->aTrimMode->menu()->actions();
5288     for (QAction *trimModeAction : trimModeActions) {
5289         if (trimModeAction->data().toInt() != except_id) {
5290             trimModeAction->setChecked(false);
5291         }
5292     }
5293 }
5294 
5295 bool PageView::mouseReleaseOverLink(const Okular::ObjectRect *rect) const
5296 {
5297     if (rect) {
5298         // handle click over a link
5299         const Okular::Action *action = static_cast<const Okular::Action *>(rect->object());
5300         d->document->processAction(action);
5301         return true;
5302     }
5303     return false;
5304 }
5305 
5306 void PageView::slotTrimMarginsToggled(bool on)
5307 {
5308     if (on) { // Turn off any other Trim modes
5309         updateTrimMode(d->aTrimMargins->data().toInt());
5310     }
5311 
5312     if (Okular::Settings::trimMargins() != on) {
5313         Okular::Settings::setTrimMargins(on);
5314         Okular::Settings::self()->save();
5315         if (d->document->pages() > 0) {
5316             slotRelayoutPages();
5317             slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already!
5318         }
5319     }
5320 }
5321 
5322 void PageView::slotTrimToSelectionToggled(bool on)
5323 {
5324     if (on) { // Turn off any other Trim modes
5325         updateTrimMode(d->aTrimToSelection->data().toInt());
5326 
5327         // Change the mouse mode
5328         d->mouseMode = Okular::Settings::EnumMouseMode::TrimSelect;
5329         d->aMouseNormal->setChecked(false);
5330 
5331         // change the text in messageWindow (and show it if hidden)
5332         d->messageWindow->display(i18n("Draw a rectangle around the page area you wish to keep visible"), QString(), PageViewMessage::Info, -1);
5333         // force an update of the cursor
5334         updateCursor();
5335     } else {
5336         // toggled off while making selection
5337         if (Okular::Settings::EnumMouseMode::TrimSelect == d->mouseMode) {
5338             // clear widget selection and invalidate rect
5339             selectionClear();
5340 
5341             // When Trim selection bbox interaction is over, we should switch to another mousemode.
5342             if (d->aPrevAction) {
5343                 d->aPrevAction->trigger();
5344                 d->aPrevAction = nullptr;
5345             } else {
5346                 d->aMouseNormal->trigger();
5347             }
5348         }
5349 
5350         d->trimBoundingBox = Okular::NormalizedRect(); // invalidate box
5351         if (d->document->pages() > 0) {
5352             slotRelayoutPages();
5353             slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already!
5354         }
5355     }
5356 }
5357 
5358 void PageView::slotToggleForms()
5359 {
5360     toggleFormWidgets(!d->m_formsVisible);
5361 }
5362 
5363 void PageView::slotFormChanged(int pageNumber)
5364 {
5365     if (!d->refreshTimer) {
5366         d->refreshTimer = new QTimer(this);
5367         d->refreshTimer->setSingleShot(true);
5368         connect(d->refreshTimer, &QTimer::timeout, this, &PageView::slotRefreshPage);
5369     }
5370     d->refreshPages << pageNumber;
5371     int delay = 0;
5372     if (d->m_formsVisible) {
5373         delay = 1000;
5374     }
5375     d->refreshTimer->start(delay);
5376 }
5377 
5378 void PageView::slotRefreshPage()
5379 {
5380     for (int req : std::as_const(d->refreshPages)) {
5381         QTimer::singleShot(0, this, [this, req] { d->document->refreshPixmaps(req); });
5382     }
5383     d->refreshPages.clear();
5384 }
5385 
5386 #if HAVE_SPEECH
5387 void PageView::slotSpeakDocument()
5388 {
5389     QString text;
5390     for (const PageViewItem *item : std::as_const(d->items)) {
5391         Okular::RegularAreaRect *area = textSelectionForItem(item);
5392         text.append(item->page()->text(area));
5393         text.append(QLatin1Char('\n'));
5394         delete area;
5395     }
5396 
5397     d->tts()->say(text);
5398 }
5399 
5400 void PageView::slotSpeakCurrentPage()
5401 {
5402     const int currentPage = d->document->viewport().pageNumber;
5403 
5404     PageViewItem *item = d->items.at(currentPage);
5405     Okular::RegularAreaRect *area = textSelectionForItem(item);
5406     const QString text = item->page()->text(area);
5407     delete area;
5408 
5409     d->tts()->say(text);
5410 }
5411 
5412 void PageView::slotStopSpeaks()
5413 {
5414     if (!d->m_tts) {
5415         return;
5416     }
5417 
5418     d->m_tts->stopAllSpeechs();
5419 }
5420 
5421 void PageView::slotPauseResumeSpeech()
5422 {
5423     if (!d->m_tts) {
5424         return;
5425     }
5426 
5427     d->m_tts->pauseResumeSpeech();
5428 }
5429 
5430 #endif
5431 
5432 void PageView::slotAction(Okular::Action *action)
5433 {
5434     d->document->processAction(action);
5435 }
5436 
5437 void PageView::slotMouseUpAction(Okular::Action *action, Okular::FormField *form)
5438 {
5439     if (form && action->actionType() == Okular::Action::Script) {
5440         d->document->processFormMouseUpScripAction(action, form);
5441     } else {
5442         d->document->processAction(action);
5443     }
5444 }
5445 
5446 void PageView::externalKeyPressEvent(QKeyEvent *e)
5447 {
5448     keyPressEvent(e);
5449 }
5450 
5451 void PageView::slotProcessMovieAction(const Okular::MovieAction *action)
5452 {
5453     const Okular::MovieAnnotation *movieAnnotation = action->annotation();
5454     if (!movieAnnotation) {
5455         return;
5456     }
5457 
5458     Okular::Movie *movie = movieAnnotation->movie();
5459     if (!movie) {
5460         return;
5461     }
5462 
5463     const int currentPage = d->document->viewport().pageNumber;
5464 
5465     PageViewItem *item = d->items.at(currentPage);
5466     if (!item) {
5467         return;
5468     }
5469 
5470     VideoWidget *vw = item->videoWidgets().value(movie);
5471     if (!vw) {
5472         return;
5473     }
5474 
5475     vw->show();
5476 
5477     switch (action->operation()) {
5478     case Okular::MovieAction::Play:
5479         vw->stop();
5480         vw->play();
5481         break;
5482     case Okular::MovieAction::Stop:
5483         vw->stop();
5484         break;
5485     case Okular::MovieAction::Pause:
5486         vw->pause();
5487         break;
5488     case Okular::MovieAction::Resume:
5489         vw->play();
5490         break;
5491     };
5492 }
5493 
5494 void PageView::slotProcessRenditionAction(const Okular::RenditionAction *action)
5495 {
5496     Okular::Movie *movie = action->movie();
5497     if (!movie) {
5498         return;
5499     }
5500 
5501     const int currentPage = d->document->viewport().pageNumber;
5502 
5503     PageViewItem *item = d->items.at(currentPage);
5504     if (!item) {
5505         return;
5506     }
5507 
5508     VideoWidget *vw = item->videoWidgets().value(movie);
5509     if (!vw) {
5510         return;
5511     }
5512 
5513     if (action->operation() == Okular::RenditionAction::None) {
5514         return;
5515     }
5516 
5517     vw->show();
5518 
5519     switch (action->operation()) {
5520     case Okular::RenditionAction::Play:
5521         vw->stop();
5522         vw->play();
5523         break;
5524     case Okular::RenditionAction::Stop:
5525         vw->stop();
5526         break;
5527     case Okular::RenditionAction::Pause:
5528         vw->pause();
5529         break;
5530     case Okular::RenditionAction::Resume:
5531         vw->play();
5532         break;
5533     default:
5534         return;
5535     };
5536 }
5537 
5538 void PageView::slotFitWindowToPage()
5539 {
5540     const PageViewItem *currentPageItem = nullptr;
5541     QSize viewportSize = viewport()->size();
5542     for (const PageViewItem *pageItem : std::as_const(d->items)) {
5543         if (pageItem->isVisible()) {
5544             currentPageItem = pageItem;
5545             break;
5546         }
5547     }
5548 
5549     if (!currentPageItem) {
5550         return;
5551     }
5552 
5553     const QSize pageSize = QSize(currentPageItem->uncroppedWidth() + kcolWidthMargin, currentPageItem->uncroppedHeight() + krowHeightMargin);
5554     if (verticalScrollBar()->isVisible()) {
5555         viewportSize.setWidth(viewportSize.width() + verticalScrollBar()->width());
5556     }
5557     if (horizontalScrollBar()->isVisible()) {
5558         viewportSize.setHeight(viewportSize.height() + horizontalScrollBar()->height());
5559     }
5560     Q_EMIT fitWindowToPage(viewportSize, pageSize);
5561 }
5562 
5563 void PageView::slotSelectPage()
5564 {
5565     textSelectionClear();
5566     const int currentPage = d->document->viewport().pageNumber;
5567     PageViewItem *item = d->items.at(currentPage);
5568 
5569     if (item) {
5570         Okular::RegularAreaRect *area = textSelectionForItem(item);
5571         d->pagesWithTextSelection.insert(currentPage);
5572         d->document->setPageTextSelection(currentPage, area, palette().color(QPalette::Active, QPalette::Highlight));
5573     }
5574 }
5575 
5576 void PageView::highlightSignatureFormWidget(const Okular::FormFieldSignature *form)
5577 {
5578     QVector<PageViewItem *>::const_iterator dIt = d->items.constBegin(), dEnd = d->items.constEnd();
5579     for (; dIt != dEnd; ++dIt) {
5580         const QSet<FormWidgetIface *> fwi = (*dIt)->formWidgets();
5581         for (FormWidgetIface *fw : fwi) {
5582             if (fw->formField() == form) {
5583                 SignatureEdit *widget = static_cast<SignatureEdit *>(fw);
5584                 widget->setDummyMode(true);
5585                 QTimer::singleShot(250, this, [=] { widget->setDummyMode(false); });
5586                 return;
5587             }
5588         }
5589     }
5590 }
5591 
5592 // END private SLOTS
5593 
5594 /* kate: replace-tabs on; indent-width 4; */