File indexing completed on 2024-05-19 08:24:46

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                 delete te;
3143                 continue;
3144             }
3145 
3146             Okular::NormalizedRect wordArea = *te->area();
3147 
3148             // convert it from item coordinates to part coordinates
3149             wordArea.left -= tsp.rectInItem.left;
3150             wordArea.left /= (tsp.rectInItem.right - tsp.rectInItem.left);
3151             wordArea.right -= tsp.rectInItem.left;
3152             wordArea.right /= (tsp.rectInItem.right - tsp.rectInItem.left);
3153             wordArea.top -= tsp.rectInItem.top;
3154             wordArea.top /= (tsp.rectInItem.bottom - tsp.rectInItem.top);
3155             wordArea.bottom -= tsp.rectInItem.top;
3156             wordArea.bottom /= (tsp.rectInItem.bottom - tsp.rectInItem.top);
3157 
3158             // convert from part coordinates to table coordinates
3159             wordArea.left *= (tsp.rectInSelection.right - tsp.rectInSelection.left);
3160             wordArea.left += tsp.rectInSelection.left;
3161             wordArea.right *= (tsp.rectInSelection.right - tsp.rectInSelection.left);
3162             wordArea.right += tsp.rectInSelection.left;
3163             wordArea.top *= (tsp.rectInSelection.bottom - tsp.rectInSelection.top);
3164             wordArea.top += tsp.rectInSelection.top;
3165             wordArea.bottom *= (tsp.rectInSelection.bottom - tsp.rectInSelection.top);
3166             wordArea.bottom += tsp.rectInSelection.top;
3167 
3168             // add to the ticks arrays...
3169             colTicks.append(qMakePair(wordArea.left, +1));
3170             colTicks.append(qMakePair(wordArea.right, -1));
3171             rowTicks.append(qMakePair(wordArea.top, +1));
3172             rowTicks.append(qMakePair(wordArea.bottom, -1));
3173 
3174             delete te;
3175         }
3176     }
3177 
3178     int tally = 0;
3179 
3180     std::sort(colSelectionTicks.begin(), colSelectionTicks.end());
3181     std::sort(rowSelectionTicks.begin(), rowSelectionTicks.end());
3182 
3183     for (int i = 0; i < colSelectionTicks.length(); ++i) {
3184         tally += colSelectionTicks[i].second;
3185         if (tally == 0 && i + 1 < colSelectionTicks.length() && colSelectionTicks[i + 1].first != colSelectionTicks[i].first) {
3186             colTicks.append(qMakePair(colSelectionTicks[i].first, +1));
3187             colTicks.append(qMakePair(colSelectionTicks[i + 1].first, -1));
3188         }
3189     }
3190     Q_ASSERT(tally == 0);
3191 
3192     for (int i = 0; i < rowSelectionTicks.length(); ++i) {
3193         tally += rowSelectionTicks[i].second;
3194         if (tally == 0 && i + 1 < rowSelectionTicks.length() && rowSelectionTicks[i + 1].first != rowSelectionTicks[i].first) {
3195             rowTicks.append(qMakePair(rowSelectionTicks[i].first, +1));
3196             rowTicks.append(qMakePair(rowSelectionTicks[i + 1].first, -1));
3197         }
3198     }
3199     Q_ASSERT(tally == 0);
3200 
3201     std::sort(colTicks.begin(), colTicks.end());
3202     std::sort(rowTicks.begin(), rowTicks.end());
3203 
3204     for (int i = 0; i < colTicks.length(); ++i) {
3205         tally += colTicks[i].second;
3206         if (tally == 0 && i + 1 < colTicks.length() && colTicks[i + 1].first != colTicks[i].first) {
3207             d->tableSelectionCols.append((colTicks[i].first + colTicks[i + 1].first) / 2);
3208             d->tableDividersGuessed = true;
3209         }
3210     }
3211     Q_ASSERT(tally == 0);
3212 
3213     for (int i = 0; i < rowTicks.length(); ++i) {
3214         tally += rowTicks[i].second;
3215         if (tally == 0 && i + 1 < rowTicks.length() && rowTicks[i + 1].first != rowTicks[i].first) {
3216             d->tableSelectionRows.append((rowTicks[i].first + rowTicks[i + 1].first) / 2);
3217             d->tableDividersGuessed = true;
3218         }
3219     }
3220     Q_ASSERT(tally == 0);
3221 }
3222 
3223 void PageView::mouseDoubleClickEvent(QMouseEvent *e)
3224 {
3225     if (e->button() == Qt::LeftButton) {
3226         const QPoint eventPos = contentAreaPoint(e->pos());
3227         PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
3228         if (pageItem) {
3229             // find out normalized mouse coords inside current item
3230             double nX = pageItem->absToPageX(eventPos.x());
3231             double nY = pageItem->absToPageY(eventPos.y());
3232 
3233             if (d->mouseMode == Okular::Settings::EnumMouseMode::TextSelect) {
3234                 textSelectionClear();
3235 
3236                 Okular::RegularAreaRect *wordRect = pageItem->page()->wordAt(Okular::NormalizedPoint(nX, nY));
3237                 if (wordRect) {
3238                     // TODO words with hyphens across pages
3239                     d->document->setPageTextSelection(pageItem->pageNumber(), wordRect, palette().color(QPalette::Active, QPalette::Highlight));
3240                     d->pagesWithTextSelection << pageItem->pageNumber();
3241                     if (d->document->isAllowed(Okular::AllowCopy)) {
3242                         const QString text = d->selectedText();
3243                         if (!text.isEmpty()) {
3244                             QClipboard *cb = QApplication::clipboard();
3245                             if (cb->supportsSelection()) {
3246                                 cb->setText(text, QClipboard::Selection);
3247                             }
3248                         }
3249                     }
3250                     return;
3251                 }
3252             }
3253 
3254             const QRect &itemRect = pageItem->uncroppedGeometry();
3255             Okular::Annotation *ann = nullptr;
3256 
3257             const Okular::ObjectRect *orect = pageItem->page()->objectRect(Okular::ObjectRect::OAnnotation, nX, nY, itemRect.width(), itemRect.height());
3258             if (orect) {
3259                 ann = ((Okular::AnnotationObjectRect *)orect)->annotation();
3260             }
3261             if (ann && ann->subType() != Okular::Annotation::AWidget) {
3262                 openAnnotationWindow(ann, pageItem->pageNumber());
3263             }
3264         }
3265     }
3266 }
3267 
3268 void PageView::wheelEvent(QWheelEvent *e)
3269 {
3270     if (!d->document->isOpened()) {
3271         QAbstractScrollArea::wheelEvent(e);
3272         return;
3273     }
3274 
3275     int delta = e->angleDelta().y(), vScroll = verticalScrollBar()->value();
3276     e->accept();
3277     if ((e->modifiers() & Qt::ControlModifier) == Qt::ControlModifier) {
3278         continuousZoom(delta);
3279     } else {
3280         if (delta <= -QWheelEvent::DefaultDeltasPerStep && !getContinuousMode() && vScroll == verticalScrollBar()->maximum()) {
3281             // go to next page
3282             if ((int)d->document->currentPage() < d->items.count() - 1) {
3283                 // more optimized than document->setNextPage and then move view to top
3284                 Okular::DocumentViewport newViewport = d->document->viewport();
3285                 newViewport.pageNumber += viewColumns();
3286                 if (newViewport.pageNumber >= (int)d->items.count()) {
3287                     newViewport.pageNumber = d->items.count() - 1;
3288                 }
3289                 newViewport.rePos.enabled = true;
3290                 newViewport.rePos.normalizedY = 0.0;
3291                 d->document->setViewport(newViewport);
3292                 d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); // sync scroller with scrollbar
3293             }
3294         } else if (delta >= QWheelEvent::DefaultDeltasPerStep && !getContinuousMode() && vScroll == verticalScrollBar()->minimum()) {
3295             // go to prev page
3296             if (d->document->currentPage() > 0) {
3297                 // more optimized than document->setPrevPage and then move view to bottom
3298                 Okular::DocumentViewport newViewport = d->document->viewport();
3299                 newViewport.pageNumber -= viewColumns();
3300                 if (newViewport.pageNumber < 0) {
3301                     newViewport.pageNumber = 0;
3302                 }
3303                 newViewport.rePos.enabled = true;
3304                 newViewport.rePos.normalizedY = 1.0;
3305                 d->document->setViewport(newViewport);
3306                 d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); // sync scroller with scrollbar
3307             }
3308         } else {
3309             // When the shift key is held down, scroll ten times faster
3310             int multiplier = e->modifiers() & Qt::ShiftModifier ? 10 : 1;
3311 
3312             if (delta != 0 && delta % QWheelEvent::DefaultDeltasPerStep == 0) {
3313                 // number of scroll wheel steps Qt gives to us at the same time
3314                 int count = abs(delta / QWheelEvent::DefaultDeltasPerStep) * multiplier;
3315                 if (delta < 0) {
3316                     slotScrollDown(count);
3317                 } else {
3318                     slotScrollUp(count);
3319                 }
3320             } else {
3321                 d->scroller->scrollTo(d->scroller->finalPosition() - e->angleDelta() * multiplier, 0);
3322             }
3323         }
3324     }
3325 }
3326 
3327 bool PageView::viewportEvent(QEvent *e)
3328 {
3329     if (e->type() == QEvent::ToolTip
3330         // Show tool tips only for those modes that change the cursor
3331         // to a hand when hovering over the link.
3332         && (d->mouseMode == Okular::Settings::EnumMouseMode::Browse || d->mouseMode == Okular::Settings::EnumMouseMode::RectSelect || d->mouseMode == Okular::Settings::EnumMouseMode::TextSelect ||
3333             d->mouseMode == Okular::Settings::EnumMouseMode::TrimSelect)) {
3334         QHelpEvent *he = static_cast<QHelpEvent *>(e);
3335         if (d->mouseAnnotation->isMouseOver()) {
3336             d->mouseAnnotation->routeTooltipEvent(he);
3337         } else {
3338             const QPoint eventPos = contentAreaPoint(he->pos());
3339             PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
3340             const Okular::ObjectRect *rect = nullptr;
3341             const Okular::Action *link = nullptr;
3342             if (pageItem) {
3343                 double nX = pageItem->absToPageX(eventPos.x());
3344                 double nY = pageItem->absToPageY(eventPos.y());
3345                 rect = pageItem->page()->objectRect(Okular::ObjectRect::Action, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight());
3346                 if (rect) {
3347                     link = static_cast<const Okular::Action *>(rect->object());
3348                 }
3349             }
3350 
3351             if (link) {
3352                 QRect r = rect->boundingRect(pageItem->uncroppedWidth(), pageItem->uncroppedHeight());
3353                 r.translate(pageItem->uncroppedGeometry().topLeft());
3354                 r.translate(-contentAreaPosition());
3355                 QString tip = link->actionTip();
3356                 if (!tip.isEmpty()) {
3357                     QToolTip::showText(he->globalPos(), tip, viewport(), r);
3358                 }
3359             }
3360         }
3361         e->accept();
3362         return true;
3363     } else {
3364         // do not stop the event
3365         return QAbstractScrollArea::viewportEvent(e);
3366     }
3367 }
3368 
3369 void PageView::scrollContentsBy(int dx, int dy)
3370 {
3371     const QRect r = viewport()->rect();
3372     viewport()->scroll(dx, dy, r);
3373     // HACK manually repaint the damaged regions, as it seems some updates are missed
3374     // thus leaving artifacts around
3375     QRegion rgn(r);
3376     rgn -= rgn & r.translated(dx, dy);
3377 
3378     for (const QRect &rect : rgn) {
3379         viewport()->update(rect);
3380     }
3381 
3382     updateCursor();
3383 }
3384 // END widget events
3385 
3386 QList<Okular::RegularAreaRect *> PageView::textSelections(const QPoint start, const QPoint end, int &firstpage)
3387 {
3388     firstpage = -1;
3389     QList<Okular::RegularAreaRect *> ret;
3390     QSet<int> affectedItemsSet;
3391     QRect selectionRect = QRect(start, end).normalized();
3392     for (const PageViewItem *item : std::as_const(d->items)) {
3393         if (item->isVisible() && selectionRect.intersects(item->croppedGeometry())) {
3394             affectedItemsSet.insert(item->pageNumber());
3395         }
3396     }
3397 #ifdef PAGEVIEW_DEBUG
3398     qCDebug(OkularUiDebug) << ">>>> item selected by mouse:" << affectedItemsSet.count();
3399 #endif
3400 
3401     if (!affectedItemsSet.isEmpty()) {
3402         // is the mouse drag line the ne-sw diagonal of the selection rect?
3403         bool direction_ne_sw = start == selectionRect.topRight() || start == selectionRect.bottomLeft();
3404 
3405         int tmpmin = d->document->pages();
3406         int tmpmax = 0;
3407         for (const int p : std::as_const(affectedItemsSet)) {
3408             if (p < tmpmin) {
3409                 tmpmin = p;
3410             }
3411             if (p > tmpmax) {
3412                 tmpmax = p;
3413             }
3414         }
3415 
3416         PageViewItem *a = pickItemOnPoint((int)(direction_ne_sw ? selectionRect.right() : selectionRect.left()), (int)selectionRect.top());
3417         int min = a && (a->pageNumber() != tmpmax) ? a->pageNumber() : tmpmin;
3418         PageViewItem *b = pickItemOnPoint((int)(direction_ne_sw ? selectionRect.left() : selectionRect.right()), (int)selectionRect.bottom());
3419         int max = b && (b->pageNumber() != tmpmin) ? b->pageNumber() : tmpmax;
3420 
3421         QList<int> affectedItemsIds;
3422         for (int i = min; i <= max; ++i) {
3423             affectedItemsIds.append(i);
3424         }
3425 #ifdef PAGEVIEW_DEBUG
3426         qCDebug(OkularUiDebug) << ">>>> pages:" << affectedItemsIds;
3427 #endif
3428         firstpage = affectedItemsIds.first();
3429 
3430         if (affectedItemsIds.count() == 1) {
3431             PageViewItem *item = d->items[affectedItemsIds.first()];
3432             selectionRect.translate(-item->uncroppedGeometry().topLeft());
3433             ret.append(textSelectionForItem(item, direction_ne_sw ? selectionRect.topRight() : selectionRect.topLeft(), direction_ne_sw ? selectionRect.bottomLeft() : selectionRect.bottomRight()));
3434         } else if (affectedItemsIds.count() > 1) {
3435             // first item
3436             PageViewItem *first = d->items[affectedItemsIds.first()];
3437             QRect geom = first->croppedGeometry().intersected(selectionRect).translated(-first->uncroppedGeometry().topLeft());
3438             ret.append(textSelectionForItem(first, selectionRect.bottom() > geom.height() ? (direction_ne_sw ? geom.topRight() : geom.topLeft()) : (direction_ne_sw ? geom.bottomRight() : geom.bottomLeft()), QPoint()));
3439             // last item
3440             PageViewItem *last = d->items[affectedItemsIds.last()];
3441             geom = last->croppedGeometry().intersected(selectionRect).translated(-last->uncroppedGeometry().topLeft());
3442             // the last item needs to appended at last...
3443             Okular::RegularAreaRect *lastArea =
3444                 textSelectionForItem(last, QPoint(), selectionRect.bottom() > geom.height() ? (direction_ne_sw ? geom.bottomLeft() : geom.bottomRight()) : (direction_ne_sw ? geom.topLeft() : geom.topRight()));
3445             affectedItemsIds.removeFirst();
3446             affectedItemsIds.removeLast();
3447             // item between the two above
3448             for (const int page : std::as_const(affectedItemsIds)) {
3449                 ret.append(textSelectionForItem(d->items[page]));
3450             }
3451             ret.append(lastArea);
3452         }
3453     }
3454     return ret;
3455 }
3456 
3457 void PageView::drawDocumentOnPainter(const QRect contentsRect, QPainter *p)
3458 {
3459     QColor backColor;
3460 
3461     if (Okular::Settings::useCustomBackgroundColor()) {
3462         backColor = Okular::Settings::backgroundColor();
3463     } else {
3464         backColor = viewport()->palette().color(QPalette::Dark);
3465     }
3466 
3467     // create a region from which we'll subtract painted rects
3468     QRegion remainingArea(contentsRect);
3469 
3470     // This loop draws the actual pages
3471     // iterate over all items painting the ones intersecting contentsRect
3472     for (const PageViewItem *item : std::as_const(d->items)) {
3473         // check if a piece of the page intersects the contents rect
3474         if (!item->isVisible() || !item->croppedGeometry().intersects(contentsRect)) {
3475             continue;
3476         }
3477 
3478         // get item and item's outline geometries
3479         QRect itemGeometry = item->croppedGeometry();
3480 
3481         // move the painter to the top-left corner of the real page
3482         p->save();
3483         p->translate(itemGeometry.left(), itemGeometry.top());
3484 
3485         // draw the page using the PagePainter with all flags active
3486         if (contentsRect.intersects(itemGeometry)) {
3487             Okular::NormalizedPoint *viewPortPoint = nullptr;
3488             Okular::NormalizedPoint point(d->lastSourceLocationViewportNormalizedX, d->lastSourceLocationViewportNormalizedY);
3489             if (Okular::Settings::showSourceLocationsGraphically() && item->pageNumber() == d->lastSourceLocationViewportPageNumber) {
3490                 viewPortPoint = &point;
3491             }
3492             QRect pixmapRect = contentsRect.intersected(itemGeometry);
3493             pixmapRect.translate(-item->croppedGeometry().topLeft());
3494             PagePainter::paintCroppedPageOnPainter(p, item->page(), this, pageflags, item->uncroppedWidth(), item->uncroppedHeight(), pixmapRect, item->crop(), viewPortPoint);
3495         }
3496 
3497         // remove painted area from 'remainingArea' and restore painter
3498         remainingArea -= itemGeometry;
3499         p->restore();
3500     }
3501 
3502     // fill the visible area around the page with the background color
3503     for (const QRect &backRect : remainingArea) {
3504         p->fillRect(backRect, backColor);
3505     }
3506 
3507     // take outline and shadow into account when testing whether a repaint is necessary
3508     auto dpr = devicePixelRatioF();
3509     QRect checkRect = contentsRect;
3510     checkRect.adjust(-3, -3, 1, 1);
3511 
3512     // Method to linearly interpolate between black (=(0,0,0), omitted) and the background color
3513     auto interpolateColor = [&backColor](double t) { return QColor(t * backColor.red(), t * backColor.green(), t * backColor.blue()); };
3514 
3515     // width of the shadow in device pixels
3516     static const int shadowWidth = 2 * dpr;
3517 
3518     // iterate over all items painting a black outline and a simple bottom/right gradient
3519     for (const PageViewItem *item : std::as_const(d->items)) {
3520         // check if a piece of the page intersects the contents rect
3521         if (!item->isVisible() || !item->croppedGeometry().intersects(checkRect)) {
3522             continue;
3523         }
3524 
3525         // get item and item's outline geometries
3526         QRect itemGeometry = item->croppedGeometry();
3527 
3528         // move the painter to the top-left corner of the real page
3529         p->save();
3530         p->translate(itemGeometry.left(), itemGeometry.top());
3531 
3532         // draw the page outline (black border and bottom-right shadow)
3533         if (!itemGeometry.contains(contentsRect)) {
3534             int itemWidth = itemGeometry.width();
3535             int itemHeight = itemGeometry.height();
3536             // draw simple outline
3537             QPen pen(Qt::black);
3538             pen.setWidth(0);
3539             p->setPen(pen);
3540 
3541             QRectF outline(-1.0 / dpr, -1.0 / dpr, itemWidth + 1.0 / dpr, itemHeight + 1.0 / dpr);
3542             p->drawRect(outline);
3543 
3544             // draw bottom/right gradient
3545             for (int i = 1; i <= shadowWidth; i++) {
3546                 pen.setColor(interpolateColor(double(i) / (shadowWidth + 1)));
3547                 p->setPen(pen);
3548                 QPointF left((i - 1) / dpr, itemHeight + i / dpr);
3549                 QPointF up(itemWidth + i / dpr, (i - 1) / dpr);
3550                 QPointF corner(itemWidth + i / dpr, itemHeight + i / dpr);
3551                 p->drawLine(left, corner);
3552                 p->drawLine(up, corner);
3553             }
3554         }
3555 
3556         p->restore();
3557     }
3558 }
3559 
3560 void PageView::updateItemSize(PageViewItem *item, int colWidth, int rowHeight)
3561 {
3562     const Okular::Page *okularPage = item->page();
3563     double width = okularPage->width(), height = okularPage->height(), zoom = d->zoomFactor;
3564     Okular::NormalizedRect crop(0., 0., 1., 1.);
3565 
3566     // Handle cropping, due to either "Trim Margin" or "Trim to Selection" cases
3567     if ((Okular::Settings::trimMargins() && okularPage->isBoundingBoxKnown() && !okularPage->boundingBox().isNull()) || (d->aTrimToSelection && d->aTrimToSelection->isChecked() && !d->trimBoundingBox.isNull())) {
3568         crop = Okular::Settings::trimMargins() ? okularPage->boundingBox() : d->trimBoundingBox;
3569 
3570         // Rotate the bounding box
3571         for (int i = okularPage->rotation(); i > 0; --i) {
3572             Okular::NormalizedRect rot = crop;
3573             crop.left = 1 - rot.bottom;
3574             crop.top = rot.left;
3575             crop.right = 1 - rot.top;
3576             crop.bottom = rot.right;
3577         }
3578 
3579         // Expand the crop slightly beyond the bounding box (for Trim Margins only)
3580         if (Okular::Settings::trimMargins()) {
3581             static const double cropExpandRatio = 0.04;
3582             const double cropExpand = cropExpandRatio * ((crop.right - crop.left) + (crop.bottom - crop.top)) / 2;
3583             crop = Okular::NormalizedRect(crop.left - cropExpand, crop.top - cropExpand, crop.right + cropExpand, crop.bottom + cropExpand) & Okular::NormalizedRect(0, 0, 1, 1);
3584         }
3585 
3586         // We currently generate a larger image and then crop it, so if the
3587         // crop rect is very small the generated image is huge. Hence, we shouldn't
3588         // let the crop rect become too small.
3589         static double minCropRatio;
3590         if (Okular::Settings::trimMargins()) {
3591             // Make sure we crop by at most 50% in either dimension:
3592             minCropRatio = 0.5;
3593         } else {
3594             // Looser Constraint for "Trim Selection"
3595             minCropRatio = 0.20;
3596         }
3597         if ((crop.right - crop.left) < minCropRatio) {
3598             const double newLeft = (crop.left + crop.right) / 2 - minCropRatio / 2;
3599             crop.left = qMax(0.0, qMin(1.0 - minCropRatio, newLeft));
3600             crop.right = crop.left + minCropRatio;
3601         }
3602         if ((crop.bottom - crop.top) < minCropRatio) {
3603             const double newTop = (crop.top + crop.bottom) / 2 - minCropRatio / 2;
3604             crop.top = qMax(0.0, qMin(1.0 - minCropRatio, newTop));
3605             crop.bottom = crop.top + minCropRatio;
3606         }
3607 
3608         width *= (crop.right - crop.left);
3609         height *= (crop.bottom - crop.top);
3610 #ifdef PAGEVIEW_DEBUG
3611         qCDebug(OkularUiDebug) << "Cropped page" << okularPage->number() << "to" << crop << "width" << width << "height" << height << "by bbox" << okularPage->boundingBox();
3612 #endif
3613     }
3614 
3615     if (d->zoomMode == ZoomFixed) {
3616         width *= zoom;
3617         height *= zoom;
3618         item->setWHZC((int)width, (int)height, d->zoomFactor, crop);
3619     } else if (d->zoomMode == ZoomFitWidth) {
3620         height = (height / width) * colWidth;
3621         zoom = (double)colWidth / width;
3622         item->setWHZC(colWidth, (int)height, zoom, crop);
3623         if ((uint)item->pageNumber() == d->document->currentPage()) {
3624             d->zoomFactor = zoom;
3625         }
3626     } else if (d->zoomMode == ZoomFitPage) {
3627         const double scaleW = (double)colWidth / (double)width;
3628         const double scaleH = (double)rowHeight / (double)height;
3629         zoom = qMin(scaleW, scaleH);
3630         item->setWHZC((int)(zoom * width), (int)(zoom * height), zoom, crop);
3631         if ((uint)item->pageNumber() == d->document->currentPage()) {
3632             d->zoomFactor = zoom;
3633         }
3634     } else if (d->zoomMode == ZoomFitAuto) {
3635         const double aspectRatioRelation = 1.25; // relation between aspect ratios for "auto fit"
3636         const double uiAspect = (double)rowHeight / (double)colWidth;
3637         const double pageAspect = (double)height / (double)width;
3638         const double rel = uiAspect / pageAspect;
3639 
3640         if (!getContinuousMode() && rel > aspectRatioRelation) {
3641             // UI space is relatively much higher than the page
3642             zoom = (double)rowHeight / (double)height;
3643         } else if (rel < 1.0 / aspectRatioRelation) {
3644             // UI space is relatively much wider than the page in relation
3645             zoom = (double)colWidth / (double)width;
3646         } else {
3647             // aspect ratios of page and UI space are very similar
3648             const double scaleW = (double)colWidth / (double)width;
3649             const double scaleH = (double)rowHeight / (double)height;
3650             zoom = qMin(scaleW, scaleH);
3651         }
3652         item->setWHZC((int)(zoom * width), (int)(zoom * height), zoom, crop);
3653         if ((uint)item->pageNumber() == d->document->currentPage()) {
3654             d->zoomFactor = zoom;
3655         }
3656     }
3657 #ifndef NDEBUG
3658     else {
3659         qCDebug(OkularUiDebug) << "calling updateItemSize with unrecognized d->zoomMode!";
3660     }
3661 #endif
3662 }
3663 
3664 PageViewItem *PageView::pickItemOnPoint(int x, int y)
3665 {
3666     PageViewItem *item = nullptr;
3667     for (PageViewItem *i : std::as_const(d->visibleItems)) {
3668         const QRect &r = i->croppedGeometry();
3669         if (x < r.right() && x > r.left() && y < r.bottom()) {
3670             if (y > r.top()) {
3671                 item = i;
3672             }
3673             break;
3674         }
3675     }
3676     return item;
3677 }
3678 
3679 void PageView::textSelectionClear()
3680 {
3681     // something to clear
3682     if (!d->pagesWithTextSelection.isEmpty()) {
3683         for (const int page : std::as_const(d->pagesWithTextSelection)) {
3684             d->document->setPageTextSelection(page, nullptr, QColor());
3685         }
3686         d->pagesWithTextSelection.clear();
3687     }
3688 }
3689 
3690 void PageView::selectionStart(const QPoint pos, const QColor &color, bool /*aboveAll*/)
3691 {
3692     selectionClear();
3693     d->mouseSelecting = true;
3694     d->mouseSelectionRect.setRect(pos.x(), pos.y(), 1, 1);
3695     d->mouseSelectionColor = color;
3696     // ensures page doesn't scroll
3697     if (d->autoScrollTimer) {
3698         d->scrollIncrement = 0;
3699         d->autoScrollTimer->stop();
3700     }
3701 }
3702 
3703 void PageView::scrollPosIntoView(const QPoint pos)
3704 {
3705     // 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
3706     const int damping = 6;
3707 
3708     if (pos.x() < horizontalScrollBar()->value()) {
3709         d->dragScrollVector.setX((pos.x() - horizontalScrollBar()->value()) / damping);
3710     } else if (horizontalScrollBar()->value() + viewport()->width() < pos.x()) {
3711         d->dragScrollVector.setX((pos.x() - horizontalScrollBar()->value() - viewport()->width()) / damping);
3712     } else {
3713         d->dragScrollVector.setX(0);
3714     }
3715 
3716     if (pos.y() < verticalScrollBar()->value()) {
3717         d->dragScrollVector.setY((pos.y() - verticalScrollBar()->value()) / damping);
3718     } else if (verticalScrollBar()->value() + viewport()->height() < pos.y()) {
3719         d->dragScrollVector.setY((pos.y() - verticalScrollBar()->value() - viewport()->height()) / damping);
3720     } else {
3721         d->dragScrollVector.setY(0);
3722     }
3723 
3724     if (d->dragScrollVector != QPoint(0, 0)) {
3725         if (!d->dragScrollTimer.isActive()) {
3726             d->dragScrollTimer.start(1000 / 60); // 60 fps
3727         }
3728     } else {
3729         d->dragScrollTimer.stop();
3730     }
3731 }
3732 
3733 QPoint PageView::viewportToContentArea(const Okular::DocumentViewport &vp) const
3734 {
3735     Q_ASSERT(vp.pageNumber >= 0);
3736 
3737     const QRect &r = d->items[vp.pageNumber]->croppedGeometry();
3738     QPoint c {r.left(), r.top()};
3739 
3740     if (vp.rePos.enabled) {
3741         // Convert the coordinates of vp to normalized coordinates on the cropped page.
3742         // This is a no-op if the page isn't cropped.
3743         const Okular::NormalizedRect &crop = d->items[vp.pageNumber]->crop();
3744         const double normalized_on_crop_x = (vp.rePos.normalizedX - crop.left) / (crop.right - crop.left);
3745         const double normalized_on_crop_y = (vp.rePos.normalizedY - crop.top) / (crop.bottom - crop.top);
3746 
3747         if (vp.rePos.pos == Okular::DocumentViewport::Center) {
3748             c.rx() += qRound(normClamp(normalized_on_crop_x, 0.5) * (double)r.width());
3749             c.ry() += qRound(normClamp(normalized_on_crop_y, 0.0) * (double)r.height());
3750         } else {
3751             // TopLeft
3752             c.rx() += qRound(normClamp(normalized_on_crop_x, 0.0) * (double)r.width() + viewport()->width() / 2.0);
3753             c.ry() += qRound(normClamp(normalized_on_crop_y, 0.0) * (double)r.height() + viewport()->height() / 2.0);
3754         }
3755     } else {
3756         // exact repositioning disabled, align page top margin with viewport top border by default
3757         c.rx() += r.width() / 2;
3758         c.ry() += viewport()->height() / 2 - 10;
3759     }
3760     return c;
3761 }
3762 
3763 void PageView::updateSelection(const QPoint pos)
3764 {
3765     if (d->mouseSelecting) {
3766         scrollPosIntoView(pos);
3767         // update the selection rect
3768         QRect updateRect = d->mouseSelectionRect;
3769         d->mouseSelectionRect.setBottomLeft(pos);
3770         updateRect |= d->mouseSelectionRect;
3771         updateRect.translate(-contentAreaPosition());
3772         viewport()->update(updateRect.adjusted(-1, -2, 2, 1));
3773     } else if (d->mouseTextSelecting) {
3774         scrollPosIntoView(pos);
3775         int first = -1;
3776         const QList<Okular::RegularAreaRect *> selections = textSelections(pos, d->mouseSelectPos.toPoint(), first);
3777         QSet<int> pagesWithSelectionSet;
3778         for (int i = 0; i < selections.count(); ++i) {
3779             pagesWithSelectionSet.insert(i + first);
3780         }
3781 
3782         const QSet<int> noMoreSelectedPages = d->pagesWithTextSelection - pagesWithSelectionSet;
3783         // clear the selection from pages not selected anymore
3784         for (int p : noMoreSelectedPages) {
3785             d->document->setPageTextSelection(p, nullptr, QColor());
3786         }
3787         // set the new selection for the selected pages
3788         for (int p : std::as_const(pagesWithSelectionSet)) {
3789             d->document->setPageTextSelection(p, selections[p - first], palette().color(QPalette::Active, QPalette::Highlight));
3790         }
3791         d->pagesWithTextSelection = pagesWithSelectionSet;
3792     }
3793 }
3794 
3795 static Okular::NormalizedPoint rotateInNormRect(const QPoint rotated, const QRect rect, Okular::Rotation rotation)
3796 {
3797     Okular::NormalizedPoint ret;
3798 
3799     switch (rotation) {
3800     case Okular::Rotation0:
3801         ret = Okular::NormalizedPoint(rotated.x(), rotated.y(), rect.width(), rect.height());
3802         break;
3803     case Okular::Rotation90:
3804         ret = Okular::NormalizedPoint(rotated.y(), rect.width() - rotated.x(), rect.height(), rect.width());
3805         break;
3806     case Okular::Rotation180:
3807         ret = Okular::NormalizedPoint(rect.width() - rotated.x(), rect.height() - rotated.y(), rect.width(), rect.height());
3808         break;
3809     case Okular::Rotation270:
3810         ret = Okular::NormalizedPoint(rect.height() - rotated.y(), rotated.x(), rect.height(), rect.width());
3811         break;
3812     }
3813 
3814     return ret;
3815 }
3816 
3817 Okular::RegularAreaRect *PageView::textSelectionForItem(const PageViewItem *item, const QPoint startPoint, const QPoint endPoint)
3818 {
3819     const QRect &geometry = item->uncroppedGeometry();
3820     Okular::NormalizedPoint startCursor(0.0, 0.0);
3821     if (!startPoint.isNull()) {
3822         startCursor = rotateInNormRect(startPoint, geometry, item->page()->rotation());
3823     }
3824     Okular::NormalizedPoint endCursor(1.0, 1.0);
3825     if (!endPoint.isNull()) {
3826         endCursor = rotateInNormRect(endPoint, geometry, item->page()->rotation());
3827     }
3828     Okular::TextSelection mouseTextSelectionInfo(startCursor, endCursor);
3829 
3830     const Okular::Page *okularPage = item->page();
3831 
3832     if (!okularPage->hasTextPage()) {
3833         d->document->requestTextPage(okularPage->number());
3834     }
3835 
3836     Okular::RegularAreaRect *selectionArea = okularPage->textArea(&mouseTextSelectionInfo);
3837 #ifdef PAGEVIEW_DEBUG
3838     qCDebug(OkularUiDebug).nospace() << "text areas (" << okularPage->number() << "): " << (selectionArea ? QString::number(selectionArea->count()) : "(none)");
3839 #endif
3840     return selectionArea;
3841 }
3842 
3843 void PageView::selectionClear(const ClearMode mode)
3844 {
3845     QRect updatedRect = d->mouseSelectionRect.normalized().adjusted(-2, -2, 2, 2);
3846     d->mouseSelecting = false;
3847     d->mouseSelectionRect.setCoords(0, 0, 0, 0);
3848     d->tableSelectionCols.clear();
3849     d->tableSelectionRows.clear();
3850     d->tableDividersGuessed = false;
3851     for (const TableSelectionPart &tsp : std::as_const(d->tableSelectionParts)) {
3852         QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight());
3853         selectionPartRect.translate(tsp.item->uncroppedGeometry().topLeft());
3854         // should check whether this is on-screen here?
3855         updatedRect = updatedRect.united(selectionPartRect);
3856     }
3857     if (mode != ClearOnlyDividers) {
3858         d->tableSelectionParts.clear();
3859     }
3860     d->tableSelectionParts.clear();
3861     updatedRect.translate(-contentAreaPosition());
3862     viewport()->update(updatedRect);
3863 }
3864 
3865 // const to be used for both zoomFactorFitMode function and slotRelayoutPages.
3866 static const int kcolWidthMargin = 6;
3867 static const int krowHeightMargin = 12;
3868 
3869 double PageView::zoomFactorFitMode(ZoomMode mode)
3870 {
3871     const int pageCount = d->items.count();
3872     if (pageCount == 0) {
3873         return 0;
3874     }
3875     const bool facingCentered = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered || (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount == 1);
3876     const bool overrideCentering = facingCentered && pageCount < 3;
3877     const int nCols = overrideCentering ? 1 : viewColumns();
3878     const int colWidth = viewport()->width() / nCols - kcolWidthMargin;
3879     const double rowHeight = viewport()->height() - krowHeightMargin;
3880     const PageViewItem *currentItem = d->items[qMax(0, (int)d->document->currentPage())];
3881     // prevent segmentation fault when opening a new document;
3882     if (!currentItem) {
3883         return 0;
3884     }
3885 
3886     // We need the real width/height of the cropped page.
3887     const Okular::Page *okularPage = currentItem->page();
3888     const double width = okularPage->width() * currentItem->crop().width();
3889     const double height = okularPage->height() * currentItem->crop().height();
3890 
3891     if (mode == ZoomFitWidth) {
3892         return (double)colWidth / width;
3893     }
3894     if (mode == ZoomFitPage) {
3895         const double scaleW = (double)colWidth / (double)width;
3896         const double scaleH = (double)rowHeight / (double)height;
3897         return qMin(scaleW, scaleH);
3898     }
3899     return 0;
3900 }
3901 
3902 static double parseZoomString(QString z)
3903 {
3904     // kdelibs4 sometimes adds accelerators to actions' text directly :(
3905     z.remove(QLatin1Char('&'));
3906     z.remove(QLatin1Char('%'));
3907     return QLocale().toDouble(z) / 100.0;
3908 }
3909 
3910 static QString makePrettyZoomString(double value)
3911 {
3912     // we do not need to display 2-digit precision
3913     QString localValue(QLocale().toString(value * 100.0, 'f', 1));
3914     localValue.remove(QLocale().decimalPoint() + QLatin1Char('0'));
3915     // remove a trailing zero in numbers like 66.70
3916     if (localValue.right(1) == QLatin1String("0") && localValue.indexOf(QLocale().decimalPoint()) > -1) {
3917         localValue.chop(1);
3918     }
3919     return localValue;
3920 }
3921 
3922 void PageView::updateZoom(ZoomMode newZoomMode)
3923 {
3924     if (newZoomMode == ZoomFixed) {
3925         if (d->aZoom->currentItem() == 0) {
3926             newZoomMode = ZoomFitWidth;
3927         } else if (d->aZoom->currentItem() == 1) {
3928             newZoomMode = ZoomFitPage;
3929         } else if (d->aZoom->currentItem() == 2) {
3930             newZoomMode = ZoomFitAuto;
3931         }
3932     }
3933 
3934     float newFactor = d->zoomFactor;
3935     QAction *checkedZoomAction = nullptr;
3936     switch (newZoomMode) {
3937     case ZoomFixed: { // ZoomFixed case
3938         newFactor = parseZoomString(d->aZoom->currentText());
3939     } break;
3940     case ZoomIn:
3941     case ZoomOut: {
3942         const float zoomFactorFitWidth = zoomFactorFitMode(ZoomFitWidth);
3943         const float zoomFactorFitPage = zoomFactorFitMode(ZoomFitPage);
3944 
3945         QVector<float> zoomValue(kZoomValues.size());
3946 
3947         std::copy(kZoomValues.begin(), kZoomValues.end(), zoomValue.begin());
3948         zoomValue.append(zoomFactorFitWidth);
3949         zoomValue.append(zoomFactorFitPage);
3950         std::sort(zoomValue.begin(), zoomValue.end());
3951 
3952         QVector<float>::iterator i;
3953         if (newZoomMode == ZoomOut) {
3954             if (newFactor <= zoomValue.first()) {
3955                 return;
3956             }
3957             i = std::lower_bound(zoomValue.begin(), zoomValue.end(), newFactor) - 1;
3958         } else {
3959             if (newFactor >= zoomValue.last()) {
3960                 return;
3961             }
3962             i = std::upper_bound(zoomValue.begin(), zoomValue.end(), newFactor);
3963         }
3964         const float tmpFactor = *i;
3965         if (tmpFactor == zoomFactorFitWidth) {
3966             newZoomMode = ZoomFitWidth;
3967             checkedZoomAction = d->aZoomFitWidth;
3968         } else if (tmpFactor == zoomFactorFitPage) {
3969             newZoomMode = ZoomFitPage;
3970             checkedZoomAction = d->aZoomFitPage;
3971         } else {
3972             newFactor = tmpFactor;
3973             newZoomMode = ZoomFixed;
3974         }
3975     } break;
3976     case ZoomActual:
3977         newZoomMode = ZoomFixed;
3978         newFactor = 1.0;
3979         break;
3980     case ZoomFitWidth:
3981         checkedZoomAction = d->aZoomFitWidth;
3982         break;
3983     case ZoomFitPage:
3984         checkedZoomAction = d->aZoomFitPage;
3985         break;
3986     case ZoomFitAuto:
3987         checkedZoomAction = d->aZoomAutoFit;
3988         break;
3989     case ZoomRefreshCurrent:
3990         newZoomMode = ZoomFixed;
3991         d->zoomFactor = -1;
3992         break;
3993     }
3994     const float upperZoomLimit = d->document->supportsTiles() ? 100.0 : 4.0;
3995     if (newFactor > upperZoomLimit) {
3996         newFactor = upperZoomLimit;
3997     }
3998     if (newFactor < kZoomValues[0]) {
3999         newFactor = kZoomValues[0];
4000     }
4001 
4002     if (newZoomMode != d->zoomMode || (newZoomMode == ZoomFixed && newFactor != d->zoomFactor)) {
4003         // rebuild layout and update the whole viewport
4004         d->zoomMode = newZoomMode;
4005         d->zoomFactor = newFactor;
4006         // be sure to block updates to document's viewport
4007         bool prevState = d->blockViewport;
4008         d->blockViewport = true;
4009         slotRelayoutPages();
4010         d->blockViewport = prevState;
4011         // request pixmaps
4012         slotRequestVisiblePixmaps();
4013         // update zoom text
4014         updateZoomText();
4015         // update actions checked state
4016         if (d->aZoomFitWidth) {
4017             d->aZoomFitWidth->setChecked(checkedZoomAction == d->aZoomFitWidth);
4018             d->aZoomFitPage->setChecked(checkedZoomAction == d->aZoomFitPage);
4019             d->aZoomAutoFit->setChecked(checkedZoomAction == d->aZoomAutoFit);
4020         }
4021     } else if (newZoomMode == ZoomFixed && newFactor == d->zoomFactor) {
4022         updateZoomText();
4023     }
4024 
4025     updateZoomActionsEnabledStatus();
4026 }
4027 
4028 void PageView::updateZoomActionsEnabledStatus()
4029 {
4030     const float upperZoomLimit = d->document->supportsTiles() ? kZoomValues.back() : 4.0;
4031     const bool hasPages = d->document && d->document->pages() > 0;
4032 
4033     if (d->aZoomFitWidth) {
4034         d->aZoomFitWidth->setEnabled(hasPages);
4035     }
4036     if (d->aZoomFitPage) {
4037         d->aZoomFitPage->setEnabled(hasPages);
4038     }
4039     if (d->aZoomAutoFit) {
4040         d->aZoomAutoFit->setEnabled(hasPages);
4041     }
4042     if (d->aZoom) {
4043         d->aZoom->selectableActionGroup()->setEnabled(hasPages);
4044         d->aZoom->setEnabled(hasPages);
4045     }
4046     if (d->aZoomIn) {
4047         d->aZoomIn->setEnabled(hasPages && d->zoomFactor < upperZoomLimit - 0.001);
4048     }
4049     if (d->aZoomOut) {
4050         d->aZoomOut->setEnabled(hasPages && d->zoomFactor > (kZoomValues[0] + 0.001));
4051     }
4052     if (d->aZoomActual) {
4053         d->aZoomActual->setEnabled(hasPages && d->zoomFactor != 1.0);
4054     }
4055 }
4056 
4057 void PageView::updateZoomText()
4058 {
4059     // use current page zoom as zoomFactor if in ZoomFit/* mode
4060     if (d->zoomMode != ZoomFixed && d->items.count() > 0) {
4061         d->zoomFactor = d->items[qMax(0, (int)d->document->currentPage())]->zoomFactor();
4062     }
4063     float newFactor = d->zoomFactor;
4064     d->aZoom->removeAllActions();
4065 
4066     // add items that describe fit actions
4067     QStringList translated;
4068     translated << i18n("Fit Width") << i18n("Fit Page") << i18n("Auto Fit");
4069 
4070     // add percent items
4071     int idx = 0, selIdx = 3;
4072     bool inserted = false; // use: "d->zoomMode != ZoomFixed" to hide Fit/* zoom ratio
4073     int zoomValueCount = 11;
4074     if (d->document->supportsTiles()) {
4075         zoomValueCount = kZoomValues.size();
4076     }
4077     while (idx < zoomValueCount || !inserted) {
4078         float value = idx < zoomValueCount ? kZoomValues[idx] : newFactor;
4079         if (!inserted && newFactor < (value - 0.0001)) {
4080             value = newFactor;
4081         } else {
4082             idx++;
4083         }
4084         if (value > (newFactor - 0.0001) && value < (newFactor + 0.0001)) {
4085             inserted = true;
4086         }
4087         if (!inserted) {
4088             selIdx++;
4089         }
4090         const QString localizedValue = makePrettyZoomString(value);
4091         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);
4092         if (makePrettyZoomString(parseZoomString(i18nZoomName)) == localizedValue) {
4093             translated << i18nZoomName;
4094         } else {
4095             qWarning() << "Wrong translation of zoom percentage. Please file a bug";
4096             translated << QStringLiteral("%1%").arg(localizedValue);
4097         }
4098     }
4099     d->aZoom->setItems(translated);
4100 
4101     // select current item in list
4102     if (d->zoomMode == ZoomFitWidth) {
4103         selIdx = 0;
4104     } else if (d->zoomMode == ZoomFitPage) {
4105         selIdx = 1;
4106     } else if (d->zoomMode == ZoomFitAuto) {
4107         selIdx = 2;
4108     }
4109     // we have to temporarily enable the actions as otherwise we can't set a new current item
4110     d->aZoom->setEnabled(true);
4111     d->aZoom->selectableActionGroup()->setEnabled(true);
4112     d->aZoom->setCurrentItem(selIdx);
4113     d->aZoom->setEnabled(d->items.size() > 0);
4114     d->aZoom->selectableActionGroup()->setEnabled(d->items.size() > 0);
4115 }
4116 
4117 void PageView::updateViewMode(const int nr)
4118 {
4119     const QList<QAction *> actions = d->viewModeActionGroup->actions();
4120     for (QAction *action : actions) {
4121         QVariant mode_id = action->data();
4122         if (mode_id.toInt() == nr) {
4123             action->trigger();
4124         }
4125     }
4126 }
4127 
4128 void PageView::updateCursor()
4129 {
4130     const QPoint p = contentAreaPosition() + viewport()->mapFromGlobal(QCursor::pos());
4131     updateCursor(p);
4132 }
4133 
4134 void PageView::updateCursor(const QPoint p)
4135 {
4136     // reset mouse over link it will be re-set if that still valid
4137     d->mouseOverLinkObject = nullptr;
4138 
4139     // detect the underlaying page (if present)
4140     PageViewItem *pageItem = pickItemOnPoint(p.x(), p.y());
4141     QScroller::State scrollerState = d->scroller->state();
4142 
4143     if (d->annotator && d->annotator->active()) {
4144         if (pageItem || d->annotator->annotating()) {
4145             setCursor(d->annotator->cursor());
4146         } else {
4147             setCursor(Qt::ForbiddenCursor);
4148         }
4149     } else if (scrollerState == QScroller::Pressed || scrollerState == QScroller::Dragging) {
4150         setCursor(Qt::ClosedHandCursor);
4151     } else if (pageItem) {
4152         double nX = pageItem->absToPageX(p.x());
4153         double nY = pageItem->absToPageY(p.y());
4154         Qt::CursorShape cursorShapeFallback;
4155 
4156         // if over a ObjectRect (of type Link) change cursor to hand
4157         switch (d->mouseMode) {
4158         case Okular::Settings::EnumMouseMode::TextSelect:
4159             if (d->mouseTextSelecting) {
4160                 setCursor(Qt::IBeamCursor);
4161                 return;
4162             }
4163             cursorShapeFallback = Qt::IBeamCursor;
4164             break;
4165         case Okular::Settings::EnumMouseMode::Magnifier:
4166             setCursor(Qt::CrossCursor);
4167             return;
4168         case Okular::Settings::EnumMouseMode::RectSelect:
4169         case Okular::Settings::EnumMouseMode::TrimSelect:
4170             if (d->mouseSelecting) {
4171                 setCursor(Qt::CrossCursor);
4172                 return;
4173             }
4174             cursorShapeFallback = Qt::CrossCursor;
4175             break;
4176         case Okular::Settings::EnumMouseMode::Browse:
4177             d->mouseOnRect = false;
4178             if (d->mouseAnnotation->isMouseOver()) {
4179                 d->mouseOnRect = true;
4180                 setCursor(d->mouseAnnotation->cursor());
4181                 return;
4182             } else {
4183                 cursorShapeFallback = Qt::OpenHandCursor;
4184             }
4185             break;
4186         default:
4187             setCursor(Qt::ArrowCursor);
4188             return;
4189         }
4190 
4191         const Okular::ObjectRect *linkobj = pageItem->page()->objectRect(Okular::ObjectRect::Action, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight());
4192         if (linkobj) {
4193             d->mouseOverLinkObject = linkobj;
4194             d->mouseOnRect = true;
4195             setCursor(Qt::PointingHandCursor);
4196         } else {
4197             setCursor(cursorShapeFallback);
4198         }
4199     } else {
4200         // if there's no page over the cursor and we were showing the pointingHandCursor
4201         // go back to the normal one
4202         d->mouseOnRect = false;
4203         setCursor(Qt::ArrowCursor);
4204     }
4205 }
4206 
4207 void PageView::reloadForms()
4208 {
4209     if (d->m_formsVisible) {
4210         for (PageViewItem *item : std::as_const(d->visibleItems)) {
4211             item->reloadFormWidgetsState();
4212         }
4213     }
4214 }
4215 
4216 void PageView::moveMagnifier(const QPoint p) // non scaled point
4217 {
4218     const int w = d->magnifierView->width() * 0.5;
4219     const int h = d->magnifierView->height() * 0.5;
4220 
4221     int x = p.x() - w;
4222     int y = p.y() - h;
4223 
4224     const int max_x = viewport()->width();
4225     const int max_y = viewport()->height();
4226 
4227     QPoint scroll(0, 0);
4228 
4229     if (x < 0) {
4230         if (horizontalScrollBar()->value() > 0) {
4231             scroll.setX(x - w);
4232         }
4233         x = 0;
4234     }
4235 
4236     if (y < 0) {
4237         if (verticalScrollBar()->value() > 0) {
4238             scroll.setY(y - h);
4239         }
4240         y = 0;
4241     }
4242 
4243     if (p.x() + w > max_x) {
4244         if (horizontalScrollBar()->value() < horizontalScrollBar()->maximum()) {
4245             scroll.setX(p.x() + 2 * w - max_x);
4246         }
4247         x = max_x - d->magnifierView->width() - 1;
4248     }
4249 
4250     if (p.y() + h > max_y) {
4251         if (verticalScrollBar()->value() < verticalScrollBar()->maximum()) {
4252             scroll.setY(p.y() + 2 * h - max_y);
4253         }
4254         y = max_y - d->magnifierView->height() - 1;
4255     }
4256 
4257     if (!scroll.isNull()) {
4258         scrollPosIntoView(contentAreaPoint(p + scroll));
4259     }
4260 
4261     d->magnifierView->move(x, y);
4262 }
4263 
4264 void PageView::updateMagnifier(const QPoint p) // scaled point
4265 {
4266     /* translate mouse coordinates to page coordinates and inform the magnifier of the situation */
4267     PageViewItem *item = pickItemOnPoint(p.x(), p.y());
4268     if (item) {
4269         Okular::NormalizedPoint np(item->absToPageX(p.x()), item->absToPageY(p.y()));
4270         d->magnifierView->updateView(np, item->page());
4271     }
4272 }
4273 
4274 int PageView::viewColumns() const
4275 {
4276     int vm = Okular::Settings::viewMode();
4277     if (vm == Okular::Settings::EnumViewMode::Single) {
4278         return 1;
4279     } else if (vm == Okular::Settings::EnumViewMode::Facing || vm == Okular::Settings::EnumViewMode::FacingFirstCentered) {
4280         return 2;
4281     } else if (vm == Okular::Settings::EnumViewMode::Summary && d->document->pages() < Okular::Settings::viewColumns()) {
4282         return d->document->pages();
4283     } else {
4284         return Okular::Settings::viewColumns();
4285     }
4286 }
4287 
4288 void PageView::center(int cx, int cy, bool smoothMove)
4289 {
4290     scrollTo(cx - viewport()->width() / 2, cy - viewport()->height() / 2, smoothMove);
4291 }
4292 
4293 void PageView::scrollTo(int x, int y, bool smoothMove)
4294 {
4295     bool prevState = d->blockPixmapsRequest;
4296 
4297     int newValue = -1;
4298     if (x != horizontalScrollBar()->value() || y != verticalScrollBar()->value()) {
4299         newValue = 1; // Pretend this call is the result of a scrollbar event
4300     }
4301 
4302     d->blockPixmapsRequest = true;
4303 
4304     if (smoothMove) {
4305         d->scroller->scrollTo(QPoint(x, y), d->currentLongScrollDuration);
4306     } else {
4307         d->scroller->scrollTo(QPoint(x, y), 0);
4308     }
4309 
4310     d->blockPixmapsRequest = prevState;
4311 
4312     slotRequestVisiblePixmaps(newValue);
4313 }
4314 
4315 void PageView::toggleFormWidgets(bool on)
4316 {
4317     bool somehadfocus = false;
4318     for (PageViewItem *item : std::as_const(d->items)) {
4319         const bool hadfocus = item->setFormWidgetsVisible(on);
4320         somehadfocus = somehadfocus || hadfocus;
4321     }
4322     if (somehadfocus) {
4323         setFocus();
4324     }
4325     d->m_formsVisible = on;
4326 }
4327 
4328 void PageView::resizeContentArea(const QSize newSize)
4329 {
4330     const QSize vs = viewport()->size();
4331     int hRange = newSize.width() - vs.width();
4332     int vRange = newSize.height() - vs.height();
4333     if (horizontalScrollBar()->isVisible() && hRange == verticalScrollBar()->width() && verticalScrollBar()->isVisible() && vRange == horizontalScrollBar()->height() && Okular::Settings::showScrollBars()) {
4334         hRange = 0;
4335         vRange = 0;
4336     }
4337     horizontalScrollBar()->setRange(0, hRange);
4338     verticalScrollBar()->setRange(0, vRange);
4339     updatePageStep();
4340 }
4341 
4342 void PageView::updatePageStep()
4343 {
4344     const QSize vs = viewport()->size();
4345     horizontalScrollBar()->setPageStep(vs.width());
4346     verticalScrollBar()->setPageStep(vs.height() * (100 - Okular::Settings::scrollOverlap()) / 100);
4347 }
4348 
4349 void PageView::addWebShortcutsMenu(QMenu *menu, const QString &text)
4350 {
4351     if (text.isEmpty()) {
4352         return;
4353     }
4354 
4355     QString searchText = text;
4356     searchText = searchText.replace(QLatin1Char('\n'), QLatin1Char(' ')).replace(QLatin1Char('\r'), QLatin1Char(' ')).simplified();
4357 
4358     if (searchText.isEmpty()) {
4359         return;
4360     }
4361 
4362     KUriFilterData filterData(searchText);
4363 
4364     filterData.setSearchFilteringOptions(KUriFilterData::RetrievePreferredSearchProvidersOnly);
4365 
4366     if (KUriFilter::self()->filterSearchUri(filterData, KUriFilter::NormalTextFilter)) {
4367         const QStringList searchProviders = filterData.preferredSearchProviders();
4368 
4369         if (!searchProviders.isEmpty()) {
4370             QMenu *webShortcutsMenu = new QMenu(menu);
4371             webShortcutsMenu->setIcon(QIcon::fromTheme(QStringLiteral("preferences-web-browser-shortcuts")));
4372 
4373             const QString squeezedText = KStringHandler::rsqueeze(searchText, searchTextPreviewLength);
4374             webShortcutsMenu->setTitle(i18n("Search for '%1' with", squeezedText));
4375 
4376             QAction *action = nullptr;
4377 
4378             for (const QString &searchProvider : searchProviders) {
4379                 action = new QAction(searchProvider, webShortcutsMenu);
4380                 action->setIcon(QIcon::fromTheme(filterData.iconNameForPreferredSearchProvider(searchProvider)));
4381                 action->setData(filterData.queryForPreferredSearchProvider(searchProvider));
4382                 connect(action, &QAction::triggered, this, &PageView::slotHandleWebShortcutAction);
4383                 webShortcutsMenu->addAction(action);
4384             }
4385 
4386             webShortcutsMenu->addSeparator();
4387 
4388             action = new QAction(i18n("Configure Web Shortcuts..."), webShortcutsMenu);
4389             action->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
4390             connect(action, &QAction::triggered, this, &PageView::slotConfigureWebShortcuts);
4391             webShortcutsMenu->addAction(action);
4392 
4393             menu->addMenu(webShortcutsMenu);
4394         }
4395     }
4396 }
4397 
4398 QMenu *PageView::createProcessLinkMenu(PageViewItem *item, const QPoint eventPos)
4399 {
4400     // check if the right-click was over a link
4401     const double nX = item->absToPageX(eventPos.x());
4402     const double nY = item->absToPageY(eventPos.y());
4403     const Okular::ObjectRect *rect = item->page()->objectRect(Okular::ObjectRect::Action, nX, nY, item->uncroppedWidth(), item->uncroppedHeight());
4404     if (rect) {
4405         const Okular::Action *link = static_cast<const Okular::Action *>(rect->object());
4406 
4407         if (!link) {
4408             return nullptr;
4409         }
4410 
4411         QMenu *menu = new QMenu(this);
4412 
4413         // creating the menu and its actions
4414         QAction *processLink = menu->addAction(i18n("Follow This Link"));
4415         processLink->setObjectName(QStringLiteral("ProcessLinkAction"));
4416         if (link->actionType() == Okular::Action::Sound) {
4417             processLink->setText(i18n("Play this Sound"));
4418             if (Okular::AudioPlayer::instance()->state() == Okular::AudioPlayer::PlayingState) {
4419                 QAction *actStopSound = menu->addAction(i18n("Stop Sound"));
4420                 connect(actStopSound, &QAction::triggered, []() { Okular::AudioPlayer::instance()->stopPlaybacks(); });
4421             }
4422         }
4423 
4424         if (dynamic_cast<const Okular::BrowseAction *>(link)) {
4425             QAction *actCopyLinkLocation = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy Link Address"));
4426             actCopyLinkLocation->setObjectName(QStringLiteral("CopyLinkLocationAction"));
4427             connect(actCopyLinkLocation, &QAction::triggered, menu, [link]() {
4428                 const Okular::BrowseAction *browseLink = static_cast<const Okular::BrowseAction *>(link);
4429                 QClipboard *cb = QApplication::clipboard();
4430                 cb->setText(browseLink->url().toDisplayString(), QClipboard::Clipboard);
4431                 if (cb->supportsSelection()) {
4432                     cb->setText(browseLink->url().toDisplayString(), QClipboard::Selection);
4433                 }
4434             });
4435         }
4436 
4437         connect(processLink, &QAction::triggered, this, [this, link]() { d->document->processAction(link); });
4438         return menu;
4439     }
4440     return nullptr;
4441 }
4442 
4443 void PageView::addSearchWithinDocumentAction(QMenu *menu, const QString &searchText)
4444 {
4445     const QString squeezedText = KStringHandler::rsqueeze(searchText, searchTextPreviewLength);
4446     QAction *action = new QAction(i18n("Search for '%1' in this document", squeezedText.simplified()), menu);
4447     action->setIcon(QIcon::fromTheme(QStringLiteral("document-preview")));
4448     connect(action, &QAction::triggered, this, [this, searchText] { Q_EMIT triggerSearch(searchText); });
4449     menu->addAction(action);
4450 }
4451 
4452 void PageView::updateSmoothScrollAnimationSpeed()
4453 {
4454     // If it's turned off in Okular's own settings, don't bother to look at the
4455     // global settings
4456     if (!Okular::Settings::smoothScrolling()) {
4457         d->currentShortScrollDuration = 0;
4458         d->currentLongScrollDuration = 0;
4459         return;
4460     }
4461 
4462     // If we are using smooth scrolling, scale the speed of the animated
4463     // transitions according to the global animation speed setting
4464     KConfigGroup kdeglobalsConfig = KConfigGroup(KSharedConfig::openConfig(), QStringLiteral("KDE"));
4465     const qreal globalAnimationScale = qMax(0.0, kdeglobalsConfig.readEntry("AnimationDurationFactor", 1.0));
4466     d->currentShortScrollDuration = d->baseShortScrollDuration * globalAnimationScale;
4467     d->currentLongScrollDuration = d->baseLongScrollDuration * globalAnimationScale;
4468 }
4469 
4470 bool PageView::getContinuousMode() const
4471 {
4472     return d->aViewContinuous ? d->aViewContinuous->isChecked() : Okular::Settings::viewContinuous();
4473 }
4474 
4475 void PageView::zoomWithFixedCenter(PageView::ZoomMode newZoomMode, QPointF zoomCenter, float newZoom)
4476 {
4477     const Okular::DocumentViewport &vp = d->document->viewport();
4478     Q_ASSERT(vp.pageNumber >= 0);
4479 
4480     // determine the page below zoom center
4481     const QPoint contentPos = contentAreaPoint(zoomCenter.toPoint());
4482     const PageViewItem *page = pickItemOnPoint(contentPos.x(), contentPos.y());
4483     const int hScrollBarMaximum = horizontalScrollBar()->maximum();
4484     const int vScrollBarMaximum = verticalScrollBar()->maximum();
4485 
4486     // if the zoom center is not over a page, use viewport page number
4487     if (!page) {
4488         page = d->items[vp.pageNumber];
4489     }
4490 
4491     const QRect beginGeometry = page->croppedGeometry();
4492 
4493     QPoint offset {beginGeometry.left(), beginGeometry.top()};
4494 
4495     const QPointF oldScroll = contentAreaPosition() - offset;
4496 
4497     d->blockPixmapsRequest = true;
4498     if (newZoom) {
4499         d->zoomFactor = newZoom;
4500     }
4501 
4502     updateZoom(newZoomMode);
4503     d->blockPixmapsRequest = false;
4504 
4505     const QRect afterGeometry = page->croppedGeometry();
4506     const double vpZoomY = (double)afterGeometry.height() / (double)beginGeometry.height();
4507     const double vpZoomX = (double)afterGeometry.width() / (double)beginGeometry.width();
4508 
4509     QPointF newScroll;
4510     // The calculation for newScroll is taken from Gwenview class Abstractimageview::setZoom
4511     newScroll.setY(vpZoomY * (oldScroll.y() + zoomCenter.y()) - (zoomCenter.y()));
4512     newScroll.setX(vpZoomX * (oldScroll.x() + zoomCenter.x()) - (zoomCenter.x()));
4513 
4514     // add the remaining scroll from the previous zoom event
4515     newScroll.setY(newScroll.y() + d->remainingScroll.y() * vpZoomY);
4516     newScroll.setX(newScroll.x() + d->remainingScroll.x() * vpZoomX);
4517 
4518     // adjust newScroll to the new margins after zooming
4519     offset = QPoint {afterGeometry.left(), afterGeometry.top()};
4520     newScroll += offset;
4521 
4522     // adjust newScroll for appear and disappear of the scrollbars
4523     if (Okular::Settings::showScrollBars()) {
4524         if (hScrollBarMaximum == 0 && horizontalScrollBar()->maximum() > 0) {
4525             newScroll.setY(newScroll.y() - (horizontalScrollBar()->height() / 2.0));
4526         }
4527 
4528         if (hScrollBarMaximum > 0 && horizontalScrollBar()->maximum() == 0) {
4529             newScroll.setY(newScroll.y() + (horizontalScrollBar()->height() / 2.0));
4530         }
4531 
4532         if (vScrollBarMaximum == 0 && verticalScrollBar()->maximum() > 0) {
4533             newScroll.setX(newScroll.x() - (verticalScrollBar()->width() / 2.0));
4534         }
4535 
4536         if (vScrollBarMaximum > 0 && verticalScrollBar()->maximum() == 0) {
4537             newScroll.setX(newScroll.x() + (verticalScrollBar()->width() / 2.0));
4538         }
4539     }
4540 
4541     const int newScrollX = std::round(newScroll.x());
4542     const int newScrollY = std::round(newScroll.y());
4543     scrollTo(newScrollX, newScrollY, false);
4544 
4545     viewport()->setUpdatesEnabled(true);
4546     viewport()->update();
4547 
4548     // test if target scroll position was reached, if not save
4549     // the difference in d->remainingScroll for later use
4550     const QPointF diffF = newScroll - contentAreaPosition();
4551     if (abs(diffF.x()) < 0.5 && abs(diffF.y()) < 0.5) {
4552         // scroll target reached set d->remainingScroll to 0.0
4553         d->remainingScroll = QPointF(0.0, 0.0);
4554     } else {
4555         d->remainingScroll = diffF;
4556     }
4557 }
4558 
4559 // BEGIN private SLOTS
4560 void PageView::slotRelayoutPages()
4561 // called by: notifySetup, viewportResizeEvent, slotViewMode, slotContinuousToggled, updateZoom
4562 {
4563     // set an empty container if we have no pages
4564     const int pageCount = d->items.count();
4565     if (pageCount < 1) {
4566         return;
4567     }
4568 
4569     int viewportWidth = viewport()->width(), viewportHeight = viewport()->height(), fullWidth = 0, fullHeight = 0;
4570 
4571     // handle the 'center first page in row' stuff
4572     const bool facing = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount > 1;
4573     const bool facingCentered = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered || (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount == 1);
4574     const bool overrideCentering = facingCentered && pageCount < 3;
4575     const bool centerFirstPage = facingCentered && !overrideCentering;
4576     const bool facingPages = facing || centerFirstPage;
4577     const bool centerLastPage = centerFirstPage && pageCount % 2 == 0;
4578     const bool continuousView = getContinuousMode();
4579     const int nCols = overrideCentering ? 1 : viewColumns();
4580     const bool singlePageViewMode = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Single;
4581 
4582     if (d->aFitWindowToPage) {
4583         d->aFitWindowToPage->setEnabled(!continuousView && singlePageViewMode);
4584     }
4585 
4586     // set all items geometry and resize contents. handle 'continuous' and 'single' modes separately
4587 
4588     PageViewItem *currentItem = d->items[qMax(0, (int)d->document->currentPage())];
4589 
4590     // Here we find out column's width and row's height to compute a table
4591     // so we can place widgets 'centered in virtual cells'.
4592     const int nRows = (int)ceil((float)(centerFirstPage ? (pageCount + nCols - 1) : pageCount) / (float)nCols);
4593 
4594     int *colWidth = new int[nCols], *rowHeight = new int[nRows], cIdx = 0, rIdx = 0;
4595     for (int i = 0; i < nCols; i++) {
4596         colWidth[i] = viewportWidth / nCols;
4597     }
4598     for (int i = 0; i < nRows; i++) {
4599         rowHeight[i] = 0;
4600     }
4601     // handle the 'centering on first row' stuff
4602     if (centerFirstPage) {
4603         cIdx += nCols - 1;
4604     }
4605 
4606     // 1) find the maximum columns width and rows height for a grid in
4607     // which each page must well-fit inside a cell
4608     for (PageViewItem *item : std::as_const(d->items)) {
4609         // update internal page size (leaving a little margin in case of Fit* modes)
4610         updateItemSize(item, colWidth[cIdx] - kcolWidthMargin, viewportHeight - krowHeightMargin);
4611         // find row's maximum height and column's max width
4612         if (item->croppedWidth() + kcolWidthMargin > colWidth[cIdx]) {
4613             colWidth[cIdx] = item->croppedWidth() + kcolWidthMargin;
4614         }
4615         if (item->croppedHeight() + krowHeightMargin > rowHeight[rIdx]) {
4616             rowHeight[rIdx] = item->croppedHeight() + krowHeightMargin;
4617         }
4618         // handle the 'centering on first row' stuff
4619         // update col/row indices
4620         if (++cIdx == nCols) {
4621             cIdx = 0;
4622             rIdx++;
4623         }
4624     }
4625 
4626     const int pageRowIdx = ((centerFirstPage ? nCols - 1 : 0) + currentItem->pageNumber()) / nCols;
4627 
4628     // 2) compute full size
4629     for (int i = 0; i < nCols; i++) {
4630         fullWidth += colWidth[i];
4631     }
4632     if (continuousView) {
4633         for (int i = 0; i < nRows; i++) {
4634             fullHeight += rowHeight[i];
4635         }
4636     } else {
4637         fullHeight = rowHeight[pageRowIdx];
4638     }
4639 
4640     // 3) arrange widgets inside cells (and refine fullHeight if needed)
4641     int insertX = 0, insertY = fullHeight < viewportHeight ? (viewportHeight - fullHeight) / 2 : 0;
4642     const int origInsertY = insertY;
4643     cIdx = 0;
4644     rIdx = 0;
4645     if (centerFirstPage) {
4646         cIdx += nCols - 1;
4647         for (int i = 0; i < cIdx; ++i) {
4648             insertX += colWidth[i];
4649         }
4650     }
4651     for (PageViewItem *item : std::as_const(d->items)) {
4652         int cWidth = colWidth[cIdx], rHeight = rowHeight[rIdx];
4653         if (continuousView || rIdx == pageRowIdx) {
4654             const bool reallyDoCenterFirst = item->pageNumber() == 0 && centerFirstPage;
4655             const bool reallyDoCenterLast = item->pageNumber() == pageCount - 1 && centerLastPage;
4656             int actualX = 0;
4657             if (reallyDoCenterFirst || reallyDoCenterLast) {
4658                 // page is centered across entire viewport
4659                 actualX = (fullWidth - item->croppedWidth()) / 2;
4660             } else if (facingPages) {
4661                 if (Okular::Settings::rtlReadingDirection()) {
4662                     // RTL reading mode
4663                     actualX = ((centerFirstPage && item->pageNumber() % 2 == 0) || (!centerFirstPage && item->pageNumber() % 2 == 1)) ? (fullWidth / 2) - item->croppedWidth() - 1 : (fullWidth / 2) + 1;
4664                 } else {
4665                     // page edges 'touch' the center of the viewport
4666                     actualX = ((centerFirstPage && item->pageNumber() % 2 == 1) || (!centerFirstPage && item->pageNumber() % 2 == 0)) ? (fullWidth / 2) - item->croppedWidth() - 1 : (fullWidth / 2) + 1;
4667                 }
4668             } else {
4669                 // page is centered within its virtual column
4670                 // actualX = insertX + (cWidth - item->croppedWidth()) / 2;
4671                 if (Okular::Settings::rtlReadingDirection()) {
4672                     actualX = fullWidth - insertX - cWidth + ((cWidth - item->croppedWidth()) / 2);
4673                 } else {
4674                     actualX = insertX + (cWidth - item->croppedWidth()) / 2;
4675                 }
4676             }
4677             item->moveTo(actualX, (continuousView ? insertY : origInsertY) + (rHeight - item->croppedHeight()) / 2);
4678             item->setVisible(true);
4679         } else {
4680             item->moveTo(0, 0);
4681             item->setVisible(false);
4682         }
4683         item->setFormWidgetsVisible(d->m_formsVisible);
4684         // advance col/row index
4685         insertX += cWidth;
4686         if (++cIdx == nCols) {
4687             cIdx = 0;
4688             rIdx++;
4689             insertX = 0;
4690             insertY += rHeight;
4691         }
4692 #ifdef PAGEVIEW_DEBUG
4693         qWarning() << "updating size for pageno" << item->pageNumber() << "cropped" << item->croppedGeometry() << "uncropped" << item->uncroppedGeometry();
4694 #endif
4695     }
4696 
4697     delete[] colWidth;
4698     delete[] rowHeight;
4699 
4700     // 3) reset dirty state
4701     d->dirtyLayout = false;
4702 
4703     // 4) update scrollview's contents size and recenter view
4704     bool wasUpdatesEnabled = viewport()->updatesEnabled();
4705     if (fullWidth != contentAreaWidth() || fullHeight != contentAreaHeight()) {
4706         const Okular::DocumentViewport vp = d->document->viewport();
4707         // disable updates and resize the viewportContents
4708         if (wasUpdatesEnabled) {
4709             viewport()->setUpdatesEnabled(false);
4710         }
4711         resizeContentArea(QSize(fullWidth, fullHeight));
4712         // restore previous viewport if defined and updates enabled
4713         if (wasUpdatesEnabled && !d->pinchZoomActive) {
4714             if (vp.pageNumber >= 0) {
4715                 int prevX = horizontalScrollBar()->value(), prevY = verticalScrollBar()->value();
4716 
4717                 const QPoint centerPos = viewportToContentArea(vp);
4718                 center(centerPos.x(), centerPos.y());
4719 
4720                 // center() usually moves the viewport, that requests pixmaps too.
4721                 // if that doesn't happen we have to request them by hand
4722                 if (prevX == horizontalScrollBar()->value() && prevY == verticalScrollBar()->value()) {
4723                     slotRequestVisiblePixmaps();
4724                 }
4725             }
4726             // or else go to center page
4727             else {
4728                 center(fullWidth / 2, 0);
4729             }
4730             viewport()->setUpdatesEnabled(true);
4731         }
4732     } else {
4733         slotRequestVisiblePixmaps();
4734     }
4735 
4736     // 5) update the whole viewport if updated enabled
4737     if (wasUpdatesEnabled && !d->pinchZoomActive) {
4738         viewport()->update();
4739     }
4740 }
4741 
4742 void PageView::delayedResizeEvent()
4743 {
4744     // If we already got here we don't need to execute the timer slot again
4745     d->delayResizeEventTimer->stop();
4746     slotRelayoutPages();
4747     slotRequestVisiblePixmaps();
4748 }
4749 
4750 static void slotRequestPreloadPixmap(PageView *pageView, const PageViewItem *i, const QRect expandedViewportRect, QList<Okular::PixmapRequest *> *requestedPixmaps)
4751 {
4752     Okular::NormalizedRect preRenderRegion;
4753     const QRect intersectionRect = expandedViewportRect.intersected(i->croppedGeometry());
4754     if (!intersectionRect.isEmpty()) {
4755         preRenderRegion = Okular::NormalizedRect(intersectionRect.translated(-i->uncroppedGeometry().topLeft()), i->uncroppedWidth(), i->uncroppedHeight());
4756     }
4757 
4758     // request the pixmap if not already present
4759     if (!i->page()->hasPixmap(pageView, i->uncroppedWidth(), i->uncroppedHeight(), preRenderRegion) && i->uncroppedWidth() > 0) {
4760         Okular::PixmapRequest::PixmapRequestFeatures requestFeatures = Okular::PixmapRequest::Preload;
4761         requestFeatures |= Okular::PixmapRequest::Asynchronous;
4762         const bool pageHasTilesManager = i->page()->hasTilesManager(pageView);
4763         if (pageHasTilesManager && !preRenderRegion.isNull()) {
4764             Okular::PixmapRequest *p = new Okular::PixmapRequest(pageView, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), pageView->devicePixelRatioF(), PAGEVIEW_PRELOAD_PRIO, requestFeatures);
4765             requestedPixmaps->push_back(p);
4766 
4767             p->setNormalizedRect(preRenderRegion);
4768             p->setTile(true);
4769         } else if (!pageHasTilesManager) {
4770             Okular::PixmapRequest *p = new Okular::PixmapRequest(pageView, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), pageView->devicePixelRatioF(), PAGEVIEW_PRELOAD_PRIO, requestFeatures);
4771             requestedPixmaps->push_back(p);
4772             p->setNormalizedRect(preRenderRegion);
4773         }
4774     }
4775 }
4776 
4777 void PageView::slotRequestVisiblePixmaps(int newValue)
4778 {
4779     // if requests are blocked (because raised by an unwanted event), exit
4780     if (d->blockPixmapsRequest) {
4781         return;
4782     }
4783 
4784     // precalc view limits for intersecting with page coords inside the loop
4785     const bool isEvent = newValue != -1 && !d->blockViewport;
4786     const QRectF viewportRect(horizontalScrollBar()->value(), verticalScrollBar()->value(), viewport()->width(), viewport()->height());
4787     const QRectF viewportRectAtZeroZero(0, 0, viewport()->width(), viewport()->height());
4788 
4789     // some variables used to determine the viewport
4790     int nearPageNumber = -1;
4791     const double viewportCenterX = (viewportRect.left() + viewportRect.right()) / 2.0;
4792     const double viewportCenterY = (viewportRect.top() + viewportRect.bottom()) / 2.0;
4793     double focusedX = 0.5, focusedY = 0.0, minDistance = -1.0;
4794     // Margin (in pixels) around the viewport to preload
4795     const int pixelsToExpand = 512;
4796 
4797     // iterate over all items
4798     d->visibleItems.clear();
4799     QList<Okular::PixmapRequest *> requestedPixmaps;
4800     QVector<Okular::VisiblePageRect *> visibleRects;
4801     for (PageViewItem *i : std::as_const(d->items)) {
4802         const QSet<FormWidgetIface *> formWidgetsList = i->formWidgets();
4803         for (FormWidgetIface *fwi : formWidgetsList) {
4804             Okular::NormalizedRect r = fwi->rect();
4805             fwi->moveTo(qRound(i->uncroppedGeometry().left() + i->uncroppedWidth() * r.left) + 1 - viewportRect.left(), qRound(i->uncroppedGeometry().top() + i->uncroppedHeight() * r.top) + 1 - viewportRect.top());
4806         }
4807         const QHash<Okular::Movie *, VideoWidget *> videoWidgets = i->videoWidgets();
4808         for (VideoWidget *vw : videoWidgets) {
4809             const Okular::NormalizedRect r = vw->normGeometry();
4810             vw->move(qRound(i->uncroppedGeometry().left() + i->uncroppedWidth() * r.left) + 1 - viewportRect.left(), qRound(i->uncroppedGeometry().top() + i->uncroppedHeight() * r.top) + 1 - viewportRect.top());
4811 
4812             if (vw->isPlaying() && viewportRectAtZeroZero.intersected(vw->geometry()).isEmpty()) {
4813                 vw->stop();
4814                 vw->pageLeft();
4815             }
4816         }
4817 
4818         if (!i->isVisible()) {
4819             continue;
4820         }
4821 #ifdef PAGEVIEW_DEBUG
4822         qWarning() << "checking page" << i->pageNumber();
4823         qWarning().nospace() << "viewportRect is " << viewportRect << ", page item is " << i->croppedGeometry() << " intersect : " << viewportRect.intersects(i->croppedGeometry());
4824 #endif
4825         // if the item doesn't intersect the viewport, skip it
4826         QRectF intersectionRect = viewportRect.intersected(i->croppedGeometry());
4827         if (intersectionRect.isEmpty()) {
4828             continue;
4829         }
4830 
4831         // add the item to the 'visible list'
4832         d->visibleItems.push_back(i);
4833 
4834         intersectionRect.translate(-i->uncroppedGeometry().topLeft());
4835         const Okular::NormalizedRect normRect(intersectionRect.left() / i->uncroppedWidth(), intersectionRect.top() / i->uncroppedHeight(), intersectionRect.right() / i->uncroppedWidth(), intersectionRect.bottom() / i->uncroppedHeight());
4836 
4837         Okular::VisiblePageRect *vItem = new Okular::VisiblePageRect(i->pageNumber(), normRect);
4838         visibleRects.push_back(vItem);
4839 #ifdef PAGEVIEW_DEBUG
4840         qWarning() << "checking for pixmap for page" << i->pageNumber() << "=" << i->page()->hasPixmap(this, i->uncroppedWidth(), i->uncroppedHeight());
4841         qWarning() << "checking for text for page" << i->pageNumber() << "=" << i->page()->hasTextPage();
4842 #endif
4843 
4844         Okular::NormalizedRect expandedVisibleRect = vItem->rect;
4845         if (i->page()->hasTilesManager(this) && Okular::Settings::memoryLevel() != Okular::Settings::EnumMemoryLevel::Low) {
4846             double rectMargin = pixelsToExpand / (double)i->uncroppedHeight();
4847             expandedVisibleRect.left = qMax(0.0, vItem->rect.left - rectMargin);
4848             expandedVisibleRect.top = qMax(0.0, vItem->rect.top - rectMargin);
4849             expandedVisibleRect.right = qMin(1.0, vItem->rect.right + rectMargin);
4850             expandedVisibleRect.bottom = qMin(1.0, vItem->rect.bottom + rectMargin);
4851         }
4852 
4853         // if the item has not the right pixmap, add a request for it
4854         if (!i->page()->hasPixmap(this, i->uncroppedWidth(), i->uncroppedHeight(), expandedVisibleRect)) {
4855 #ifdef PAGEVIEW_DEBUG
4856             qWarning() << "rerequesting visible pixmaps for page" << i->pageNumber() << "!";
4857 #endif
4858             Okular::PixmapRequest *p = new Okular::PixmapRequest(this, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), devicePixelRatioF(), PAGEVIEW_PRIO, Okular::PixmapRequest::Asynchronous);
4859             requestedPixmaps.push_back(p);
4860 
4861             if (i->page()->hasTilesManager(this)) {
4862                 p->setNormalizedRect(expandedVisibleRect);
4863                 p->setTile(true);
4864             } else {
4865                 p->setNormalizedRect(vItem->rect);
4866             }
4867         }
4868 
4869         // look for the item closest to viewport center and the relative
4870         // position between the item and the viewport center
4871         if (isEvent) {
4872             const QRect &geometry = i->croppedGeometry();
4873             // compute distance between item center and viewport center (slightly moved left)
4874             const double distance = hypot((geometry.left() + geometry.right()) / 2.0 - (viewportCenterX - 4), (geometry.top() + geometry.bottom()) / 2.0 - viewportCenterY);
4875             if (distance >= minDistance && nearPageNumber != -1) {
4876                 continue;
4877             }
4878             nearPageNumber = i->pageNumber();
4879             minDistance = distance;
4880             if (geometry.height() > 0 && geometry.width() > 0) {
4881                 // Compute normalized coordinates w.r.t. cropped page
4882                 focusedX = (viewportCenterX - (double)geometry.left()) / (double)geometry.width();
4883                 focusedY = (viewportCenterY - (double)geometry.top()) / (double)geometry.height();
4884                 // Convert to normalized coordinates w.r.t. full page (no-op if not cropped)
4885                 focusedX = i->crop().left + focusedX * i->crop().width();
4886                 focusedY = i->crop().top + focusedY * i->crop().height();
4887             }
4888         }
4889     }
4890 
4891     // if preloading is enabled, add the pages before and after in preloading
4892     if (!d->visibleItems.isEmpty() && Okular::SettingsCore::memoryLevel() != Okular::SettingsCore::EnumMemoryLevel::Low) {
4893         // as the requests are done in the order as they appear in the list,
4894         // request first the next page and then the previous
4895 
4896         int pagesToPreload = viewColumns();
4897 
4898         // if the greedy option is set, preload all pages
4899         if (Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Greedy) {
4900             pagesToPreload = d->items.count();
4901         }
4902 
4903         const QRectF adjustedViewportRect = viewportRect.adjusted(0, -pixelsToExpand, 0, pixelsToExpand);
4904         const QRect expandedViewportRect(adjustedViewportRect.x(), adjustedViewportRect.y(), adjustedViewportRect.width(), adjustedViewportRect.height());
4905 
4906         for (int j = 1; j <= pagesToPreload; j++) {
4907             // add the page after the 'visible series' in preload
4908             const int tailRequest = d->visibleItems.last()->pageNumber() + j;
4909             if (tailRequest < (int)d->items.count()) {
4910                 slotRequestPreloadPixmap(this, d->items[tailRequest], expandedViewportRect, &requestedPixmaps);
4911             }
4912 
4913             // add the page before the 'visible series' in preload
4914             const int headRequest = d->visibleItems.first()->pageNumber() - j;
4915             if (headRequest >= 0) {
4916                 slotRequestPreloadPixmap(this, d->items[headRequest], expandedViewportRect, &requestedPixmaps);
4917             }
4918 
4919             // stop if we've already reached both ends of the document
4920             if (headRequest < 0 && tailRequest >= (int)d->items.count()) {
4921                 break;
4922             }
4923         }
4924     }
4925 
4926     // send requests to the document
4927     if (!requestedPixmaps.isEmpty()) {
4928         d->document->requestPixmaps(requestedPixmaps);
4929     }
4930     // if this functions was invoked by viewport events, send update to document
4931     if (isEvent && nearPageNumber != -1) {
4932         // determine the document viewport
4933         Okular::DocumentViewport newViewport(nearPageNumber);
4934         newViewport.rePos.enabled = true;
4935         newViewport.rePos.normalizedX = focusedX;
4936         newViewport.rePos.normalizedY = focusedY;
4937         // set the viewport to other observers
4938         // do not update history if the viewport is autoscrolling
4939         d->document->setViewport(newViewport, this, false, d->scroller->state() != QScroller::Scrolling);
4940     }
4941     d->document->setVisiblePageRects(visibleRects, this);
4942 }
4943 
4944 void PageView::slotAutoScroll()
4945 {
4946     // the first time create the timer
4947     if (!d->autoScrollTimer) {
4948         d->autoScrollTimer = new QTimer(this);
4949         d->autoScrollTimer->setSingleShot(true);
4950         connect(d->autoScrollTimer, &QTimer::timeout, this, &PageView::slotAutoScroll);
4951     }
4952 
4953     // if scrollIncrement is zero, stop the timer
4954     if (!d->scrollIncrement) {
4955         d->autoScrollTimer->stop();
4956         return;
4957     }
4958 
4959     // compute delay between timer ticks and scroll amount per tick
4960     int index = abs(d->scrollIncrement) - 1; // 0..9
4961     const int scrollDelay[10] = {200, 100, 50, 30, 20, 30, 25, 20, 30, 20};
4962     const int scrollOffset[10] = {1, 1, 1, 1, 1, 2, 2, 2, 4, 4};
4963     d->autoScrollTimer->start(scrollDelay[index]);
4964     int delta = d->scrollIncrement > 0 ? scrollOffset[index] : -scrollOffset[index];
4965     d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, delta), scrollDelay[index]);
4966 }
4967 
4968 void PageView::slotDragScroll()
4969 {
4970     scrollTo(horizontalScrollBar()->value() + d->dragScrollVector.x(), verticalScrollBar()->value() + d->dragScrollVector.y());
4971     QPoint p = contentAreaPosition() + viewport()->mapFromGlobal(QCursor::pos());
4972     updateSelection(p);
4973 }
4974 
4975 void PageView::slotShowWelcome()
4976 {
4977     // show initial welcome text
4978     d->messageWindow->display(i18n("Welcome"), QString(), PageViewMessage::Info, 2000);
4979 }
4980 
4981 void PageView::slotShowSizeAllCursor()
4982 {
4983     setCursor(Qt::SizeAllCursor);
4984 }
4985 
4986 void PageView::slotHandleWebShortcutAction()
4987 {
4988     QAction *action = qobject_cast<QAction *>(sender());
4989 
4990     if (action) {
4991         KUriFilterData filterData(action->data().toString());
4992 
4993         if (KUriFilter::self()->filterSearchUri(filterData, KUriFilter::WebShortcutFilter)) {
4994             QDesktopServices::openUrl(filterData.uri());
4995         }
4996     }
4997 }
4998 
4999 void PageView::slotConfigureWebShortcuts()
5000 {
5001     auto *job = new KIO::CommandLauncherJob(QStringLiteral("kcmshell5"), QStringList() << QStringLiteral("webshortcuts"));
5002     job->start();
5003 }
5004 
5005 void PageView::slotZoom()
5006 {
5007     if (!d->aZoom->selectableActionGroup()->isEnabled()) {
5008         return;
5009     }
5010 
5011     setFocus();
5012     updateZoom(ZoomFixed);
5013 }
5014 
5015 void PageView::slotZoomIn()
5016 {
5017     updateZoom(ZoomIn);
5018 }
5019 
5020 void PageView::slotZoomOut()
5021 {
5022     updateZoom(ZoomOut);
5023 }
5024 
5025 void PageView::slotZoomActual()
5026 {
5027     updateZoom(ZoomActual);
5028 }
5029 
5030 void PageView::slotFitToWidthToggled(bool on)
5031 {
5032     if (on) {
5033         updateZoom(ZoomFitWidth);
5034     }
5035 }
5036 
5037 void PageView::slotFitToPageToggled(bool on)
5038 {
5039     if (on) {
5040         updateZoom(ZoomFitPage);
5041     }
5042 }
5043 
5044 void PageView::slotAutoFitToggled(bool on)
5045 {
5046     if (on) {
5047         updateZoom(ZoomFitAuto);
5048     }
5049 }
5050 
5051 void PageView::slotViewMode(QAction *action)
5052 {
5053     const int nr = action->data().toInt();
5054     if ((int)Okular::Settings::viewMode() != nr) {
5055         Okular::Settings::setViewMode(nr);
5056         Okular::Settings::self()->save();
5057         if (d->document->pages() > 0) {
5058             slotRelayoutPages();
5059         }
5060     }
5061 }
5062 
5063 void PageView::slotContinuousToggled()
5064 {
5065     if (d->document->pages() > 0) {
5066         slotRelayoutPages();
5067     }
5068 }
5069 
5070 void PageView::slotReadingDirectionToggled(bool leftToRight)
5071 {
5072     Okular::Settings::setRtlReadingDirection(leftToRight);
5073     Okular::Settings::self()->save();
5074 }
5075 
5076 void PageView::slotUpdateReadingDirectionAction()
5077 {
5078     d->aReadingDirection->setChecked(Okular::Settings::rtlReadingDirection());
5079 }
5080 
5081 void PageView::slotSetMouseNormal()
5082 {
5083     d->mouseMode = Okular::Settings::EnumMouseMode::Browse;
5084     Okular::Settings::setMouseMode(d->mouseMode);
5085     // hide the messageWindow
5086     d->messageWindow->hide();
5087     // force an update of the cursor
5088     updateCursor();
5089     Okular::Settings::self()->save();
5090     d->annotator->detachAnnotation();
5091 }
5092 
5093 void PageView::slotSetMouseZoom()
5094 {
5095     d->mouseMode = Okular::Settings::EnumMouseMode::Zoom;
5096     Okular::Settings::setMouseMode(d->mouseMode);
5097     // change the text in messageWindow (and show it if hidden)
5098     d->messageWindow->display(i18n("Select zooming area. Right-click to zoom out."), QString(), PageViewMessage::Info, -1);
5099     // force an update of the cursor
5100     updateCursor();
5101     Okular::Settings::self()->save();
5102     d->annotator->detachAnnotation();
5103 }
5104 
5105 void PageView::slotSetMouseMagnifier()
5106 {
5107     d->mouseMode = Okular::Settings::EnumMouseMode::Magnifier;
5108     Okular::Settings::setMouseMode(d->mouseMode);
5109     d->messageWindow->display(i18n("Click to see the magnified view."), QString());
5110 
5111     // force an update of the cursor
5112     updateCursor();
5113     Okular::Settings::self()->save();
5114     d->annotator->detachAnnotation();
5115 }
5116 
5117 void PageView::slotSetMouseSelect()
5118 {
5119     d->mouseMode = Okular::Settings::EnumMouseMode::RectSelect;
5120     Okular::Settings::setMouseMode(d->mouseMode);
5121     // change the text in messageWindow (and show it if hidden)
5122     d->messageWindow->display(i18n("Draw a rectangle around the text/graphics to copy."), QString(), PageViewMessage::Info, -1);
5123     // force an update of the cursor
5124     updateCursor();
5125     Okular::Settings::self()->save();
5126     d->annotator->detachAnnotation();
5127 }
5128 
5129 void PageView::slotSetMouseTextSelect()
5130 {
5131     d->mouseMode = Okular::Settings::EnumMouseMode::TextSelect;
5132     Okular::Settings::setMouseMode(d->mouseMode);
5133     // change the text in messageWindow (and show it if hidden)
5134     d->messageWindow->display(i18n("Select text"), QString(), PageViewMessage::Info, -1);
5135     // force an update of the cursor
5136     updateCursor();
5137     Okular::Settings::self()->save();
5138     d->annotator->detachAnnotation();
5139 }
5140 
5141 void PageView::slotSetMouseTableSelect()
5142 {
5143     d->mouseMode = Okular::Settings::EnumMouseMode::TableSelect;
5144     Okular::Settings::setMouseMode(d->mouseMode);
5145     // change the text in messageWindow (and show it if hidden)
5146     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);
5147     // force an update of the cursor
5148     updateCursor();
5149     Okular::Settings::self()->save();
5150     d->annotator->detachAnnotation();
5151 }
5152 
5153 void PageView::showNoSigningCertificatesDialog(bool nonDateValidCerts)
5154 {
5155     if (nonDateValidCerts) {
5156         KMessageBox::information(this, i18n("All your signing certificates are either not valid yet or are past their validity date."));
5157     } else {
5158         KMessageBox::information(this,
5159                                  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.",
5160                                       QStringLiteral("help:/okular/signatures.html#adding_digital_signatures")),
5161                                  QString(),
5162                                  QString(),
5163                                  KMessageBox::Notify | KMessageBox::AllowLink);
5164     }
5165 }
5166 
5167 Okular::Document *PageView::document() const
5168 {
5169     return d->document;
5170 }
5171 
5172 void PageView::slotSignature()
5173 {
5174     if (!d->document->isHistoryClean()) {
5175         KMessageBox::information(this, i18n("You have unsaved changes. Please save the document before signing it."));
5176         return;
5177     }
5178 
5179     const Okular::CertificateStore *certStore = d->document->certificateStore();
5180     bool userCancelled, nonDateValidCerts;
5181     const QList<Okular::CertificateInfo> &certs = certStore->signingCertificatesForNow(&userCancelled, &nonDateValidCerts);
5182     if (userCancelled) {
5183         return;
5184     }
5185 
5186     if (certs.isEmpty()) {
5187         showNoSigningCertificatesDialog(nonDateValidCerts);
5188         return;
5189     }
5190 
5191     d->messageWindow->display(i18n("Draw a rectangle to insert the signature field"), QString(), PageViewMessage::Info, -1);
5192 
5193     d->annotator->setSignatureMode(true);
5194 
5195     // force an update of the cursor
5196     updateCursor();
5197     Okular::Settings::self()->save();
5198 }
5199 
5200 void PageView::slotAutoScrollUp()
5201 {
5202     if (d->scrollIncrement < -9) {
5203         return;
5204     }
5205     d->scrollIncrement--;
5206     slotAutoScroll();
5207     setFocus();
5208 }
5209 
5210 void PageView::slotAutoScrollDown()
5211 {
5212     if (d->scrollIncrement > 9) {
5213         return;
5214     }
5215     d->scrollIncrement++;
5216     slotAutoScroll();
5217     setFocus();
5218 }
5219 
5220 void PageView::slotScrollUp(int nSteps)
5221 {
5222     if (verticalScrollBar()->value() > verticalScrollBar()->minimum()) {
5223         if (nSteps) {
5224             d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, -100 * nSteps), d->currentShortScrollDuration);
5225         } else {
5226             if (d->scroller->finalPosition().y() > verticalScrollBar()->minimum()) {
5227                 d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, -(1 - Okular::Settings::scrollOverlap() / 100.0) * viewport()->height()), d->currentLongScrollDuration);
5228             }
5229         }
5230     } else if (!getContinuousMode() && d->document->currentPage() > 0) {
5231         // Since we are in single page mode and at the top of the page, go to previous page.
5232         // setViewport() is more optimized than document->setPrevPage and then move view to bottom.
5233         Okular::DocumentViewport newViewport = d->document->viewport();
5234         newViewport.pageNumber -= viewColumns();
5235         if (newViewport.pageNumber < 0) {
5236             newViewport.pageNumber = 0;
5237         }
5238         newViewport.rePos.enabled = true;
5239         newViewport.rePos.normalizedY = 1.0;
5240         d->document->setViewport(newViewport);
5241     }
5242 }
5243 
5244 void PageView::slotScrollDown(int nSteps)
5245 {
5246     if (verticalScrollBar()->value() < verticalScrollBar()->maximum()) {
5247         if (nSteps) {
5248             d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, 100 * nSteps), d->currentShortScrollDuration);
5249         } else {
5250             if (d->scroller->finalPosition().y() < verticalScrollBar()->maximum()) {
5251                 d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, (1 - Okular::Settings::scrollOverlap() / 100.0) * viewport()->height()), d->currentLongScrollDuration);
5252             }
5253         }
5254     } else if (!getContinuousMode() && (int)d->document->currentPage() < d->items.count() - 1) {
5255         // Since we are in single page mode and at the bottom of the page, go to next page.
5256         // setViewport() is more optimized than document->setNextPage and then move view to top
5257         Okular::DocumentViewport newViewport = d->document->viewport();
5258         newViewport.pageNumber += viewColumns();
5259         if (newViewport.pageNumber >= (int)d->items.count()) {
5260             newViewport.pageNumber = d->items.count() - 1;
5261         }
5262         newViewport.rePos.enabled = true;
5263         newViewport.rePos.normalizedY = 0.0;
5264         d->document->setViewport(newViewport);
5265     }
5266 }
5267 
5268 void PageView::slotRotateClockwise()
5269 {
5270     int id = ((int)d->document->rotation() + 1) % 4;
5271     d->document->setRotation(id);
5272 }
5273 
5274 void PageView::slotRotateCounterClockwise()
5275 {
5276     int id = ((int)d->document->rotation() + 3) % 4;
5277     d->document->setRotation(id);
5278 }
5279 
5280 void PageView::slotRotateOriginal()
5281 {
5282     d->document->setRotation(0);
5283 }
5284 
5285 // Enforce mutual-exclusion between trim modes
5286 // Each mode is uniquely identified by a single value
5287 // From Okular::Settings::EnumTrimMode
5288 void PageView::updateTrimMode(int except_id)
5289 {
5290     const QList<QAction *> trimModeActions = d->aTrimMode->menu()->actions();
5291     for (QAction *trimModeAction : trimModeActions) {
5292         if (trimModeAction->data().toInt() != except_id) {
5293             trimModeAction->setChecked(false);
5294         }
5295     }
5296 }
5297 
5298 bool PageView::mouseReleaseOverLink(const Okular::ObjectRect *rect) const
5299 {
5300     if (rect) {
5301         // handle click over a link
5302         const Okular::Action *action = static_cast<const Okular::Action *>(rect->object());
5303         d->document->processAction(action);
5304         return true;
5305     }
5306     return false;
5307 }
5308 
5309 void PageView::slotTrimMarginsToggled(bool on)
5310 {
5311     if (on) { // Turn off any other Trim modes
5312         updateTrimMode(d->aTrimMargins->data().toInt());
5313     }
5314 
5315     if (Okular::Settings::trimMargins() != on) {
5316         Okular::Settings::setTrimMargins(on);
5317         Okular::Settings::self()->save();
5318         if (d->document->pages() > 0) {
5319             slotRelayoutPages();
5320             slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already!
5321         }
5322     }
5323 }
5324 
5325 void PageView::slotTrimToSelectionToggled(bool on)
5326 {
5327     if (on) { // Turn off any other Trim modes
5328         updateTrimMode(d->aTrimToSelection->data().toInt());
5329 
5330         // Change the mouse mode
5331         d->mouseMode = Okular::Settings::EnumMouseMode::TrimSelect;
5332         d->aMouseNormal->setChecked(false);
5333 
5334         // change the text in messageWindow (and show it if hidden)
5335         d->messageWindow->display(i18n("Draw a rectangle around the page area you wish to keep visible"), QString(), PageViewMessage::Info, -1);
5336         // force an update of the cursor
5337         updateCursor();
5338     } else {
5339         // toggled off while making selection
5340         if (Okular::Settings::EnumMouseMode::TrimSelect == d->mouseMode) {
5341             // clear widget selection and invalidate rect
5342             selectionClear();
5343 
5344             // When Trim selection bbox interaction is over, we should switch to another mousemode.
5345             if (d->aPrevAction) {
5346                 d->aPrevAction->trigger();
5347                 d->aPrevAction = nullptr;
5348             } else {
5349                 d->aMouseNormal->trigger();
5350             }
5351         }
5352 
5353         d->trimBoundingBox = Okular::NormalizedRect(); // invalidate box
5354         if (d->document->pages() > 0) {
5355             slotRelayoutPages();
5356             slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already!
5357         }
5358     }
5359 }
5360 
5361 void PageView::slotToggleForms()
5362 {
5363     toggleFormWidgets(!d->m_formsVisible);
5364 }
5365 
5366 void PageView::slotFormChanged(int pageNumber)
5367 {
5368     if (!d->refreshTimer) {
5369         d->refreshTimer = new QTimer(this);
5370         d->refreshTimer->setSingleShot(true);
5371         connect(d->refreshTimer, &QTimer::timeout, this, &PageView::slotRefreshPage);
5372     }
5373     d->refreshPages << pageNumber;
5374     int delay = 0;
5375     if (d->m_formsVisible) {
5376         delay = 1000;
5377     }
5378     d->refreshTimer->start(delay);
5379 }
5380 
5381 void PageView::slotRefreshPage()
5382 {
5383     for (int req : std::as_const(d->refreshPages)) {
5384         QTimer::singleShot(0, this, [this, req] { d->document->refreshPixmaps(req); });
5385     }
5386     d->refreshPages.clear();
5387 }
5388 
5389 #if HAVE_SPEECH
5390 void PageView::slotSpeakDocument()
5391 {
5392     QString text;
5393     for (const PageViewItem *item : std::as_const(d->items)) {
5394         Okular::RegularAreaRect *area = textSelectionForItem(item);
5395         text.append(item->page()->text(area));
5396         text.append(QLatin1Char('\n'));
5397         delete area;
5398     }
5399 
5400     d->tts()->say(text);
5401 }
5402 
5403 void PageView::slotSpeakCurrentPage()
5404 {
5405     const int currentPage = d->document->viewport().pageNumber;
5406 
5407     PageViewItem *item = d->items.at(currentPage);
5408     Okular::RegularAreaRect *area = textSelectionForItem(item);
5409     const QString text = item->page()->text(area);
5410     delete area;
5411 
5412     d->tts()->say(text);
5413 }
5414 
5415 void PageView::slotStopSpeaks()
5416 {
5417     if (!d->m_tts) {
5418         return;
5419     }
5420 
5421     d->m_tts->stopAllSpeechs();
5422 }
5423 
5424 void PageView::slotPauseResumeSpeech()
5425 {
5426     if (!d->m_tts) {
5427         return;
5428     }
5429 
5430     d->m_tts->pauseResumeSpeech();
5431 }
5432 
5433 #endif
5434 
5435 void PageView::slotAction(Okular::Action *action)
5436 {
5437     d->document->processAction(action);
5438 }
5439 
5440 void PageView::slotMouseUpAction(Okular::Action *action, Okular::FormField *form)
5441 {
5442     if (form && action->actionType() == Okular::Action::Script) {
5443         d->document->processFormMouseUpScripAction(action, form);
5444     } else {
5445         d->document->processAction(action);
5446     }
5447 }
5448 
5449 void PageView::externalKeyPressEvent(QKeyEvent *e)
5450 {
5451     keyPressEvent(e);
5452 }
5453 
5454 void PageView::slotProcessMovieAction(const Okular::MovieAction *action)
5455 {
5456     const Okular::MovieAnnotation *movieAnnotation = action->annotation();
5457     if (!movieAnnotation) {
5458         return;
5459     }
5460 
5461     Okular::Movie *movie = movieAnnotation->movie();
5462     if (!movie) {
5463         return;
5464     }
5465 
5466     const int currentPage = d->document->viewport().pageNumber;
5467 
5468     PageViewItem *item = d->items.at(currentPage);
5469     if (!item) {
5470         return;
5471     }
5472 
5473     VideoWidget *vw = item->videoWidgets().value(movie);
5474     if (!vw) {
5475         return;
5476     }
5477 
5478     vw->show();
5479 
5480     switch (action->operation()) {
5481     case Okular::MovieAction::Play:
5482         vw->stop();
5483         vw->play();
5484         break;
5485     case Okular::MovieAction::Stop:
5486         vw->stop();
5487         break;
5488     case Okular::MovieAction::Pause:
5489         vw->pause();
5490         break;
5491     case Okular::MovieAction::Resume:
5492         vw->play();
5493         break;
5494     };
5495 }
5496 
5497 void PageView::slotProcessRenditionAction(const Okular::RenditionAction *action)
5498 {
5499     Okular::Movie *movie = action->movie();
5500     if (!movie) {
5501         return;
5502     }
5503 
5504     const int currentPage = d->document->viewport().pageNumber;
5505 
5506     PageViewItem *item = d->items.at(currentPage);
5507     if (!item) {
5508         return;
5509     }
5510 
5511     VideoWidget *vw = item->videoWidgets().value(movie);
5512     if (!vw) {
5513         return;
5514     }
5515 
5516     if (action->operation() == Okular::RenditionAction::None) {
5517         return;
5518     }
5519 
5520     vw->show();
5521 
5522     switch (action->operation()) {
5523     case Okular::RenditionAction::Play:
5524         vw->stop();
5525         vw->play();
5526         break;
5527     case Okular::RenditionAction::Stop:
5528         vw->stop();
5529         break;
5530     case Okular::RenditionAction::Pause:
5531         vw->pause();
5532         break;
5533     case Okular::RenditionAction::Resume:
5534         vw->play();
5535         break;
5536     default:
5537         return;
5538     };
5539 }
5540 
5541 void PageView::slotFitWindowToPage()
5542 {
5543     const PageViewItem *currentPageItem = nullptr;
5544     QSize viewportSize = viewport()->size();
5545     for (const PageViewItem *pageItem : std::as_const(d->items)) {
5546         if (pageItem->isVisible()) {
5547             currentPageItem = pageItem;
5548             break;
5549         }
5550     }
5551 
5552     if (!currentPageItem) {
5553         return;
5554     }
5555 
5556     const QSize pageSize = QSize(currentPageItem->uncroppedWidth() + kcolWidthMargin, currentPageItem->uncroppedHeight() + krowHeightMargin);
5557     if (verticalScrollBar()->isVisible()) {
5558         viewportSize.setWidth(viewportSize.width() + verticalScrollBar()->width());
5559     }
5560     if (horizontalScrollBar()->isVisible()) {
5561         viewportSize.setHeight(viewportSize.height() + horizontalScrollBar()->height());
5562     }
5563     Q_EMIT fitWindowToPage(viewportSize, pageSize);
5564 }
5565 
5566 void PageView::slotSelectPage()
5567 {
5568     textSelectionClear();
5569     const int currentPage = d->document->viewport().pageNumber;
5570     PageViewItem *item = d->items.at(currentPage);
5571 
5572     if (item) {
5573         Okular::RegularAreaRect *area = textSelectionForItem(item);
5574         d->pagesWithTextSelection.insert(currentPage);
5575         d->document->setPageTextSelection(currentPage, area, palette().color(QPalette::Active, QPalette::Highlight));
5576     }
5577 }
5578 
5579 void PageView::highlightSignatureFormWidget(const Okular::FormFieldSignature *form)
5580 {
5581     QVector<PageViewItem *>::const_iterator dIt = d->items.constBegin(), dEnd = d->items.constEnd();
5582     for (; dIt != dEnd; ++dIt) {
5583         const QSet<FormWidgetIface *> fwi = (*dIt)->formWidgets();
5584         for (FormWidgetIface *fw : fwi) {
5585             if (fw->formField() == form) {
5586                 SignatureEdit *widget = static_cast<SignatureEdit *>(fw);
5587                 widget->setDummyMode(true);
5588                 QTimer::singleShot(250, this, [=] { widget->setDummyMode(false); });
5589                 return;
5590             }
5591         }
5592     }
5593 }
5594 
5595 // END private SLOTS
5596 
5597 /* kate: replace-tabs on; indent-width 4; */