1 /*
2     SPDX-FileCopyrightText: 2004-2005 Enrico Ros <eros.kde@email.it>
3     SPDX-FileCopyrightText: 2004-2006 Albert Astals Cid <aacid@kde.org>
4 
5     Work sponsored by the LiMux project of the city of Munich:
6     SPDX-FileCopyrightText: 2017 Klarälvdalens Datakonsult AB a KDAB Group company <info@kdab.com>
7 
8     With portions of code from kpdf/kpdf_pagewidget.cc by:
9     SPDX-FileCopyrightText: 2002 Wilco Greven <greven@kde.org>
10     SPDX-FileCopyrightText: 2003 Christophe Devriese <Christophe.Devriese@student.kuleuven.ac.be>
11     SPDX-FileCopyrightText: 2003 Laurent Montel <montel@kde.org>
12     SPDX-FileCopyrightText: 2003 Dirk Mueller <mueller@kde.org>
13     SPDX-FileCopyrightText: 2004 James Ots <kde@jamesots.com>
14     SPDX-FileCopyrightText: 2011 Jiri Baum - NICTA <jiri@baum.com.au>
15 
16     SPDX-License-Identifier: GPL-2.0-or-later
17 */
18 
19 #include "pageview.h"
20 
21 // qt/kde includes
22 #include <QApplication>
23 #include <QClipboard>
24 #include <QCursor>
25 #include <QDesktopServices>
26 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
27 #include <QDesktopWidget>
28 #endif
29 #include <QElapsedTimer>
30 #include <QEvent>
31 #include <QGestureEvent>
32 #include <QImage>
33 #include <QInputDialog>
34 #include <QLoggingCategory>
35 #include <QMenu>
36 #include <QMimeData>
37 #include <QMimeDatabase>
38 #include <QPainter>
39 #include <QScrollBar>
40 #include <QScroller>
41 #include <QScrollerProperties>
42 #include <QSet>
43 #include <QTimer>
44 #include <QToolTip>
45 
46 #include <KActionCollection>
47 #include <KActionMenu>
48 #include <KConfigWatcher>
49 #include <KLocalizedString>
50 #include <KMessageBox>
51 #include <KRun>
52 #include <KSelectAction>
53 #include <KStandardAction>
54 #include <KStringHandler>
55 #include <KToggleAction>
56 #include <KToolInvocation>
57 #include <KUriFilter>
58 #include <QAction>
59 #include <QDebug>
60 #include <QIcon>
61 #include <kwidgetsaddons_version.h>
62 
63 // system includes
64 #include <array>
65 #include <math.h>
66 #include <stdlib.h>
67 
68 // local includes
69 #include "annotationpopup.h"
70 #include "annotwindow.h"
71 #include "colormodemenu.h"
72 #include "core/annotations.h"
73 #include "cursorwraphelper.h"
74 #include "debug_ui.h"
75 #include "formwidgets.h"
76 #include "guiutils.h"
77 #include "okmenutitle.h"
78 #include "pagepainter.h"
79 #include "pageviewannotator.h"
80 #include "pageviewmouseannotation.h"
81 #include "pageviewutils.h"
82 #include "priorities.h"
83 #include "toggleactionmenu.h"
84 #ifdef HAVE_SPEECH
85 #include "tts.h"
86 #endif
87 #include "core/action.h"
88 #include "core/audioplayer.h"
89 #include "core/document_p.h"
90 #include "core/form.h"
91 #include "core/generator.h"
92 #include "core/misc.h"
93 #include "core/movie.h"
94 #include "core/page.h"
95 #include "core/page_p.h"
96 #include "core/sourcereference.h"
97 #include "core/tile.h"
98 #include "magnifierview.h"
99 #include "settings.h"
100 #include "settings_core.h"
101 #include "url_utils.h"
102 #include "videowidget.h"
103 
104 static const int pageflags = PagePainter::Accessibility | PagePainter::EnhanceLinks | PagePainter::EnhanceImages | PagePainter::Highlights | PagePainter::TextSelection | PagePainter::Annotations;
105 
106 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};
107 
108 // This is the length of the text that will be shown when the user is searching for a specific piece of text.
109 static const int searchTextPreviewLength = 21;
110 
111 // When following a link, only a preview of this length will be used to set the text of the action.
112 static const int linkTextPreviewLength = 30;
113 
normClamp(double value,double def)114 static inline double normClamp(double value, double def)
115 {
116     return (value < 0.0 || value > 1.0) ? def : value;
117 }
118 
119 struct TableSelectionPart {
120     PageViewItem *item;
121     Okular::NormalizedRect rectInItem;
122     Okular::NormalizedRect rectInSelection;
123 
124     TableSelectionPart(PageViewItem *item_p, const Okular::NormalizedRect &rectInItem_p, const Okular::NormalizedRect &rectInSelection_p);
125 };
126 
TableSelectionPart(PageViewItem * item_p,const Okular::NormalizedRect & rectInItem_p,const Okular::NormalizedRect & rectInSelection_p)127 TableSelectionPart::TableSelectionPart(PageViewItem *item_p, const Okular::NormalizedRect &rectInItem_p, const Okular::NormalizedRect &rectInSelection_p)
128     : item(item_p)
129     , rectInItem(rectInItem_p)
130     , rectInSelection(rectInSelection_p)
131 {
132 }
133 
134 // structure used internally by PageView for data storage
135 class PageViewPrivate
136 {
137 public:
138     explicit PageViewPrivate(PageView *qq);
139 
140     FormWidgetsController *formWidgetsController();
141 #ifdef HAVE_SPEECH
142     OkularTTS *tts();
143 #endif
144     QString selectedText() const;
145 
146     // the document, pageviewItems and the 'visible cache'
147     PageView *q;
148     Okular::Document *document;
149     QVector<PageViewItem *> items;
150     QLinkedList<PageViewItem *> visibleItems;
151     MagnifierView *magnifierView;
152 
153     // view layout (columns in Settings), zoom and mouse
154     PageView::ZoomMode zoomMode;
155     float zoomFactor;
156     QPoint mouseGrabOffset;
157     QPoint mousePressPos;
158     QPoint mouseSelectPos;
159     QPoint previousMouseMovePos;
160     int mouseMidLastY;
161     bool mouseSelecting;
162     QRect mouseSelectionRect;
163     QColor mouseSelectionColor;
164     bool mouseTextSelecting;
165     QSet<int> pagesWithTextSelection;
166     bool mouseOnRect;
167     int mouseMode;
168     MouseAnnotation *mouseAnnotation;
169 
170     // table selection
171     QList<double> tableSelectionCols;
172     QList<double> tableSelectionRows;
173     QList<TableSelectionPart> tableSelectionParts;
174     bool tableDividersGuessed;
175 
176     int lastSourceLocationViewportPageNumber;
177     double lastSourceLocationViewportNormalizedX;
178     double lastSourceLocationViewportNormalizedY;
179     int controlWheelAccumulatedDelta;
180 
181     // for everything except PgUp/PgDn and scroll to arbitrary locations
182     const int baseShortScrollDuration = 100;
183     int currentShortScrollDuration;
184     // for PgUp/PgDn and scroll to arbitrary locations
185     const int baseLongScrollDuration = baseShortScrollDuration * 2;
186     int currentLongScrollDuration;
187 
188     // auto scroll
189     int scrollIncrement;
190     QTimer *autoScrollTimer;
191     // annotations
192     PageViewAnnotator *annotator;
193     // text annotation dialogs list
194     QSet<AnnotWindow *> m_annowindows;
195     // other stuff
196     QTimer *delayResizeEventTimer;
197     bool dirtyLayout;
198     bool blockViewport;             // prevents changes to viewport
199     bool blockPixmapsRequest;       // prevent pixmap requests
200     PageViewMessage *messageWindow; // in pageviewutils.h
201     bool m_formsVisible;
202     FormWidgetsController *formsWidgetController;
203 #ifdef HAVE_SPEECH
204     OkularTTS *m_tts;
205 #endif
206     QTimer *refreshTimer;
207     QSet<int> refreshPages;
208 
209     // bbox state for Trim to Selection mode
210     Okular::NormalizedRect trimBoundingBox;
211 
212     // infinite resizing loop prevention
213     bool verticalScrollBarVisible = false;
214     bool horizontalScrollBarVisible = false;
215 
216     // drag scroll
217     QPoint dragScrollVector;
218     QTimer dragScrollTimer;
219 
220     // left click depress
221     QTimer leftClickTimer;
222 
223     // actions
224     QAction *aRotateClockwise;
225     QAction *aRotateCounterClockwise;
226     QAction *aRotateOriginal;
227     KActionMenu *aTrimMode;
228     KToggleAction *aTrimMargins;
229     KToggleAction *aReadingDirection;
230     QAction *aMouseNormal;
231     QAction *aMouseZoom;
232     QAction *aMouseSelect;
233     QAction *aMouseTextSelect;
234     QAction *aMouseTableSelect;
235     QAction *aMouseMagnifier;
236     KToggleAction *aTrimToSelection;
237     QAction *aSignature;
238     KSelectAction *aZoom;
239     QAction *aZoomIn;
240     QAction *aZoomOut;
241     QAction *aZoomActual;
242     KToggleAction *aZoomFitWidth;
243     KToggleAction *aZoomFitPage;
244     KToggleAction *aZoomAutoFit;
245     KActionMenu *aViewModeMenu;
246     QActionGroup *viewModeActionGroup;
247     ColorModeMenu *aColorModeMenu;
248     KToggleAction *aViewContinuous;
249     QAction *aPrevAction;
250     KToggleAction *aToggleForms;
251     QAction *aSpeakDoc;
252     QAction *aSpeakPage;
253     QAction *aSpeakStop;
254     QAction *aSpeakPauseResume;
255     KActionCollection *actionCollection;
256     QActionGroup *mouseModeActionGroup;
257     ToggleActionMenu *aMouseModeMenu;
258     QAction *aFitWindowToPage;
259 
260     int setting_viewCols;
261     bool rtl_Mode;
262     // Keep track of whether tablet pen is currently pressed down
263     bool penDown;
264 
265     // Keep track of mouse over link object
266     const Okular::ObjectRect *mouseOverLinkObject;
267 
268     QScroller *scroller;
269 };
270 
PageViewPrivate(PageView * qq)271 PageViewPrivate::PageViewPrivate(PageView *qq)
272     : q(qq)
273 #ifdef HAVE_SPEECH
274     , m_tts(nullptr)
275 #endif
276 {
277 }
278 
formWidgetsController()279 FormWidgetsController *PageViewPrivate::formWidgetsController()
280 {
281     if (!formsWidgetController) {
282         formsWidgetController = new FormWidgetsController(document);
283         QObject::connect(formsWidgetController, &FormWidgetsController::changed, q, &PageView::slotFormChanged);
284         QObject::connect(formsWidgetController, &FormWidgetsController::action, q, &PageView::slotAction);
285         QObject::connect(formsWidgetController, &FormWidgetsController::formatAction, q, [this](const Okular::Action *action, Okular::FormFieldText *fft) { document->processFormatAction(action, fft); });
286         QObject::connect(formsWidgetController, &FormWidgetsController::keystrokeAction, q, [this](const Okular::Action *action, Okular::FormFieldText *fft, bool &ok) { document->processKeystrokeAction(action, fft, ok); });
287         QObject::connect(formsWidgetController, &FormWidgetsController::focusAction, q, [this](const Okular::Action *action, Okular::FormFieldText *fft) { document->processFocusAction(action, fft); });
288         QObject::connect(formsWidgetController, &FormWidgetsController::validateAction, q, [this](const Okular::Action *action, Okular::FormFieldText *fft, bool &ok) { document->processValidateAction(action, fft, ok); });
289     }
290 
291     return formsWidgetController;
292 }
293 
294 #ifdef HAVE_SPEECH
tts()295 OkularTTS *PageViewPrivate::tts()
296 {
297     if (!m_tts) {
298         m_tts = new OkularTTS(q);
299         if (aSpeakStop) {
300             QObject::connect(m_tts, &OkularTTS::canPauseOrResume, aSpeakStop, &QAction::setEnabled);
301         }
302 
303         if (aSpeakPauseResume) {
304             QObject::connect(m_tts, &OkularTTS::canPauseOrResume, aSpeakPauseResume, &QAction::setEnabled);
305         }
306     }
307 
308     return m_tts;
309 }
310 #endif
311 
312 /* PageView. What's in this file? -> quick overview.
313  * Code weight (in rows) and meaning:
314  *  160 - constructor and creating actions plus their connected slots (empty stuff)
315  *  70  - DocumentObserver inherited methodes (important)
316  *  550 - events: mouse, keyboard, drag
317  *  170 - slotRelayoutPages: set contents of the scrollview on continuous/single modes
318  *  100 - zoom: zooming pages in different ways, keeping update the toolbar actions, etc..
319  *  other misc functions: only slotRequestVisiblePixmaps and pickItemOnPoint noticeable,
320  * and many insignificant stuff like this comment :-)
321  */
PageView(QWidget * parent,Okular::Document * document)322 PageView::PageView(QWidget *parent, Okular::Document *document)
323     : QAbstractScrollArea(parent)
324     , Okular::View(QStringLiteral("PageView"))
325 {
326     // create and initialize private storage structure
327     d = new PageViewPrivate(this);
328     d->document = document;
329     d->aRotateClockwise = nullptr;
330     d->aRotateCounterClockwise = nullptr;
331     d->aRotateOriginal = nullptr;
332     d->aViewModeMenu = nullptr;
333     d->zoomMode = PageView::ZoomFitWidth;
334     d->zoomFactor = 1.0;
335     d->mouseSelecting = false;
336     d->mouseTextSelecting = false;
337     d->mouseOnRect = false;
338     d->mouseMode = Okular::Settings::mouseMode();
339     d->mouseAnnotation = new MouseAnnotation(this, document);
340     d->tableDividersGuessed = false;
341     d->lastSourceLocationViewportPageNumber = -1;
342     d->lastSourceLocationViewportNormalizedX = 0.0;
343     d->lastSourceLocationViewportNormalizedY = 0.0;
344     d->controlWheelAccumulatedDelta = 0;
345     d->currentShortScrollDuration = d->baseShortScrollDuration;
346     d->currentLongScrollDuration = d->baseLongScrollDuration;
347     d->scrollIncrement = 0;
348     d->autoScrollTimer = nullptr;
349     d->annotator = nullptr;
350     d->dirtyLayout = false;
351     d->blockViewport = false;
352     d->blockPixmapsRequest = false;
353     d->messageWindow = new PageViewMessage(this);
354     d->m_formsVisible = false;
355     d->formsWidgetController = nullptr;
356 #ifdef HAVE_SPEECH
357     d->m_tts = nullptr;
358 #endif
359     d->refreshTimer = nullptr;
360     d->aRotateClockwise = nullptr;
361     d->aRotateCounterClockwise = nullptr;
362     d->aRotateOriginal = nullptr;
363     d->aTrimMode = nullptr;
364     d->aTrimMargins = nullptr;
365     d->aTrimToSelection = nullptr;
366     d->aReadingDirection = nullptr;
367     d->aMouseNormal = nullptr;
368     d->aMouseZoom = nullptr;
369     d->aMouseSelect = nullptr;
370     d->aMouseTextSelect = nullptr;
371     d->aSignature = nullptr;
372     d->aZoomFitWidth = nullptr;
373     d->aZoomFitPage = nullptr;
374     d->aZoomAutoFit = nullptr;
375     d->aViewModeMenu = nullptr;
376     d->aViewContinuous = nullptr;
377     d->viewModeActionGroup = nullptr;
378     d->aColorModeMenu = nullptr;
379     d->aPrevAction = nullptr;
380     d->aToggleForms = nullptr;
381     d->aSpeakDoc = nullptr;
382     d->aSpeakPage = nullptr;
383     d->aSpeakStop = nullptr;
384     d->aSpeakPauseResume = nullptr;
385     d->actionCollection = nullptr;
386     d->setting_viewCols = Okular::Settings::viewColumns();
387     d->rtl_Mode = Okular::Settings::rtlReadingDirection();
388     d->mouseModeActionGroup = nullptr;
389     d->aMouseModeMenu = nullptr;
390     d->penDown = false;
391     d->aMouseMagnifier = nullptr;
392     d->aFitWindowToPage = nullptr;
393     d->trimBoundingBox = Okular::NormalizedRect(); // Null box
394 
395     switch (Okular::Settings::zoomMode()) {
396     case 0: {
397         d->zoomFactor = 1;
398         d->zoomMode = PageView::ZoomFixed;
399         break;
400     }
401     case 1: {
402         d->zoomMode = PageView::ZoomFitWidth;
403         break;
404     }
405     case 2: {
406         d->zoomMode = PageView::ZoomFitPage;
407         break;
408     }
409     case 3: {
410         d->zoomMode = PageView::ZoomFitAuto;
411         break;
412     }
413     }
414 
415     connect(Okular::Settings::self(), &Okular::Settings::viewContinuousChanged, this, [=]() {
416         if (d->aViewContinuous && !d->document->isOpened())
417             d->aViewContinuous->setChecked(Okular::Settings::viewContinuous());
418     });
419 
420     d->delayResizeEventTimer = new QTimer(this);
421     d->delayResizeEventTimer->setSingleShot(true);
422     d->delayResizeEventTimer->setObjectName(QStringLiteral("delayResizeEventTimer"));
423     connect(d->delayResizeEventTimer, &QTimer::timeout, this, &PageView::delayedResizeEvent);
424 
425     setFrameStyle(QFrame::NoFrame);
426 
427     setAttribute(Qt::WA_StaticContents);
428 
429     setObjectName(QStringLiteral("okular::pageView"));
430 
431     // viewport setup: setup focus, and track mouse
432     viewport()->setFocusProxy(this);
433     viewport()->setFocusPolicy(Qt::StrongFocus);
434     viewport()->setAttribute(Qt::WA_OpaquePaintEvent);
435     viewport()->setAttribute(Qt::WA_NoSystemBackground);
436     viewport()->setMouseTracking(true);
437     viewport()->setAutoFillBackground(false);
438 
439     d->scroller = QScroller::scroller(viewport());
440 
441     QScrollerProperties prop;
442     prop.setScrollMetric(QScrollerProperties::DecelerationFactor, 0.3);
443     prop.setScrollMetric(QScrollerProperties::MaximumVelocity, 1);
444     prop.setScrollMetric(QScrollerProperties::AcceleratingFlickMaximumTime, 0.2); // Workaround for QTBUG-88249 (non-flick gestures recognized as accelerating flick)
445     prop.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
446     prop.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff);
447     prop.setScrollMetric(QScrollerProperties::DragStartDistance, 0.0);
448     d->scroller->setScrollerProperties(prop);
449 
450     connect(d->scroller, &QScroller::stateChanged, this, &PageView::slotRequestVisiblePixmaps);
451 
452     // the apparently "magic" value of 20 is the same used internally in QScrollArea
453     verticalScrollBar()->setCursor(Qt::ArrowCursor);
454     verticalScrollBar()->setSingleStep(20);
455     horizontalScrollBar()->setCursor(Qt::ArrowCursor);
456     horizontalScrollBar()->setSingleStep(20);
457 
458     // make the smooth scroll animation durations respect the global animation
459     // scale
460     KConfigWatcher::Ptr animationSpeedWatcher = KConfigWatcher::create(KSharedConfig::openConfig());
461     connect(animationSpeedWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) {
462         if (group.name() == QLatin1String("KDE") && names.contains(QByteArrayLiteral("AnimationDurationFactor"))) {
463             PageView::updateSmoothScrollAnimationSpeed();
464         }
465     });
466 
467     // connect the padding of the viewport to pixmaps requests
468     connect(horizontalScrollBar(), &QAbstractSlider::valueChanged, this, &PageView::slotRequestVisiblePixmaps);
469     connect(verticalScrollBar(), &QAbstractSlider::valueChanged, this, &PageView::slotRequestVisiblePixmaps);
470 
471     // Keep the scroller in sync with user input on the scrollbars.
472     // QAbstractSlider::sliderMoved() and sliderReleased are the intuitive signals,
473     // but are only emitted when the “slider is down”, i. e. not when the user scrolls on the scrollbar.
474     // QAbstractSlider::actionTriggered() is emitted in all user input cases,
475     // but before the value() changes, so we need queued connection here.
476     auto update_scroller = [=]() {
477         d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); // sync scroller with scrollbar
478     };
479     connect(verticalScrollBar(), &QAbstractSlider::actionTriggered, this, update_scroller, Qt::QueuedConnection);
480     connect(horizontalScrollBar(), &QAbstractSlider::actionTriggered, this, update_scroller, Qt::QueuedConnection);
481 
482     connect(&d->dragScrollTimer, &QTimer::timeout, this, &PageView::slotDragScroll);
483 
484     d->leftClickTimer.setSingleShot(true);
485     connect(&d->leftClickTimer, &QTimer::timeout, this, &PageView::slotShowSizeAllCursor);
486 
487     // set a corner button to resize the view to the page size
488     //    QPushButton * resizeButton = new QPushButton( viewport() );
489     //    resizeButton->setPixmap( SmallIcon("crop") );
490     //    setCornerWidget( resizeButton );
491     //    resizeButton->setEnabled( false );
492     // connect(...);
493     setAttribute(Qt::WA_InputMethodEnabled, true);
494 
495     // Grab pinch gestures to zoom and rotate the view
496     grabGesture(Qt::PinchGesture);
497 
498     d->magnifierView = new MagnifierView(document, this);
499     d->magnifierView->hide();
500     d->magnifierView->setGeometry(0, 0, 351, 201); // TODO: more dynamic?
501 
502     connect(document, &Okular::Document::processMovieAction, this, &PageView::slotProcessMovieAction);
503     connect(document, &Okular::Document::processRenditionAction, this, &PageView::slotProcessRenditionAction);
504 
505     // schedule the welcome message
506     QMetaObject::invokeMethod(this, "slotShowWelcome", Qt::QueuedConnection);
507 }
508 
~PageView()509 PageView::~PageView()
510 {
511 #ifdef HAVE_SPEECH
512     if (d->m_tts)
513         d->m_tts->stopAllSpeechs();
514 #endif
515 
516     delete d->mouseAnnotation;
517 
518     // delete the local storage structure
519 
520     // We need to assign it to a different list otherwise slotAnnotationWindowDestroyed
521     // will bite us and clear d->m_annowindows
522     QSet<AnnotWindow *> annowindows = d->m_annowindows;
523     d->m_annowindows.clear();
524     qDeleteAll(annowindows);
525 
526     // delete all widgets
527     qDeleteAll(d->items);
528     delete d->formsWidgetController;
529     d->document->removeObserver(this);
530     delete d;
531 }
532 
setupBaseActions(KActionCollection * ac)533 void PageView::setupBaseActions(KActionCollection *ac)
534 {
535     d->actionCollection = ac;
536 
537     // Zoom actions ( higher scales takes lots of memory! )
538     d->aZoom = new KSelectAction(QIcon::fromTheme(QStringLiteral("page-zoom")), i18n("Zoom"), this);
539     ac->addAction(QStringLiteral("zoom_to"), d->aZoom);
540     d->aZoom->setEditable(true);
541     d->aZoom->setMaxComboViewCount(kZoomValues.size() + 3);
542     connect(d->aZoom, QOverload<QAction *>::of(&KSelectAction::triggered), this, &PageView::slotZoom);
543     updateZoomText();
544 
545     d->aZoomIn = KStandardAction::zoomIn(this, SLOT(slotZoomIn()), ac);
546 
547     d->aZoomOut = KStandardAction::zoomOut(this, SLOT(slotZoomOut()), ac);
548 
549     d->aZoomActual = KStandardAction::actualSize(this, &PageView::slotZoomActual, ac);
550     d->aZoomActual->setText(i18n("Zoom to 100%"));
551 }
552 
setupViewerActions(KActionCollection * ac)553 void PageView::setupViewerActions(KActionCollection *ac)
554 {
555     d->actionCollection = ac;
556 
557     ac->setDefaultShortcut(d->aZoomIn, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_Plus));
558     ac->setDefaultShortcut(d->aZoomOut, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_Minus));
559 
560     // orientation menu actions
561     d->aRotateClockwise = new QAction(QIcon::fromTheme(QStringLiteral("object-rotate-right")), i18n("Rotate &Right"), this);
562     d->aRotateClockwise->setIconText(i18nc("Rotate right", "Right"));
563     ac->addAction(QStringLiteral("view_orientation_rotate_cw"), d->aRotateClockwise);
564     d->aRotateClockwise->setEnabled(false);
565     connect(d->aRotateClockwise, &QAction::triggered, this, &PageView::slotRotateClockwise);
566     d->aRotateCounterClockwise = new QAction(QIcon::fromTheme(QStringLiteral("object-rotate-left")), i18n("Rotate &Left"), this);
567     d->aRotateCounterClockwise->setIconText(i18nc("Rotate left", "Left"));
568     ac->addAction(QStringLiteral("view_orientation_rotate_ccw"), d->aRotateCounterClockwise);
569     d->aRotateCounterClockwise->setEnabled(false);
570     connect(d->aRotateCounterClockwise, &QAction::triggered, this, &PageView::slotRotateCounterClockwise);
571     d->aRotateOriginal = new QAction(i18n("Original Orientation"), this);
572     ac->addAction(QStringLiteral("view_orientation_original"), d->aRotateOriginal);
573     d->aRotateOriginal->setEnabled(false);
574     connect(d->aRotateOriginal, &QAction::triggered, this, &PageView::slotRotateOriginal);
575 
576     // Trim View actions
577     d->aTrimMode = new KActionMenu(i18n("&Trim View"), this);
578     d->aTrimMode->setDelayed(false);
579     ac->addAction(QStringLiteral("view_trim_mode"), d->aTrimMode);
580 
581     d->aTrimMargins = new KToggleAction(QIcon::fromTheme(QStringLiteral("trim-margins")), i18n("&Trim Margins"), d->aTrimMode->menu());
582     d->aTrimMode->addAction(d->aTrimMargins);
583     ac->addAction(QStringLiteral("view_trim_margins"), d->aTrimMargins);
584     d->aTrimMargins->setData(QVariant::fromValue((int)Okular::Settings::EnumTrimMode::Margins));
585     connect(d->aTrimMargins, &QAction::toggled, this, &PageView::slotTrimMarginsToggled);
586     d->aTrimMargins->setChecked(Okular::Settings::trimMargins());
587 
588     d->aTrimToSelection = new KToggleAction(QIcon::fromTheme(QStringLiteral("trim-to-selection")), i18n("Trim To &Selection"), d->aTrimMode->menu());
589     d->aTrimMode->addAction(d->aTrimToSelection);
590     ac->addAction(QStringLiteral("view_trim_selection"), d->aTrimToSelection);
591     d->aTrimToSelection->setData(QVariant::fromValue((int)Okular::Settings::EnumTrimMode::Selection));
592     connect(d->aTrimToSelection, &QAction::toggled, this, &PageView::slotTrimToSelectionToggled);
593 
594     d->aZoomFitWidth = new KToggleAction(QIcon::fromTheme(QStringLiteral("zoom-fit-width")), i18n("Fit &Width"), this);
595     ac->addAction(QStringLiteral("view_fit_to_width"), d->aZoomFitWidth);
596     connect(d->aZoomFitWidth, &QAction::toggled, this, &PageView::slotFitToWidthToggled);
597 
598     d->aZoomFitPage = new KToggleAction(QIcon::fromTheme(QStringLiteral("zoom-fit-best")), i18n("Fit &Page"), this);
599     ac->addAction(QStringLiteral("view_fit_to_page"), d->aZoomFitPage);
600     connect(d->aZoomFitPage, &QAction::toggled, this, &PageView::slotFitToPageToggled);
601 
602     d->aZoomAutoFit = new KToggleAction(QIcon::fromTheme(QStringLiteral("zoom-fit-best")), i18n("&Auto Fit"), this);
603     ac->addAction(QStringLiteral("view_auto_fit"), d->aZoomAutoFit);
604     connect(d->aZoomAutoFit, &QAction::toggled, this, &PageView::slotAutoFitToggled);
605 
606     d->aFitWindowToPage = new QAction(QIcon::fromTheme(QStringLiteral("zoom-fit-width")), i18n("Fit Wi&ndow to Page"), this);
607     d->aFitWindowToPage->setEnabled(Okular::Settings::viewMode() == (int)Okular::Settings::EnumViewMode::Single);
608     ac->setDefaultShortcut(d->aFitWindowToPage, QKeySequence(Qt::CTRL | Qt::Key_J));
609     ac->addAction(QStringLiteral("fit_window_to_page"), d->aFitWindowToPage);
610     connect(d->aFitWindowToPage, &QAction::triggered, this, &PageView::slotFitWindowToPage);
611 
612     // View Mode action menu (Single Page, Facing Pages,...(choose), and Continuous (on/off))
613     d->aViewModeMenu = new KActionMenu(QIcon::fromTheme(QStringLiteral("view-split-left-right")), i18n("&View Mode"), this);
614     d->aViewModeMenu->setDelayed(false);
615     ac->addAction(QStringLiteral("view_render_mode"), d->aViewModeMenu);
616 
617     d->viewModeActionGroup = new QActionGroup(this);
618     auto addViewMode = [=](QAction *a, const QString &name, Okular::Settings::EnumViewMode::type id) {
619         a->setCheckable(true);
620         a->setData(int(id));
621         d->aViewModeMenu->addAction(a);
622         ac->addAction(name, a);
623         d->viewModeActionGroup->addAction(a);
624     };
625     addViewMode(new QAction(QIcon::fromTheme(QStringLiteral("view-pages-single")), i18nc("@item:inmenu", "&Single Page"), this), QStringLiteral("view_render_mode_single"), Okular::Settings::EnumViewMode::Single);
626     addViewMode(new QAction(QIcon::fromTheme(QStringLiteral("view-pages-facing")), i18nc("@item:inmenu", "&Facing Pages"), this), QStringLiteral("view_render_mode_facing"), Okular::Settings::EnumViewMode::Facing);
627     addViewMode(new QAction(QIcon::fromTheme(QStringLiteral("view-pages-facing-first-centered")), i18nc("@item:inmenu", "Facing Pages (&Center First Page)"), this),
628                 QStringLiteral("view_render_mode_facing_center_first"),
629                 Okular::Settings::EnumViewMode::FacingFirstCentered);
630     addViewMode(new QAction(QIcon::fromTheme(QStringLiteral("view-pages-overview")), i18nc("@item:inmenu", "&Overview"), this), QStringLiteral("view_render_mode_overview"), Okular::Settings::EnumViewMode::Summary);
631     const QList<QAction *> viewModeActions = d->viewModeActionGroup->actions();
632     for (QAction *viewModeAction : viewModeActions) {
633         if (viewModeAction->data().toInt() == Okular::Settings::viewMode()) {
634             viewModeAction->setChecked(true);
635             break;
636         }
637     }
638     connect(d->viewModeActionGroup, &QActionGroup::triggered, this, &PageView::slotViewMode);
639 
640     // Continuous view action, add to view mode action menu.
641     d->aViewModeMenu->addSeparator();
642     d->aViewContinuous = new KToggleAction(QIcon::fromTheme(QStringLiteral("view-pages-continuous")), i18n("&Continuous"), this);
643     d->aViewModeMenu->addAction(d->aViewContinuous);
644     ac->addAction(QStringLiteral("view_continuous"), d->aViewContinuous);
645     connect(d->aViewContinuous, &QAction::toggled, this, &PageView::slotContinuousToggled);
646     d->aViewContinuous->setChecked(Okular::Settings::viewContinuous());
647 
648     // Reading direction toggle action. (Checked means RTL, unchecked means LTR.)
649     d->aReadingDirection = new KToggleAction(QIcon::fromTheme(QStringLiteral("format-text-direction-rtl")), i18nc("@action page layout", "Use Right to Left Reading Direction"), this);
650     d->aReadingDirection->setChecked(Okular::Settings::rtlReadingDirection());
651     ac->addAction(QStringLiteral("rtl_page_layout"), d->aReadingDirection);
652     connect(d->aReadingDirection, &QAction::toggled, this, &PageView::slotReadingDirectionToggled);
653     connect(Okular::SettingsCore::self(), &Okular::SettingsCore::configChanged, this, &PageView::slotUpdateReadingDirectionAction);
654 
655     // Mouse mode actions for viewer mode
656     d->mouseModeActionGroup = new QActionGroup(this);
657     d->mouseModeActionGroup->setExclusive(true);
658     d->aMouseNormal = new QAction(QIcon::fromTheme(QStringLiteral("transform-browse")), i18n("&Browse"), this);
659     ac->addAction(QStringLiteral("mouse_drag"), d->aMouseNormal);
660     connect(d->aMouseNormal, &QAction::triggered, this, &PageView::slotSetMouseNormal);
661     d->aMouseNormal->setCheckable(true);
662     ac->setDefaultShortcut(d->aMouseNormal, QKeySequence(Qt::CTRL | Qt::Key_1));
663     d->aMouseNormal->setActionGroup(d->mouseModeActionGroup);
664     d->aMouseNormal->setChecked(Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::Browse);
665 
666     d->aMouseZoom = new QAction(QIcon::fromTheme(QStringLiteral("page-zoom")), i18n("&Zoom"), this);
667     ac->addAction(QStringLiteral("mouse_zoom"), d->aMouseZoom);
668     connect(d->aMouseZoom, &QAction::triggered, this, &PageView::slotSetMouseZoom);
669     d->aMouseZoom->setCheckable(true);
670     ac->setDefaultShortcut(d->aMouseZoom, QKeySequence(Qt::CTRL | Qt::Key_2));
671     d->aMouseZoom->setActionGroup(d->mouseModeActionGroup);
672     d->aMouseZoom->setChecked(Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::Zoom);
673 
674     d->aColorModeMenu = new ColorModeMenu(ac, this);
675 }
676 
677 // WARNING: 'setupViewerActions' must have been called before this method
setupActions(KActionCollection * ac)678 void PageView::setupActions(KActionCollection *ac)
679 {
680     d->actionCollection = ac;
681 
682     ac->setDefaultShortcuts(d->aZoomIn, KStandardShortcut::zoomIn());
683     ac->setDefaultShortcuts(d->aZoomOut, KStandardShortcut::zoomOut());
684 
685     // Mouse-Mode actions
686     d->aMouseSelect = new QAction(QIcon::fromTheme(QStringLiteral("select-rectangular")), i18n("Area &Selection"), this);
687     ac->addAction(QStringLiteral("mouse_select"), d->aMouseSelect);
688     connect(d->aMouseSelect, &QAction::triggered, this, &PageView::slotSetMouseSelect);
689     d->aMouseSelect->setCheckable(true);
690     ac->setDefaultShortcut(d->aMouseSelect, Qt::CTRL | Qt::Key_3);
691     d->aMouseSelect->setActionGroup(d->mouseModeActionGroup);
692 
693     d->aMouseTextSelect = new QAction(QIcon::fromTheme(QStringLiteral("edit-select-text")), i18n("&Text Selection"), this);
694     ac->addAction(QStringLiteral("mouse_textselect"), d->aMouseTextSelect);
695     connect(d->aMouseTextSelect, &QAction::triggered, this, &PageView::slotSetMouseTextSelect);
696     d->aMouseTextSelect->setCheckable(true);
697     ac->setDefaultShortcut(d->aMouseTextSelect, Qt::CTRL | Qt::Key_4);
698     d->aMouseTextSelect->setActionGroup(d->mouseModeActionGroup);
699 
700     d->aMouseTableSelect = new QAction(QIcon::fromTheme(QStringLiteral("table")), i18n("T&able Selection"), this);
701     ac->addAction(QStringLiteral("mouse_tableselect"), d->aMouseTableSelect);
702     connect(d->aMouseTableSelect, &QAction::triggered, this, &PageView::slotSetMouseTableSelect);
703     d->aMouseTableSelect->setCheckable(true);
704     ac->setDefaultShortcut(d->aMouseTableSelect, Qt::CTRL | Qt::Key_5);
705     d->aMouseTableSelect->setActionGroup(d->mouseModeActionGroup);
706 
707     d->aMouseMagnifier = new QAction(QIcon::fromTheme(QStringLiteral("document-preview")), i18n("&Magnifier"), this);
708     ac->addAction(QStringLiteral("mouse_magnifier"), d->aMouseMagnifier);
709     connect(d->aMouseMagnifier, &QAction::triggered, this, &PageView::slotSetMouseMagnifier);
710     d->aMouseMagnifier->setCheckable(true);
711     ac->setDefaultShortcut(d->aMouseMagnifier, Qt::CTRL | Qt::Key_6);
712     d->aMouseMagnifier->setActionGroup(d->mouseModeActionGroup);
713     d->aMouseMagnifier->setChecked(Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::Magnifier);
714 
715     // Mouse mode selection tools menu
716     d->aMouseModeMenu = new ToggleActionMenu(i18nc("@action", "Selection Tools"), this);
717 #if KWIDGETSADDONS_VERSION < QT_VERSION_CHECK(5, 77, 0)
718     d->aMouseModeMenu->setDelayed(false);
719     d->aMouseModeMenu->setStickyMenu(false);
720 #else
721     d->aMouseModeMenu->setPopupMode(QToolButton::MenuButtonPopup);
722 #endif
723     d->aMouseModeMenu->addAction(d->aMouseSelect);
724     d->aMouseModeMenu->addAction(d->aMouseTextSelect);
725     d->aMouseModeMenu->addAction(d->aMouseTableSelect);
726     connect(d->aMouseModeMenu->menu(), &QMenu::triggered, d->aMouseModeMenu, &ToggleActionMenu::setDefaultAction);
727     ac->addAction(QStringLiteral("mouse_selecttools"), d->aMouseModeMenu);
728 
729     switch (Okular::Settings::mouseMode()) {
730     case Okular::Settings::EnumMouseMode::TextSelect:
731         d->aMouseTextSelect->setChecked(true);
732         d->aMouseModeMenu->setDefaultAction(d->aMouseTextSelect);
733         break;
734     case Okular::Settings::EnumMouseMode::RectSelect:
735         d->aMouseSelect->setChecked(true);
736         d->aMouseModeMenu->setDefaultAction(d->aMouseSelect);
737         break;
738     case Okular::Settings::EnumMouseMode::TableSelect:
739         d->aMouseTableSelect->setChecked(true);
740         d->aMouseModeMenu->setDefaultAction(d->aMouseTableSelect);
741         break;
742     default:
743         d->aMouseModeMenu->setDefaultAction(d->aMouseTextSelect);
744     }
745 
746     // Create signature action
747     d->aSignature = new QAction(QIcon::fromTheme(QStringLiteral("document-edit-sign")), i18n("Digitally &Sign..."), this);
748     ac->addAction(QStringLiteral("add_digital_signature"), d->aSignature);
749     connect(d->aSignature, &QAction::triggered, this, &PageView::slotSignature);
750 
751     // speak actions
752 #ifdef HAVE_SPEECH
753     d->aSpeakDoc = new QAction(QIcon::fromTheme(QStringLiteral("text-speak")), i18n("Speak Whole Document"), this);
754     ac->addAction(QStringLiteral("speak_document"), d->aSpeakDoc);
755     d->aSpeakDoc->setEnabled(false);
756     connect(d->aSpeakDoc, &QAction::triggered, this, &PageView::slotSpeakDocument);
757 
758     d->aSpeakPage = new QAction(QIcon::fromTheme(QStringLiteral("text-speak")), i18n("Speak Current Page"), this);
759     ac->addAction(QStringLiteral("speak_current_page"), d->aSpeakPage);
760     d->aSpeakPage->setEnabled(false);
761     connect(d->aSpeakPage, &QAction::triggered, this, &PageView::slotSpeakCurrentPage);
762 
763     d->aSpeakStop = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-stop")), i18n("Stop Speaking"), this);
764     ac->addAction(QStringLiteral("speak_stop_all"), d->aSpeakStop);
765     d->aSpeakStop->setEnabled(false);
766     connect(d->aSpeakStop, &QAction::triggered, this, &PageView::slotStopSpeaks);
767 
768     d->aSpeakPauseResume = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-pause")), i18n("Pause/Resume Speaking"), this);
769     ac->addAction(QStringLiteral("speak_pause_resume"), d->aSpeakPauseResume);
770     d->aSpeakPauseResume->setEnabled(false);
771     connect(d->aSpeakPauseResume, &QAction::triggered, this, &PageView::slotPauseResumeSpeech);
772 #else
773     d->aSpeakDoc = nullptr;
774     d->aSpeakPage = nullptr;
775     d->aSpeakStop = nullptr;
776     d->aSpeakPauseResume = nullptr;
777 #endif
778 
779     // Other actions
780     QAction *su = new QAction(i18n("Scroll Up"), this);
781     ac->addAction(QStringLiteral("view_scroll_up"), su);
782     connect(su, &QAction::triggered, this, &PageView::slotAutoScrollUp);
783     ac->setDefaultShortcut(su, QKeySequence(Qt::SHIFT | Qt::Key_Up));
784     addAction(su);
785 
786     QAction *sd = new QAction(i18n("Scroll Down"), this);
787     ac->addAction(QStringLiteral("view_scroll_down"), sd);
788     connect(sd, &QAction::triggered, this, &PageView::slotAutoScrollDown);
789     ac->setDefaultShortcut(sd, QKeySequence(Qt::SHIFT | Qt::Key_Down));
790     addAction(sd);
791 
792     QAction *spu = new QAction(i18n("Scroll Page Up"), this);
793     ac->addAction(QStringLiteral("view_scroll_page_up"), spu);
794     connect(spu, &QAction::triggered, this, &PageView::slotScrollUp);
795     ac->setDefaultShortcut(spu, QKeySequence(Qt::SHIFT | Qt::Key_Space));
796     addAction(spu);
797 
798     QAction *spd = new QAction(i18n("Scroll Page Down"), this);
799     ac->addAction(QStringLiteral("view_scroll_page_down"), spd);
800     connect(spd, &QAction::triggered, this, &PageView::slotScrollDown);
801     ac->setDefaultShortcut(spd, QKeySequence(Qt::Key_Space));
802     addAction(spd);
803 
804     d->aToggleForms = new KToggleAction(i18n("Show Forms"), this);
805     ac->addAction(QStringLiteral("view_toggle_forms"), d->aToggleForms);
806     connect(d->aToggleForms, &QAction::toggled, this, &PageView::slotToggleForms);
807     d->aToggleForms->setEnabled(false);
808     toggleFormWidgets(false);
809 
810     // Setup undo and redo actions
811     QAction *kundo = KStandardAction::create(KStandardAction::Undo, d->document, SLOT(undo()), ac);
812     QAction *kredo = KStandardAction::create(KStandardAction::Redo, d->document, SLOT(redo()), ac);
813     connect(d->document, &Okular::Document::canUndoChanged, kundo, &QAction::setEnabled);
814     connect(d->document, &Okular::Document::canRedoChanged, kredo, &QAction::setEnabled);
815     kundo->setEnabled(false);
816     kredo->setEnabled(false);
817 
818     d->annotator = new PageViewAnnotator(this, d->document);
819     connect(d->annotator, &PageViewAnnotator::toolActive, this, [&](bool selected) {
820         if (selected) {
821             QAction *aMouseMode = d->mouseModeActionGroup->checkedAction();
822             if (aMouseMode) {
823                 aMouseMode->setChecked(false);
824             }
825         } else {
826             switch (d->mouseMode) {
827             case Okular::Settings::EnumMouseMode::Browse:
828                 d->aMouseNormal->setChecked(true);
829                 break;
830             case Okular::Settings::EnumMouseMode::Zoom:
831                 d->aMouseZoom->setChecked(true);
832                 break;
833             case Okular::Settings::EnumMouseMode::RectSelect:
834                 d->aMouseSelect->setChecked(true);
835                 break;
836             case Okular::Settings::EnumMouseMode::TableSelect:
837                 d->aMouseTableSelect->setChecked(true);
838                 break;
839             case Okular::Settings::EnumMouseMode::Magnifier:
840                 d->aMouseMagnifier->setChecked(true);
841                 break;
842             case Okular::Settings::EnumMouseMode::TextSelect:
843                 d->aMouseTextSelect->setChecked(true);
844                 break;
845             }
846         }
847     });
848     connect(d->annotator, &PageViewAnnotator::toolActive, d->mouseAnnotation, &MouseAnnotation::reset);
849     connect(d->annotator, &PageViewAnnotator::requestOpenFile, this, &PageView::requestOpenFile);
850     d->annotator->setupActions(ac);
851 }
852 
canFitPageWidth() const853 bool PageView::canFitPageWidth() const
854 {
855     return Okular::Settings::viewMode() != Okular::Settings::EnumViewMode::Single || d->zoomMode != ZoomFitWidth;
856 }
857 
fitPageWidth(int page)858 void PageView::fitPageWidth(int page)
859 {
860     // zoom: Fit Width, columns: 1. setActions + relayout + setPage + update
861     d->zoomMode = ZoomFitWidth;
862     Okular::Settings::setViewMode(Okular::Settings::EnumViewMode::Single);
863     d->aZoomFitWidth->setChecked(true);
864     d->aZoomFitPage->setChecked(false);
865     d->aZoomAutoFit->setChecked(false);
866     updateViewMode(Okular::Settings::EnumViewMode::Single);
867     viewport()->setUpdatesEnabled(false);
868     slotRelayoutPages();
869     viewport()->setUpdatesEnabled(true);
870     d->document->setViewportPage(page);
871     updateZoomText();
872     setFocus();
873 }
874 
openAnnotationWindow(Okular::Annotation * annotation,int pageNumber)875 void PageView::openAnnotationWindow(Okular::Annotation *annotation, int pageNumber)
876 {
877     if (!annotation)
878         return;
879 
880     // find the annot window
881     AnnotWindow *existWindow = nullptr;
882     for (AnnotWindow *aw : qAsConst(d->m_annowindows)) {
883         if (aw->annotation() == annotation) {
884             existWindow = aw;
885             break;
886         }
887     }
888 
889     if (existWindow == nullptr) {
890         existWindow = new AnnotWindow(this, annotation, d->document, pageNumber);
891         connect(existWindow, &QObject::destroyed, this, &PageView::slotAnnotationWindowDestroyed);
892 
893         d->m_annowindows << existWindow;
894     } else {
895         existWindow->raise();
896         existWindow->findChild<KTextEdit *>()->setFocus();
897     }
898 
899     existWindow->show();
900 }
901 
slotAnnotationWindowDestroyed(QObject * window)902 void PageView::slotAnnotationWindowDestroyed(QObject *window)
903 {
904     d->m_annowindows.remove(static_cast<AnnotWindow *>(window));
905 }
906 
displayMessage(const QString & message,const QString & details,PageViewMessage::Icon icon,int duration)907 void PageView::displayMessage(const QString &message, const QString &details, PageViewMessage::Icon icon, int duration)
908 {
909     if (!Okular::Settings::showOSD()) {
910         if (icon == PageViewMessage::Error) {
911             if (!details.isEmpty())
912                 KMessageBox::detailedError(this, message, details);
913             else
914                 KMessageBox::error(this, message);
915         }
916         return;
917     }
918 
919     // hide messageWindow if string is empty
920     if (message.isEmpty()) {
921         d->messageWindow->hide();
922         return;
923     }
924 
925     // display message (duration is length dependent)
926     if (duration == -1) {
927         duration = 500 + 100 * message.length();
928         if (!details.isEmpty())
929             duration += 500 + 100 * details.length();
930     }
931     d->messageWindow->display(message, details, icon, duration);
932 }
933 
reparseConfig()934 void PageView::reparseConfig()
935 {
936     // set smooth scrolling policies
937     PageView::updateSmoothScrollAnimationSpeed();
938 
939     // set the scroll bars policies
940     Qt::ScrollBarPolicy scrollBarMode = Okular::Settings::showScrollBars() ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff;
941     if (horizontalScrollBarPolicy() != scrollBarMode) {
942         setHorizontalScrollBarPolicy(scrollBarMode);
943         setVerticalScrollBarPolicy(scrollBarMode);
944     }
945 
946     if (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Summary && ((int)Okular::Settings::viewColumns() != d->setting_viewCols)) {
947         d->setting_viewCols = Okular::Settings::viewColumns();
948 
949         slotRelayoutPages();
950     }
951 
952     if (Okular::Settings::rtlReadingDirection() != d->rtl_Mode) {
953         d->rtl_Mode = Okular::Settings::rtlReadingDirection();
954         slotRelayoutPages();
955     }
956 
957     updatePageStep();
958 
959     if (d->annotator)
960         d->annotator->reparseConfig();
961 
962     // Something like invert colors may have changed
963     // As we don't have a way to find out the old value
964     // We just update the viewport, this shouldn't be that bad
965     // since it's just a repaint of pixmaps we already have
966     viewport()->update();
967 }
968 
actionCollection() const969 KActionCollection *PageView::actionCollection() const
970 {
971     return d->actionCollection;
972 }
973 
toggleFormsAction() const974 QAction *PageView::toggleFormsAction() const
975 {
976     return d->aToggleForms;
977 }
978 
contentAreaWidth() const979 int PageView::contentAreaWidth() const
980 {
981     return horizontalScrollBar()->maximum() + viewport()->width();
982 }
983 
contentAreaHeight() const984 int PageView::contentAreaHeight() const
985 {
986     return verticalScrollBar()->maximum() + viewport()->height();
987 }
988 
contentAreaPosition() const989 QPoint PageView::contentAreaPosition() const
990 {
991     return QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value());
992 }
993 
contentAreaPoint(const QPoint pos) const994 QPoint PageView::contentAreaPoint(const QPoint pos) const
995 {
996     return pos + contentAreaPosition();
997 }
998 
contentAreaPoint(const QPointF pos) const999 QPointF PageView::contentAreaPoint(const QPointF pos) const
1000 {
1001     return pos + contentAreaPosition();
1002 }
1003 
selectedText() const1004 QString PageViewPrivate::selectedText() const
1005 {
1006     if (pagesWithTextSelection.isEmpty())
1007         return QString();
1008 
1009     QString text;
1010     QList<int> selpages = pagesWithTextSelection.values();
1011     std::sort(selpages.begin(), selpages.end());
1012     const Okular::Page *pg = nullptr;
1013     if (selpages.count() == 1) {
1014         pg = document->page(selpages.first());
1015         text.append(pg->text(pg->textSelection(), Okular::TextPage::CentralPixelTextAreaInclusionBehaviour));
1016     } else {
1017         pg = document->page(selpages.first());
1018         text.append(pg->text(pg->textSelection(), Okular::TextPage::CentralPixelTextAreaInclusionBehaviour));
1019         int end = selpages.count() - 1;
1020         for (int i = 1; i < end; ++i) {
1021             pg = document->page(selpages.at(i));
1022             text.append(pg->text(nullptr, Okular::TextPage::CentralPixelTextAreaInclusionBehaviour));
1023         }
1024         pg = document->page(selpages.last());
1025         text.append(pg->text(pg->textSelection(), Okular::TextPage::CentralPixelTextAreaInclusionBehaviour));
1026     }
1027 
1028     if (text.endsWith('\n')) {
1029         text.chop(1);
1030     }
1031     return text;
1032 }
1033 
getTableContents() const1034 QMimeData *PageView::getTableContents() const
1035 {
1036     QString selText;
1037     QString selHtml;
1038     QList<double> xs = d->tableSelectionCols;
1039     QList<double> ys = d->tableSelectionRows;
1040     xs.prepend(0.0);
1041     xs.append(1.0);
1042     ys.prepend(0.0);
1043     ys.append(1.0);
1044     selHtml = QString::fromLatin1(
1045         "<html><head>"
1046         "<meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">"
1047         "</head><body><table>");
1048     for (int r = 0; r + 1 < ys.length(); r++) {
1049         selHtml += QLatin1String("<tr>");
1050         for (int c = 0; c + 1 < xs.length(); c++) {
1051             Okular::NormalizedRect cell(xs[c], ys[r], xs[c + 1], ys[r + 1]);
1052             if (c)
1053                 selText += QLatin1Char('\t');
1054             QString txt;
1055             for (const TableSelectionPart &tsp : qAsConst(d->tableSelectionParts)) {
1056                 // first, crop the cell to this part
1057                 if (!tsp.rectInSelection.intersects(cell))
1058                     continue;
1059                 Okular::NormalizedRect cellPart = tsp.rectInSelection & cell; // intersection
1060 
1061                 // second, convert it from table coordinates to part coordinates
1062                 cellPart.left -= tsp.rectInSelection.left;
1063                 cellPart.left /= (tsp.rectInSelection.right - tsp.rectInSelection.left);
1064                 cellPart.right -= tsp.rectInSelection.left;
1065                 cellPart.right /= (tsp.rectInSelection.right - tsp.rectInSelection.left);
1066                 cellPart.top -= tsp.rectInSelection.top;
1067                 cellPart.top /= (tsp.rectInSelection.bottom - tsp.rectInSelection.top);
1068                 cellPart.bottom -= tsp.rectInSelection.top;
1069                 cellPart.bottom /= (tsp.rectInSelection.bottom - tsp.rectInSelection.top);
1070 
1071                 // third, convert from part coordinates to item coordinates
1072                 cellPart.left *= (tsp.rectInItem.right - tsp.rectInItem.left);
1073                 cellPart.left += tsp.rectInItem.left;
1074                 cellPart.right *= (tsp.rectInItem.right - tsp.rectInItem.left);
1075                 cellPart.right += tsp.rectInItem.left;
1076                 cellPart.top *= (tsp.rectInItem.bottom - tsp.rectInItem.top);
1077                 cellPart.top += tsp.rectInItem.top;
1078                 cellPart.bottom *= (tsp.rectInItem.bottom - tsp.rectInItem.top);
1079                 cellPart.bottom += tsp.rectInItem.top;
1080 
1081                 // now get the text
1082                 Okular::RegularAreaRect rects;
1083                 rects.append(cellPart);
1084                 txt += tsp.item->page()->text(&rects, Okular::TextPage::CentralPixelTextAreaInclusionBehaviour);
1085             }
1086             QString html = txt;
1087             selText += txt.replace(QLatin1Char('\n'), QLatin1Char(' '));
1088             html.replace(QLatin1Char('&'), QLatin1String("&amp;")).replace(QLatin1Char('<'), QLatin1String("&lt;")).replace(QLatin1Char('>'), QLatin1String("&gt;"));
1089             // Remove newlines, do not turn them into <br>, because
1090             // Excel interprets <br> within cell as new cell...
1091             html.replace(QLatin1Char('\n'), QLatin1String(" "));
1092             selHtml += QStringLiteral("<td>") + html + QStringLiteral("</td>");
1093         }
1094         selText += QLatin1Char('\n');
1095         selHtml += QLatin1String("</tr>\n");
1096     }
1097     selHtml += QLatin1String("</table></body></html>\n");
1098 
1099     QMimeData *md = new QMimeData();
1100     md->setText(selText);
1101     md->setHtml(selHtml);
1102 
1103     return md;
1104 }
1105 
copyTextSelection() const1106 void PageView::copyTextSelection() const
1107 {
1108     switch (d->mouseMode) {
1109     case Okular::Settings::EnumMouseMode::TableSelect: {
1110         QClipboard *cb = QApplication::clipboard();
1111         cb->setMimeData(getTableContents(), QClipboard::Clipboard);
1112     } break;
1113 
1114     case Okular::Settings::EnumMouseMode::TextSelect: {
1115         const QString text = d->selectedText();
1116         if (!text.isEmpty()) {
1117             QClipboard *cb = QApplication::clipboard();
1118             cb->setText(text, QClipboard::Clipboard);
1119         }
1120     } break;
1121     }
1122 }
1123 
selectAll()1124 void PageView::selectAll()
1125 {
1126     for (const PageViewItem *item : qAsConst(d->items)) {
1127         Okular::RegularAreaRect *area = textSelectionForItem(item);
1128         d->pagesWithTextSelection.insert(item->pageNumber());
1129         d->document->setPageTextSelection(item->pageNumber(), area, palette().color(QPalette::Active, QPalette::Highlight));
1130     }
1131 }
1132 
createAnnotationsVideoWidgets(PageViewItem * item,const QLinkedList<Okular::Annotation * > & annotations)1133 void PageView::createAnnotationsVideoWidgets(PageViewItem *item, const QLinkedList<Okular::Annotation *> &annotations)
1134 {
1135     qDeleteAll(item->videoWidgets());
1136     item->videoWidgets().clear();
1137 
1138     for (Okular::Annotation *a : annotations) {
1139         if (a->subType() == Okular::Annotation::AMovie) {
1140             Okular::MovieAnnotation *movieAnn = static_cast<Okular::MovieAnnotation *>(a);
1141             VideoWidget *vw = new VideoWidget(movieAnn, movieAnn->movie(), d->document, viewport());
1142             item->videoWidgets().insert(movieAnn->movie(), vw);
1143             vw->pageInitialized();
1144         } else if (a->subType() == Okular::Annotation::ARichMedia) {
1145             Okular::RichMediaAnnotation *richMediaAnn = static_cast<Okular::RichMediaAnnotation *>(a);
1146             VideoWidget *vw = new VideoWidget(richMediaAnn, richMediaAnn->movie(), d->document, viewport());
1147             item->videoWidgets().insert(richMediaAnn->movie(), vw);
1148             vw->pageInitialized();
1149         } else if (a->subType() == Okular::Annotation::AScreen) {
1150             const Okular::ScreenAnnotation *screenAnn = static_cast<Okular::ScreenAnnotation *>(a);
1151             Okular::Movie *movie = GuiUtils::renditionMovieFromScreenAnnotation(screenAnn);
1152             if (movie) {
1153                 VideoWidget *vw = new VideoWidget(screenAnn, movie, d->document, viewport());
1154                 item->videoWidgets().insert(movie, vw);
1155                 vw->pageInitialized();
1156             }
1157         }
1158     }
1159 }
1160 
1161 // BEGIN DocumentObserver inherited methods
notifySetup(const QVector<Okular::Page * > & pageSet,int setupFlags)1162 void PageView::notifySetup(const QVector<Okular::Page *> &pageSet, int setupFlags)
1163 {
1164     bool documentChanged = setupFlags & Okular::DocumentObserver::DocumentChanged;
1165     const bool allowfillforms = d->document->isAllowed(Okular::AllowFillForms);
1166 
1167     // reuse current pages if nothing new
1168     if ((pageSet.count() == d->items.count()) && !documentChanged && !(setupFlags & Okular::DocumentObserver::NewLayoutForPages)) {
1169         int count = pageSet.count();
1170         for (int i = 0; (i < count) && !documentChanged; i++) {
1171             if ((int)pageSet[i]->number() != d->items[i]->pageNumber()) {
1172                 documentChanged = true;
1173             } else {
1174                 // even if the document has not changed, allowfillforms may have
1175                 // changed, so update all fields' "canBeFilled" flag
1176                 const QSet<FormWidgetIface *> formWidgetsList = d->items[i]->formWidgets();
1177                 for (FormWidgetIface *w : formWidgetsList)
1178                     w->setCanBeFilled(allowfillforms);
1179             }
1180         }
1181 
1182         if (!documentChanged) {
1183             if (setupFlags & Okular::DocumentObserver::UrlChanged) {
1184                 // Here with UrlChanged and no document changed it means we
1185                 // need to update all the Annotation* and Form* otherwise
1186                 // they still point to the old document ones, luckily the old ones are still
1187                 // around so we can look for the new ones using unique ids, etc
1188                 d->mouseAnnotation->updateAnnotationPointers();
1189 
1190                 for (AnnotWindow *aw : qAsConst(d->m_annowindows)) {
1191                     Okular::Annotation *newA = d->document->page(aw->pageNumber())->annotation(aw->annotation()->uniqueName());
1192                     aw->updateAnnotation(newA);
1193                 }
1194 
1195                 const QRect viewportRect(horizontalScrollBar()->value(), verticalScrollBar()->value(), viewport()->width(), viewport()->height());
1196                 for (int i = 0; i < count; i++) {
1197                     PageViewItem *item = d->items[i];
1198                     const QSet<FormWidgetIface *> fws = item->formWidgets();
1199                     for (FormWidgetIface *w : fws) {
1200                         Okular::FormField *f = Okular::PagePrivate::findEquivalentForm(d->document->page(i), w->formField());
1201                         if (f) {
1202                             w->setFormField(f);
1203                         } else {
1204                             qWarning() << "Lost form field on document save, something is wrong";
1205                             item->formWidgets().remove(w);
1206                             delete w;
1207                         }
1208                     }
1209 
1210                     // For the video widgets we don't really care about reusing them since they don't contain much info so just
1211                     // create them again
1212                     createAnnotationsVideoWidgets(item, pageSet[i]->annotations());
1213                     const QHash<Okular::Movie *, VideoWidget *> videoWidgets = item->videoWidgets();
1214                     for (VideoWidget *vw : videoWidgets) {
1215                         const Okular::NormalizedRect r = vw->normGeometry();
1216                         vw->setGeometry(qRound(item->uncroppedGeometry().left() + item->uncroppedWidth() * r.left) + 1 - viewportRect.left(),
1217                                         qRound(item->uncroppedGeometry().top() + item->uncroppedHeight() * r.top) + 1 - viewportRect.top(),
1218                                         qRound(fabs(r.right - r.left) * item->uncroppedGeometry().width()),
1219                                         qRound(fabs(r.bottom - r.top) * item->uncroppedGeometry().height()));
1220 
1221                         // Workaround, otherwise the size somehow gets lost
1222                         vw->show();
1223                         vw->hide();
1224                     }
1225                 }
1226             }
1227 
1228             return;
1229         }
1230     }
1231 
1232     // mouseAnnotation must not access our PageViewItem widgets any longer
1233     d->mouseAnnotation->reset();
1234 
1235     // delete all widgets (one for each page in pageSet)
1236     qDeleteAll(d->items);
1237     d->items.clear();
1238     d->visibleItems.clear();
1239     d->pagesWithTextSelection.clear();
1240     toggleFormWidgets(false);
1241     if (d->formsWidgetController)
1242         d->formsWidgetController->dropRadioButtons();
1243 
1244     bool haspages = !pageSet.isEmpty();
1245     bool hasformwidgets = false;
1246     // create children widgets
1247     for (const Okular::Page *page : pageSet) {
1248         PageViewItem *item = new PageViewItem(page);
1249         d->items.push_back(item);
1250 #ifdef PAGEVIEW_DEBUG
1251         qCDebug(OkularUiDebug).nospace() << "cropped geom for " << d->items.last()->pageNumber() << " is " << d->items.last()->croppedGeometry();
1252 #endif
1253         const QLinkedList<Okular::FormField *> pageFields = page->formFields();
1254         for (Okular::FormField *ff : pageFields) {
1255             FormWidgetIface *w = FormWidgetFactory::createWidget(ff, viewport());
1256             if (w) {
1257                 w->setPageItem(item);
1258                 w->setFormWidgetsController(d->formWidgetsController());
1259                 w->setVisibility(false);
1260                 w->setCanBeFilled(allowfillforms);
1261                 item->formWidgets().insert(w);
1262                 hasformwidgets = true;
1263             }
1264         }
1265 
1266         createAnnotationsVideoWidgets(item, page->annotations());
1267     }
1268 
1269     // invalidate layout so relayout/repaint will happen on next viewport change
1270     if (haspages) {
1271         // We do a delayed call to slotRelayoutPages but also set the dirtyLayout
1272         // because we might end up in notifyViewportChanged while slotRelayoutPages
1273         // has not been done and we don't want that to happen
1274         d->dirtyLayout = true;
1275         QMetaObject::invokeMethod(this, "slotRelayoutPages", Qt::QueuedConnection);
1276     } else {
1277         // update the mouse cursor when closing because we may have close through a link and
1278         // want the cursor to come back to the normal cursor
1279         updateCursor();
1280         // then, make the message window and scrollbars disappear, and trigger a repaint
1281         d->messageWindow->hide();
1282         resizeContentArea(QSize(0, 0));
1283         viewport()->update(); // when there is no change to the scrollbars, no repaint would
1284                               // be done and the old document would still be shown
1285     }
1286 
1287     // OSD (Message balloons) to display pages
1288     if (documentChanged && pageSet.count() > 0)
1289         d->messageWindow->display(i18np(" Loaded a one-page document.", " Loaded a %1-page document.", pageSet.count()), QString(), PageViewMessage::Info, 4000);
1290 
1291     updateActionState(haspages, hasformwidgets);
1292 
1293     // We need to assign it to a different list otherwise slotAnnotationWindowDestroyed
1294     // will bite us and clear d->m_annowindows
1295     QSet<AnnotWindow *> annowindows = d->m_annowindows;
1296     d->m_annowindows.clear();
1297     qDeleteAll(annowindows);
1298 
1299     selectionClear();
1300 }
1301 
updateActionState(bool haspages,bool hasformwidgets)1302 void PageView::updateActionState(bool haspages, bool hasformwidgets)
1303 {
1304     if (d->aTrimMargins)
1305         d->aTrimMargins->setEnabled(haspages);
1306 
1307     if (d->aTrimToSelection)
1308         d->aTrimToSelection->setEnabled(haspages);
1309 
1310     if (d->aViewModeMenu)
1311         d->aViewModeMenu->setEnabled(haspages);
1312 
1313     if (d->aViewContinuous)
1314         d->aViewContinuous->setEnabled(haspages);
1315 
1316     updateZoomActionsEnabledStatus();
1317 
1318     if (d->aColorModeMenu)
1319         d->aColorModeMenu->setEnabled(haspages);
1320 
1321     if (d->aReadingDirection) {
1322         d->aReadingDirection->setEnabled(haspages);
1323     }
1324 
1325     if (d->mouseModeActionGroup)
1326         d->mouseModeActionGroup->setEnabled(haspages);
1327     if (d->aMouseModeMenu)
1328         d->aMouseModeMenu->setEnabled(haspages);
1329 
1330     if (d->aRotateClockwise)
1331         d->aRotateClockwise->setEnabled(haspages);
1332     if (d->aRotateCounterClockwise)
1333         d->aRotateCounterClockwise->setEnabled(haspages);
1334     if (d->aRotateOriginal)
1335         d->aRotateOriginal->setEnabled(haspages);
1336     if (d->aToggleForms) { // may be null if dummy mode is on
1337         d->aToggleForms->setEnabled(haspages && hasformwidgets);
1338     }
1339     bool allowAnnotations = d->document->isAllowed(Okular::AllowNotes);
1340     if (d->annotator) {
1341         bool allowTools = haspages && allowAnnotations;
1342         d->annotator->setToolsEnabled(allowTools);
1343         d->annotator->setTextToolsEnabled(allowTools && d->document->supportsSearching());
1344     }
1345 
1346     if (d->aSignature) {
1347         const bool canSign = d->document->canSign();
1348         d->aSignature->setEnabled(canSign && haspages);
1349     }
1350 
1351 #ifdef HAVE_SPEECH
1352     if (d->aSpeakDoc) {
1353         const bool enablettsactions = haspages ? Okular::Settings::useTTS() : false;
1354         d->aSpeakDoc->setEnabled(enablettsactions);
1355         d->aSpeakPage->setEnabled(enablettsactions);
1356     }
1357 #endif
1358     if (d->aMouseMagnifier)
1359         d->aMouseMagnifier->setEnabled(d->document->supportsTiles());
1360     if (d->aFitWindowToPage)
1361         d->aFitWindowToPage->setEnabled(haspages && !getContinuousMode());
1362 }
1363 
setupActionsPostGUIActivated()1364 void PageView::setupActionsPostGUIActivated()
1365 {
1366     d->annotator->setupActionsPostGUIActivated();
1367 }
1368 
areSourceLocationsShownGraphically() const1369 bool PageView::areSourceLocationsShownGraphically() const
1370 {
1371     return Okular::Settings::showSourceLocationsGraphically();
1372 }
1373 
setShowSourceLocationsGraphically(bool show)1374 void PageView::setShowSourceLocationsGraphically(bool show)
1375 {
1376     if (show == Okular::Settings::showSourceLocationsGraphically()) {
1377         return;
1378     }
1379     Okular::Settings::setShowSourceLocationsGraphically(show);
1380     viewport()->update();
1381 }
1382 
setLastSourceLocationViewport(const Okular::DocumentViewport & vp)1383 void PageView::setLastSourceLocationViewport(const Okular::DocumentViewport &vp)
1384 {
1385     if (vp.rePos.enabled) {
1386         d->lastSourceLocationViewportNormalizedX = normClamp(vp.rePos.normalizedX, 0.5);
1387         d->lastSourceLocationViewportNormalizedY = normClamp(vp.rePos.normalizedY, 0.0);
1388     } else {
1389         d->lastSourceLocationViewportNormalizedX = 0.5;
1390         d->lastSourceLocationViewportNormalizedY = 0.0;
1391     }
1392     d->lastSourceLocationViewportPageNumber = vp.pageNumber;
1393     viewport()->update();
1394 }
1395 
clearLastSourceLocationViewport()1396 void PageView::clearLastSourceLocationViewport()
1397 {
1398     d->lastSourceLocationViewportPageNumber = -1;
1399     d->lastSourceLocationViewportNormalizedX = 0.0;
1400     d->lastSourceLocationViewportNormalizedY = 0.0;
1401     viewport()->update();
1402 }
1403 
notifyViewportChanged(bool smoothMove)1404 void PageView::notifyViewportChanged(bool smoothMove)
1405 {
1406     QMetaObject::invokeMethod(this, "slotRealNotifyViewportChanged", Qt::QueuedConnection, Q_ARG(bool, smoothMove));
1407 }
1408 
slotRealNotifyViewportChanged(bool smoothMove)1409 void PageView::slotRealNotifyViewportChanged(bool smoothMove)
1410 {
1411     // if we are the one changing viewport, skip this notify
1412     if (d->blockViewport)
1413         return;
1414 
1415     // block setViewport outgoing calls
1416     d->blockViewport = true;
1417 
1418     // find PageViewItem matching the viewport description
1419     const Okular::DocumentViewport &vp = d->document->viewport();
1420     const PageViewItem *item = nullptr;
1421     for (const PageViewItem *tmpItem : qAsConst(d->items))
1422         if (tmpItem->pageNumber() == vp.pageNumber) {
1423             item = tmpItem;
1424             break;
1425         }
1426     if (!item) {
1427         qCWarning(OkularUiDebug) << "viewport for page" << vp.pageNumber << "has no matching item!";
1428         d->blockViewport = false;
1429         return;
1430     }
1431 #ifdef PAGEVIEW_DEBUG
1432     qCDebug(OkularUiDebug) << "document viewport changed";
1433 #endif
1434     // relayout in "Single Pages" mode or if a relayout is pending
1435     d->blockPixmapsRequest = true;
1436     if (!getContinuousMode() || d->dirtyLayout)
1437         slotRelayoutPages();
1438 
1439     // restore viewport center or use default {x-center,v-top} alignment
1440     const QPoint centerCoord = viewportToContentArea(vp);
1441 
1442     // if smooth movement requested, setup parameters and start it
1443     center(centerCoord.x(), centerCoord.y(), smoothMove);
1444 
1445     d->blockPixmapsRequest = false;
1446 
1447     // request visible pixmaps in the current viewport and recompute it
1448     slotRequestVisiblePixmaps();
1449 
1450     // enable setViewport calls
1451     d->blockViewport = false;
1452 
1453     if (viewport()) {
1454         viewport()->update();
1455     }
1456 
1457     // since the page has moved below cursor, update it
1458     updateCursor();
1459 }
1460 
notifyPageChanged(int pageNumber,int changedFlags)1461 void PageView::notifyPageChanged(int pageNumber, int changedFlags)
1462 {
1463     // only handle pixmap / highlight changes notifies
1464     if (changedFlags & DocumentObserver::Bookmark)
1465         return;
1466 
1467     if (changedFlags & DocumentObserver::Annotations) {
1468         const QLinkedList<Okular::Annotation *> annots = d->document->page(pageNumber)->annotations();
1469         const QLinkedList<Okular::Annotation *>::ConstIterator annItEnd = annots.end();
1470         QSet<AnnotWindow *>::Iterator it = d->m_annowindows.begin();
1471         for (; it != d->m_annowindows.end();) {
1472             QLinkedList<Okular::Annotation *>::ConstIterator annIt = std::find(annots.begin(), annots.end(), (*it)->annotation());
1473             if (annIt != annItEnd) {
1474                 (*it)->reloadInfo();
1475                 ++it;
1476             } else {
1477                 AnnotWindow *w = *it;
1478                 it = d->m_annowindows.erase(it);
1479                 // Need to delete after removing from the list
1480                 // otherwise deleting will call slotAnnotationWindowDestroyed which will mess
1481                 // the list and the iterators
1482                 delete w;
1483             }
1484         }
1485 
1486         d->mouseAnnotation->notifyAnnotationChanged(pageNumber);
1487     }
1488 
1489     if (changedFlags & DocumentObserver::BoundingBox) {
1490 #ifdef PAGEVIEW_DEBUG
1491         qCDebug(OkularUiDebug) << "BoundingBox change on page" << pageNumber;
1492 #endif
1493         slotRelayoutPages();
1494         slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already!
1495         // Repaint the whole widget since layout may have changed
1496         viewport()->update();
1497         return;
1498     }
1499 
1500     // iterate over visible items: if page(pageNumber) is one of them, repaint it
1501     for (const PageViewItem *visibleItem : qAsConst(d->visibleItems))
1502         if (visibleItem->pageNumber() == pageNumber && visibleItem->isVisible()) {
1503             // update item's rectangle plus the little outline
1504             QRect expandedRect = visibleItem->croppedGeometry();
1505             // a PageViewItem is placed in the global page layout,
1506             // while we need to map its position in the viewport coordinates
1507             // (to get the correct area to repaint)
1508             expandedRect.translate(-contentAreaPosition());
1509             expandedRect.adjust(-1, -1, 3, 3);
1510             viewport()->update(expandedRect);
1511 
1512             // if we were "zoom-dragging" do not overwrite the "zoom-drag" cursor
1513             if (cursor().shape() != Qt::SizeVerCursor) {
1514                 // since the page has been regenerated below cursor, update it
1515                 updateCursor();
1516             }
1517             break;
1518         }
1519 }
1520 
notifyContentsCleared(int changedFlags)1521 void PageView::notifyContentsCleared(int changedFlags)
1522 {
1523     // if pixmaps were cleared, re-ask them
1524     if (changedFlags & DocumentObserver::Pixmap)
1525         QMetaObject::invokeMethod(this, "slotRequestVisiblePixmaps", Qt::QueuedConnection);
1526 }
1527 
notifyZoom(int factor)1528 void PageView::notifyZoom(int factor)
1529 {
1530     if (factor > 0)
1531         updateZoom(ZoomIn);
1532     else
1533         updateZoom(ZoomOut);
1534 }
1535 
canUnloadPixmap(int pageNumber) const1536 bool PageView::canUnloadPixmap(int pageNumber) const
1537 {
1538     if (Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Low || Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Normal) {
1539         // if the item is visible, forbid unloading
1540         for (const PageViewItem *visibleItem : qAsConst(d->visibleItems))
1541             if (visibleItem->pageNumber() == pageNumber)
1542                 return false;
1543     } else {
1544         // forbid unloading of the visible items, and of the previous and next
1545         for (const PageViewItem *visibleItem : qAsConst(d->visibleItems))
1546             if (abs(visibleItem->pageNumber() - pageNumber) <= 1)
1547                 return false;
1548     }
1549     // if hidden premit unloading
1550     return true;
1551 }
1552 
notifyCurrentPageChanged(int previous,int current)1553 void PageView::notifyCurrentPageChanged(int previous, int current)
1554 {
1555     if (previous != -1) {
1556         PageViewItem *item = d->items.at(previous);
1557         if (item) {
1558             const QHash<Okular::Movie *, VideoWidget *> videoWidgetsList = item->videoWidgets();
1559             for (VideoWidget *videoWidget : videoWidgetsList)
1560                 videoWidget->pageLeft();
1561         }
1562 
1563         // On close, run the widget scripts, needed for running animated PDF
1564         const Okular::Page *page = d->document->page(previous);
1565         const QLinkedList<Okular::Annotation *> annotations = page->annotations();
1566         for (Okular::Annotation *annotation : annotations) {
1567             if (annotation->subType() == Okular::Annotation::AWidget) {
1568                 Okular::WidgetAnnotation *widgetAnnotation = static_cast<Okular::WidgetAnnotation *>(annotation);
1569                 d->document->processAction(widgetAnnotation->additionalAction(Okular::Annotation::PageClosing));
1570             }
1571         }
1572     }
1573 
1574     if (current != -1) {
1575         PageViewItem *item = d->items.at(current);
1576         if (item) {
1577             const QHash<Okular::Movie *, VideoWidget *> videoWidgetsList = item->videoWidgets();
1578             for (VideoWidget *videoWidget : videoWidgetsList)
1579                 videoWidget->pageEntered();
1580         }
1581 
1582         // update zoom text and factor if in a ZoomFit/* zoom mode
1583         if (d->zoomMode != ZoomFixed)
1584             updateZoomText();
1585 
1586         // Opening any widget scripts, needed for running animated PDF
1587         const Okular::Page *page = d->document->page(current);
1588         const QLinkedList<Okular::Annotation *> annotations = page->annotations();
1589         for (Okular::Annotation *annotation : annotations) {
1590             if (annotation->subType() == Okular::Annotation::AWidget) {
1591                 Okular::WidgetAnnotation *widgetAnnotation = static_cast<Okular::WidgetAnnotation *>(annotation);
1592                 d->document->processAction(widgetAnnotation->additionalAction(Okular::Annotation::PageOpening));
1593             }
1594         }
1595     }
1596 
1597     // if the view is paged (or not continuous) and there is a selected annotation,
1598     // we call reset to avoid creating an artifact in the next page.
1599     if (!getContinuousMode()) {
1600         if (d->mouseAnnotation && d->mouseAnnotation->isFocused()) {
1601             d->mouseAnnotation->reset();
1602         }
1603     }
1604 }
1605 
1606 // END DocumentObserver inherited methods
1607 
1608 // BEGIN View inherited methods
supportsCapability(ViewCapability capability) const1609 bool PageView::supportsCapability(ViewCapability capability) const
1610 {
1611     switch (capability) {
1612     case Zoom:
1613     case ZoomModality:
1614     case Continuous:
1615     case ViewModeModality:
1616     case TrimMargins:
1617         return true;
1618     }
1619     return false;
1620 }
1621 
capabilityFlags(ViewCapability capability) const1622 Okular::View::CapabilityFlags PageView::capabilityFlags(ViewCapability capability) const
1623 {
1624     switch (capability) {
1625     case Zoom:
1626     case ZoomModality:
1627     case Continuous:
1628     case ViewModeModality:
1629     case TrimMargins:
1630         return CapabilityRead | CapabilityWrite | CapabilitySerializable;
1631     }
1632     return NoFlag;
1633 }
1634 
capability(ViewCapability capability) const1635 QVariant PageView::capability(ViewCapability capability) const
1636 {
1637     switch (capability) {
1638     case Zoom:
1639         return d->zoomFactor;
1640     case ZoomModality:
1641         return d->zoomMode;
1642     case Continuous:
1643         return getContinuousMode();
1644     case ViewModeModality: {
1645         if (d->viewModeActionGroup) {
1646             const QList<QAction *> actions = d->viewModeActionGroup->actions();
1647             for (const QAction *action : actions) {
1648                 if (action->isChecked()) {
1649                     return action->data();
1650                 }
1651             }
1652         }
1653         return QVariant();
1654     }
1655     case TrimMargins:
1656         return d->aTrimMargins ? d->aTrimMargins->isChecked() : false;
1657     }
1658     return QVariant();
1659 }
1660 
setCapability(ViewCapability capability,const QVariant & option)1661 void PageView::setCapability(ViewCapability capability, const QVariant &option)
1662 {
1663     switch (capability) {
1664     case Zoom: {
1665         bool ok = true;
1666         double factor = option.toDouble(&ok);
1667         if (ok && factor > 0.0) {
1668             d->zoomFactor = static_cast<float>(factor);
1669             updateZoom(ZoomRefreshCurrent);
1670         }
1671         break;
1672     }
1673     case ZoomModality: {
1674         bool ok = true;
1675         int mode = option.toInt(&ok);
1676         if (ok) {
1677             if (mode >= 0 && mode < 3)
1678                 updateZoom((ZoomMode)mode);
1679         }
1680         break;
1681     }
1682     case ViewModeModality: {
1683         bool ok = true;
1684         int mode = option.toInt(&ok);
1685         if (ok) {
1686             if (mode >= 0 && mode < Okular::Settings::EnumViewMode::COUNT)
1687                 updateViewMode(mode);
1688         }
1689         break;
1690     }
1691     case Continuous: {
1692         bool mode = option.toBool();
1693         d->aViewContinuous->setChecked(mode);
1694         break;
1695     }
1696     case TrimMargins: {
1697         bool value = option.toBool();
1698         d->aTrimMargins->setChecked(value);
1699         slotTrimMarginsToggled(value);
1700         break;
1701     }
1702     }
1703 }
1704 
1705 // END View inherited methods
1706 
1707 // BEGIN widget events
event(QEvent * event)1708 bool PageView::event(QEvent *event)
1709 {
1710     if (event->type() == QEvent::Gesture) {
1711         return gestureEvent(static_cast<QGestureEvent *>(event));
1712     }
1713 
1714     // do not stop the event
1715     return QAbstractScrollArea::event(event);
1716 }
1717 
gestureEvent(QGestureEvent * event)1718 bool PageView::gestureEvent(QGestureEvent *event)
1719 {
1720     QPinchGesture *pinch = static_cast<QPinchGesture *>(event->gesture(Qt::PinchGesture));
1721 
1722     if (pinch) {
1723         // Viewport zoom level at the moment where the pinch gesture starts.
1724         // The viewport zoom level _during_ the gesture will be this value
1725         // times the relative zoom reported by QGestureEvent.
1726         static qreal vanillaZoom = d->zoomFactor;
1727 
1728         if (pinch->state() == Qt::GestureStarted) {
1729             vanillaZoom = d->zoomFactor;
1730         }
1731 
1732         const QPinchGesture::ChangeFlags changeFlags = pinch->changeFlags();
1733 
1734         // Zoom
1735         if (pinch->changeFlags() & QPinchGesture::ScaleFactorChanged) {
1736             d->zoomFactor = vanillaZoom * pinch->totalScaleFactor();
1737 
1738             d->blockPixmapsRequest = true;
1739             updateZoom(ZoomRefreshCurrent);
1740             d->blockPixmapsRequest = false;
1741             viewport()->update();
1742         }
1743 
1744         // Count the number of 90-degree rotations we did since the start of the pinch gesture.
1745         // Otherwise a pinch turned to 90 degrees and held there will rotate the page again and again.
1746         static int rotations = 0;
1747 
1748         if (changeFlags & QPinchGesture::RotationAngleChanged) {
1749             // Rotation angle relative to the accumulated page rotations triggered by the current pinch
1750             // We actually turn at 80 degrees rather than at 90 degrees.  That's less strain on the hands.
1751             const qreal relativeAngle = pinch->rotationAngle() - rotations * 90;
1752             if (relativeAngle > 80) {
1753                 slotRotateClockwise();
1754                 rotations++;
1755             }
1756             if (relativeAngle < -80) {
1757                 slotRotateCounterClockwise();
1758                 rotations--;
1759             }
1760         }
1761 
1762         if (pinch->state() == Qt::GestureFinished) {
1763             rotations = 0;
1764         }
1765 
1766         return true;
1767     }
1768 
1769     return false;
1770 }
1771 
paintEvent(QPaintEvent * pe)1772 void PageView::paintEvent(QPaintEvent *pe)
1773 {
1774     const QPoint areaPos = contentAreaPosition();
1775     // create the rect into contents from the clipped screen rect
1776     QRect viewportRect = viewport()->rect();
1777     viewportRect.translate(areaPos);
1778     QRect contentsRect = pe->rect().translated(areaPos).intersected(viewportRect);
1779     if (!contentsRect.isValid())
1780         return;
1781 
1782 #ifdef PAGEVIEW_DEBUG
1783     qCDebug(OkularUiDebug) << "paintevent" << contentsRect;
1784 #endif
1785 
1786     // create the screen painter. a pixel painted at contentsX,contentsY
1787     // appears to the top-left corner of the scrollview.
1788     QPainter screenPainter(viewport());
1789     // translate to simulate the scrolled content widget
1790     screenPainter.translate(-areaPos);
1791 
1792     // selectionRect is the normalized mouse selection rect
1793     QRect selectionRect = d->mouseSelectionRect;
1794     if (!selectionRect.isNull())
1795         selectionRect = selectionRect.normalized();
1796     // selectionRectInternal without the border
1797     QRect selectionRectInternal = selectionRect;
1798     selectionRectInternal.adjust(1, 1, -1, -1);
1799     // color for blending
1800     QColor selBlendColor = (selectionRect.width() > 8 || selectionRect.height() > 8) ? d->mouseSelectionColor : Qt::red;
1801 
1802     // subdivide region into rects
1803     QRegion rgn = pe->region();
1804     // preprocess rects area to see if it worths or not using subdivision
1805     uint summedArea = 0;
1806     for (const QRect &r : rgn) {
1807         summedArea += r.width() * r.height();
1808     }
1809     // very elementary check: SUMj(Region[j].area) is less than boundingRect.area
1810     const bool useSubdivision = summedArea < (0.6 * contentsRect.width() * contentsRect.height());
1811     if (!useSubdivision) {
1812         rgn = contentsRect;
1813     }
1814 
1815     // iterate over the rects (only one loop if not using subdivision)
1816     for (const QRect &r : rgn) {
1817         if (useSubdivision) {
1818             // set 'contentsRect' to a part of the sub-divided region
1819             contentsRect = r.translated(areaPos).intersected(viewportRect);
1820             if (!contentsRect.isValid())
1821                 continue;
1822         }
1823 #ifdef PAGEVIEW_DEBUG
1824         qCDebug(OkularUiDebug) << contentsRect;
1825 #endif
1826 
1827         // note: this check will take care of all things requiring alpha blending (not only selection)
1828         bool wantCompositing = !selectionRect.isNull() && contentsRect.intersects(selectionRect);
1829         // also alpha-blend when there is a table selection...
1830         wantCompositing |= !d->tableSelectionParts.isEmpty();
1831 
1832         if (wantCompositing && Okular::Settings::enableCompositing()) {
1833             // create pixmap and open a painter over it (contents{left,top} becomes pixmap {0,0})
1834             QPixmap doubleBuffer(contentsRect.size() * devicePixelRatioF());
1835             doubleBuffer.setDevicePixelRatio(devicePixelRatioF());
1836             QPainter pixmapPainter(&doubleBuffer);
1837 
1838             pixmapPainter.translate(-contentsRect.left(), -contentsRect.top());
1839 
1840             // 1) Layer 0: paint items and clear bg on unpainted rects
1841             drawDocumentOnPainter(contentsRect, &pixmapPainter);
1842             // 2a) Layer 1a: paint (blend) transparent selection (rectangle)
1843             if (!selectionRect.isNull() && selectionRect.intersects(contentsRect) && !selectionRectInternal.contains(contentsRect)) {
1844                 QRect blendRect = selectionRectInternal.intersected(contentsRect);
1845                 // skip rectangles covered by the selection's border
1846                 if (blendRect.isValid()) {
1847                     // grab current pixmap into a new one to colorize contents
1848                     QPixmap blendedPixmap(blendRect.width() * devicePixelRatioF(), blendRect.height() * devicePixelRatioF());
1849                     blendedPixmap.setDevicePixelRatio(devicePixelRatioF());
1850                     QPainter p(&blendedPixmap);
1851 
1852                     p.drawPixmap(0,
1853                                  0,
1854                                  doubleBuffer,
1855                                  (blendRect.left() - contentsRect.left()) * devicePixelRatioF(),
1856                                  (blendRect.top() - contentsRect.top()) * devicePixelRatioF(),
1857                                  blendRect.width() * devicePixelRatioF(),
1858                                  blendRect.height() * devicePixelRatioF());
1859 
1860                     QColor blCol = selBlendColor.darker(140);
1861                     blCol.setAlphaF(0.2);
1862                     p.fillRect(blendedPixmap.rect(), blCol);
1863                     p.end();
1864                     // copy the blended pixmap back to its place
1865                     pixmapPainter.drawPixmap(blendRect.left(), blendRect.top(), blendedPixmap);
1866                 }
1867                 // draw border (red if the selection is too small)
1868                 pixmapPainter.setPen(selBlendColor);
1869                 pixmapPainter.drawRect(selectionRect.adjusted(0, 0, -1, -1));
1870             }
1871             // 2b) Layer 1b: paint (blend) transparent selection (table)
1872             for (const TableSelectionPart &tsp : qAsConst(d->tableSelectionParts)) {
1873                 QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight());
1874                 selectionPartRect.translate(tsp.item->uncroppedGeometry().topLeft());
1875                 QRect selectionPartRectInternal = selectionPartRect;
1876                 selectionPartRectInternal.adjust(1, 1, -1, -1);
1877                 if (!selectionPartRect.isNull() && selectionPartRect.intersects(contentsRect) && !selectionPartRectInternal.contains(contentsRect)) {
1878                     QRect blendRect = selectionPartRectInternal.intersected(contentsRect);
1879                     // skip rectangles covered by the selection's border
1880                     if (blendRect.isValid()) {
1881                         // grab current pixmap into a new one to colorize contents
1882                         QPixmap blendedPixmap(blendRect.width() * devicePixelRatioF(), blendRect.height() * devicePixelRatioF());
1883                         blendedPixmap.setDevicePixelRatio(devicePixelRatioF());
1884                         QPainter p(&blendedPixmap);
1885                         p.drawPixmap(0,
1886                                      0,
1887                                      doubleBuffer,
1888                                      (blendRect.left() - contentsRect.left()) * devicePixelRatioF(),
1889                                      (blendRect.top() - contentsRect.top()) * devicePixelRatioF(),
1890                                      blendRect.width() * devicePixelRatioF(),
1891                                      blendRect.height() * devicePixelRatioF());
1892 
1893                         QColor blCol = d->mouseSelectionColor.darker(140);
1894                         blCol.setAlphaF(0.2);
1895                         p.fillRect(blendedPixmap.rect(), blCol);
1896                         p.end();
1897                         // copy the blended pixmap back to its place
1898                         pixmapPainter.drawPixmap(blendRect.left(), blendRect.top(), blendedPixmap);
1899                     }
1900                     // draw border (red if the selection is too small)
1901                     pixmapPainter.setPen(d->mouseSelectionColor);
1902                     pixmapPainter.drawRect(selectionPartRect.adjusted(0, 0, -1, -1));
1903                 }
1904             }
1905             drawTableDividers(&pixmapPainter);
1906             // 3a) Layer 1: give annotator painting control
1907             if (d->annotator && d->annotator->routePaints(contentsRect))
1908                 d->annotator->routePaint(&pixmapPainter, contentsRect);
1909             // 3b) Layer 1: give mouseAnnotation painting control
1910             d->mouseAnnotation->routePaint(&pixmapPainter, contentsRect);
1911 
1912             // 4) Layer 2: overlays
1913             if (Okular::Settings::debugDrawBoundaries()) {
1914                 pixmapPainter.setPen(Qt::blue);
1915                 pixmapPainter.drawRect(contentsRect);
1916             }
1917 
1918             // finish painting and draw contents
1919             pixmapPainter.end();
1920             screenPainter.drawPixmap(contentsRect.left(), contentsRect.top(), doubleBuffer);
1921         } else {
1922             // 1) Layer 0: paint items and clear bg on unpainted rects
1923             drawDocumentOnPainter(contentsRect, &screenPainter);
1924             // 2a) Layer 1a: paint opaque selection (rectangle)
1925             if (!selectionRect.isNull() && selectionRect.intersects(contentsRect) && !selectionRectInternal.contains(contentsRect)) {
1926                 screenPainter.setPen(palette().color(QPalette::Active, QPalette::Highlight).darker(110));
1927                 screenPainter.drawRect(selectionRect);
1928             }
1929             // 2b) Layer 1b: paint opaque selection (table)
1930             for (const TableSelectionPart &tsp : qAsConst(d->tableSelectionParts)) {
1931                 QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight());
1932                 selectionPartRect.translate(tsp.item->uncroppedGeometry().topLeft());
1933                 QRect selectionPartRectInternal = selectionPartRect;
1934                 selectionPartRectInternal.adjust(1, 1, -1, -1);
1935                 if (!selectionPartRect.isNull() && selectionPartRect.intersects(contentsRect) && !selectionPartRectInternal.contains(contentsRect)) {
1936                     screenPainter.setPen(palette().color(QPalette::Active, QPalette::Highlight).darker(110));
1937                     screenPainter.drawRect(selectionPartRect);
1938                 }
1939             }
1940             drawTableDividers(&screenPainter);
1941             // 3a) Layer 1: give annotator painting control
1942             if (d->annotator && d->annotator->routePaints(contentsRect))
1943                 d->annotator->routePaint(&screenPainter, contentsRect);
1944             // 3b) Layer 1: give mouseAnnotation painting control
1945             d->mouseAnnotation->routePaint(&screenPainter, contentsRect);
1946 
1947             // 4) Layer 2: overlays
1948             if (Okular::Settings::debugDrawBoundaries()) {
1949                 screenPainter.setPen(Qt::red);
1950                 screenPainter.drawRect(contentsRect);
1951             }
1952         }
1953     }
1954 }
1955 
drawTableDividers(QPainter * screenPainter)1956 void PageView::drawTableDividers(QPainter *screenPainter)
1957 {
1958     if (!d->tableSelectionParts.isEmpty()) {
1959         screenPainter->setPen(d->mouseSelectionColor.darker());
1960         if (d->tableDividersGuessed) {
1961             QPen p = screenPainter->pen();
1962             p.setStyle(Qt::DashLine);
1963             screenPainter->setPen(p);
1964         }
1965         for (const TableSelectionPart &tsp : qAsConst(d->tableSelectionParts)) {
1966             QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight());
1967             selectionPartRect.translate(tsp.item->uncroppedGeometry().topLeft());
1968             QRect selectionPartRectInternal = selectionPartRect;
1969             selectionPartRectInternal.adjust(1, 1, -1, -1);
1970             for (double col : qAsConst(d->tableSelectionCols)) {
1971                 if (col >= tsp.rectInSelection.left && col <= tsp.rectInSelection.right) {
1972                     col = (col - tsp.rectInSelection.left) / (tsp.rectInSelection.right - tsp.rectInSelection.left);
1973                     const int x = selectionPartRect.left() + col * selectionPartRect.width() + 0.5;
1974                     screenPainter->drawLine(x, selectionPartRectInternal.top(), x, selectionPartRectInternal.top() + selectionPartRectInternal.height());
1975                 }
1976             }
1977             for (double row : qAsConst(d->tableSelectionRows)) {
1978                 if (row >= tsp.rectInSelection.top && row <= tsp.rectInSelection.bottom) {
1979                     row = (row - tsp.rectInSelection.top) / (tsp.rectInSelection.bottom - tsp.rectInSelection.top);
1980                     const int y = selectionPartRect.top() + row * selectionPartRect.height() + 0.5;
1981                     screenPainter->drawLine(selectionPartRectInternal.left(), y, selectionPartRectInternal.left() + selectionPartRectInternal.width(), y);
1982                 }
1983             }
1984         }
1985     }
1986 }
1987 
resizeEvent(QResizeEvent * e)1988 void PageView::resizeEvent(QResizeEvent *e)
1989 {
1990     if (d->items.isEmpty()) {
1991         resizeContentArea(e->size());
1992         return;
1993     }
1994 
1995     if ((d->zoomMode == ZoomFitWidth || d->zoomMode == ZoomFitAuto) && !verticalScrollBar()->isVisible() && qAbs(e->oldSize().height() - e->size().height()) < verticalScrollBar()->width() && d->verticalScrollBarVisible) {
1996         // this saves us from infinite resizing loop because of scrollbars appearing and disappearing
1997         // see bug 160628 for more info
1998         // TODO looks are still a bit ugly because things are left uncentered
1999         // but better a bit ugly than unusable
2000         d->verticalScrollBarVisible = false;
2001         resizeContentArea(e->size());
2002         return;
2003     } else if (d->zoomMode == ZoomFitAuto && !horizontalScrollBar()->isVisible() && qAbs(e->oldSize().width() - e->size().width()) < horizontalScrollBar()->height() && d->horizontalScrollBarVisible) {
2004         // this saves us from infinite resizing loop because of scrollbars appearing and disappearing
2005         // TODO looks are still a bit ugly because things are left uncentered
2006         // but better a bit ugly than unusable
2007         d->horizontalScrollBarVisible = false;
2008         resizeContentArea(e->size());
2009         return;
2010     }
2011 
2012     // start a timer that will refresh the pixmap after 0.2s
2013     d->delayResizeEventTimer->start(200);
2014     d->verticalScrollBarVisible = verticalScrollBar()->isVisible();
2015     d->horizontalScrollBarVisible = horizontalScrollBar()->isVisible();
2016 }
2017 
keyPressEvent(QKeyEvent * e)2018 void PageView::keyPressEvent(QKeyEvent *e)
2019 {
2020     // Ignore ESC key press to send to shell.cpp
2021     if (e->key() != Qt::Key_Escape) {
2022         e->accept();
2023     } else {
2024         e->ignore();
2025     }
2026 
2027     // if performing a selection or dyn zooming, disable keys handling
2028     if ((d->mouseSelecting && e->key() != Qt::Key_Escape) || (QApplication::mouseButtons() & Qt::MiddleButton))
2029         return;
2030 
2031     // move/scroll page by using keys
2032     switch (e->key()) {
2033     case Qt::Key_J:
2034     case Qt::Key_Down:
2035         slotScrollDown(1 /* go down 1 step */);
2036         break;
2037 
2038     case Qt::Key_PageDown:
2039         slotScrollDown();
2040         break;
2041 
2042     case Qt::Key_K:
2043     case Qt::Key_Up:
2044         slotScrollUp(1 /* go up 1 step */);
2045         break;
2046 
2047     case Qt::Key_PageUp:
2048     case Qt::Key_Backspace:
2049         slotScrollUp();
2050         break;
2051 
2052     case Qt::Key_Left:
2053     case Qt::Key_H:
2054         if (horizontalScrollBar()->maximum() == 0) {
2055             // if we cannot scroll we go to the previous page vertically
2056             int next_page = d->document->currentPage() - viewColumns();
2057             d->document->setViewportPage(next_page);
2058         } else {
2059             d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(-horizontalScrollBar()->singleStep(), 0), d->currentShortScrollDuration);
2060         }
2061         break;
2062     case Qt::Key_Right:
2063     case Qt::Key_L:
2064         if (horizontalScrollBar()->maximum() == 0) {
2065             // if we cannot scroll we advance the page vertically
2066             int next_page = d->document->currentPage() + viewColumns();
2067             d->document->setViewportPage(next_page);
2068         } else {
2069             d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(horizontalScrollBar()->singleStep(), 0), d->currentShortScrollDuration);
2070         }
2071         break;
2072     case Qt::Key_Escape:
2073         emit escPressed();
2074         selectionClear(d->tableDividersGuessed ? ClearOnlyDividers : ClearAllSelection);
2075         d->mousePressPos = QPoint();
2076         if (d->aPrevAction) {
2077             d->aPrevAction->trigger();
2078             d->aPrevAction = nullptr;
2079         }
2080         d->mouseAnnotation->routeKeyPressEvent(e);
2081         break;
2082     case Qt::Key_Delete:
2083         d->mouseAnnotation->routeKeyPressEvent(e);
2084         break;
2085     case Qt::Key_Shift:
2086     case Qt::Key_Control:
2087         if (d->autoScrollTimer) {
2088             if (d->autoScrollTimer->isActive())
2089                 d->autoScrollTimer->stop();
2090             else
2091                 slotAutoScroll();
2092             return;
2093         }
2094         // fallthrough
2095     default:
2096         e->ignore();
2097         return;
2098     }
2099     // if a known key has been pressed, stop scrolling the page
2100     if (d->autoScrollTimer) {
2101         d->scrollIncrement = 0;
2102         d->autoScrollTimer->stop();
2103     }
2104 }
2105 
keyReleaseEvent(QKeyEvent * e)2106 void PageView::keyReleaseEvent(QKeyEvent *e)
2107 {
2108     e->accept();
2109 
2110     if (d->annotator && d->annotator->active()) {
2111         if (d->annotator->routeKeyEvent(e))
2112             return;
2113     }
2114 
2115     if (e->key() == Qt::Key_Escape && d->autoScrollTimer) {
2116         d->scrollIncrement = 0;
2117         d->autoScrollTimer->stop();
2118     }
2119 }
2120 
inputMethodEvent(QInputMethodEvent * e)2121 void PageView::inputMethodEvent(QInputMethodEvent *e)
2122 {
2123     Q_UNUSED(e)
2124 }
2125 
tabletEvent(QTabletEvent * e)2126 void PageView::tabletEvent(QTabletEvent *e)
2127 {
2128     // Ignore tablet events that we don't care about
2129     if (!(e->type() == QEvent::TabletPress || e->type() == QEvent::TabletRelease || e->type() == QEvent::TabletMove)) {
2130         e->ignore();
2131         return;
2132     }
2133 
2134     // Determine pen state
2135     bool penReleased = false;
2136     if (e->type() == QEvent::TabletPress) {
2137         d->penDown = true;
2138     }
2139     if (e->type() == QEvent::TabletRelease) {
2140         d->penDown = false;
2141         penReleased = true;
2142     }
2143 
2144     // If we're editing an annotation and the tablet pen is either down or just released
2145     // then dispatch event to annotator
2146     if (d->annotator && d->annotator->active() && (d->penDown || penReleased)) {
2147         // accept the event, otherwise it comes back as a mouse event
2148         e->accept();
2149 
2150         const QPoint eventPos = contentAreaPoint(e->pos());
2151         PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2152         const QPoint localOriginInGlobal = mapToGlobal(QPoint(0, 0));
2153 
2154         // routeTabletEvent will accept or ignore event as appropriate
2155         d->annotator->routeTabletEvent(e, pageItem, localOriginInGlobal);
2156     } else {
2157         e->ignore();
2158     }
2159 }
2160 
mouseMoveEvent(QMouseEvent * e)2161 void PageView::mouseMoveEvent(QMouseEvent *e)
2162 {
2163     // For some reason in Qt 5.11.2 (no idea when this started) all wheel
2164     // events are followed by mouse move events (without changing position),
2165     // so we only actually reset the controlWheelAccumulatedDelta if there is a mouse movement
2166     if (e->globalPos() != d->previousMouseMovePos) {
2167         d->controlWheelAccumulatedDelta = 0;
2168     }
2169     d->previousMouseMovePos = e->globalPos();
2170 
2171     // don't perform any mouse action when no document is shown
2172     if (d->items.isEmpty())
2173         return;
2174 
2175     // if holding mouse mid button, perform zoom
2176     if (e->buttons() & Qt::MidButton) {
2177         int deltaY = d->mouseMidLastY - e->globalPos().y();
2178         d->mouseMidLastY = e->globalPos().y();
2179 
2180         const float upperZoomLimit = d->document->supportsTiles() ? 99.99 : 3.99;
2181 
2182         // Wrap mouse cursor
2183         Qt::Edges wrapEdges;
2184         wrapEdges.setFlag(Qt::TopEdge, d->zoomFactor < upperZoomLimit);
2185         wrapEdges.setFlag(Qt::BottomEdge, d->zoomFactor > 0.101);
2186 
2187         deltaY += CursorWrapHelper::wrapCursor(e->globalPos(), wrapEdges).y();
2188 
2189         // update zoom level, perform zoom and redraw
2190         if (deltaY) {
2191             d->zoomFactor *= (1.0 + ((double)deltaY / 500.0));
2192             d->blockPixmapsRequest = true;
2193             updateZoom(ZoomRefreshCurrent);
2194             d->blockPixmapsRequest = false;
2195             viewport()->update();
2196         }
2197         return;
2198     }
2199 
2200     const QPoint eventPos = contentAreaPoint(e->pos());
2201 
2202     // if we're editing an annotation, dispatch event to it
2203     if (d->annotator && d->annotator->active()) {
2204         PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2205         updateCursor(eventPos);
2206         d->annotator->routeMouseEvent(e, pageItem);
2207         return;
2208     }
2209 
2210     bool leftButton = (e->buttons() == Qt::LeftButton);
2211     bool rightButton = (e->buttons() == Qt::RightButton);
2212 
2213     switch (d->mouseMode) {
2214     case Okular::Settings::EnumMouseMode::Browse: {
2215         PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2216         if (leftButton) {
2217             d->leftClickTimer.stop();
2218             if (pageItem && d->mouseAnnotation->isActive()) {
2219                 // if left button pressed and annotation is focused, forward move event
2220                 d->mouseAnnotation->routeMouseMoveEvent(pageItem, eventPos, leftButton);
2221             }
2222             // drag page
2223             else {
2224                 if (d->scroller->state() == QScroller::Inactive || d->scroller->state() == QScroller::Scrolling) {
2225                     d->mouseGrabOffset = QPoint(0, 0);
2226                     d->scroller->handleInput(QScroller::InputPress, e->pos(), e->timestamp() - 1);
2227                 }
2228 
2229                 setCursor(Qt::ClosedHandCursor);
2230 
2231                 // Wrap mouse cursor
2232                 Qt::Edges wrapEdges;
2233                 wrapEdges.setFlag(Qt::TopEdge, verticalScrollBar()->value() < verticalScrollBar()->maximum());
2234                 wrapEdges.setFlag(Qt::BottomEdge, verticalScrollBar()->value() > verticalScrollBar()->minimum());
2235                 wrapEdges.setFlag(Qt::LeftEdge, horizontalScrollBar()->value() < horizontalScrollBar()->maximum());
2236                 wrapEdges.setFlag(Qt::RightEdge, horizontalScrollBar()->value() > horizontalScrollBar()->minimum());
2237 
2238                 d->mouseGrabOffset -= CursorWrapHelper::wrapCursor(e->pos(), wrapEdges);
2239 
2240                 d->scroller->handleInput(QScroller::InputMove, e->pos() + d->mouseGrabOffset, e->timestamp());
2241             }
2242         } else if (rightButton && !d->mousePressPos.isNull() && d->aMouseSelect) {
2243             // if mouse moves 5 px away from the press point, switch to 'selection'
2244             int deltaX = d->mousePressPos.x() - e->globalPos().x(), deltaY = d->mousePressPos.y() - e->globalPos().y();
2245             if (deltaX > 5 || deltaX < -5 || deltaY > 5 || deltaY < -5) {
2246                 d->aPrevAction = d->aMouseNormal;
2247                 d->aMouseSelect->trigger();
2248                 QPoint newPos = eventPos + QPoint(deltaX, deltaY);
2249                 selectionStart(newPos, palette().color(QPalette::Active, QPalette::Highlight).lighter(120), false);
2250                 updateSelection(eventPos);
2251                 break;
2252             }
2253         } else {
2254             /* Forward move events which are still not yet consumed by "mouse grab" or aMouseSelect */
2255             d->mouseAnnotation->routeMouseMoveEvent(pageItem, eventPos, leftButton);
2256             updateCursor();
2257         }
2258     } break;
2259 
2260     case Okular::Settings::EnumMouseMode::Zoom:
2261     case Okular::Settings::EnumMouseMode::RectSelect:
2262     case Okular::Settings::EnumMouseMode::TableSelect:
2263     case Okular::Settings::EnumMouseMode::TrimSelect:
2264         // set second corner of selection
2265         if (d->mouseSelecting) {
2266             updateSelection(eventPos);
2267             d->mouseOverLinkObject = nullptr;
2268         }
2269         updateCursor();
2270         break;
2271 
2272     case Okular::Settings::EnumMouseMode::Magnifier:
2273         if (e->buttons()) // if any button is pressed at all
2274         {
2275             moveMagnifier(e->pos());
2276             updateMagnifier(eventPos);
2277         }
2278         break;
2279 
2280     case Okular::Settings::EnumMouseMode::TextSelect:
2281         // if mouse moves 5 px away from the press point and the document supports text extraction, do 'textselection'
2282         if (!d->mouseTextSelecting && !d->mousePressPos.isNull() && d->document->supportsSearching() && ((eventPos - d->mouseSelectPos).manhattanLength() > 5)) {
2283             d->mouseTextSelecting = true;
2284         }
2285         updateSelection(eventPos);
2286         updateCursor();
2287         break;
2288     }
2289 }
2290 
mousePressEvent(QMouseEvent * e)2291 void PageView::mousePressEvent(QMouseEvent *e)
2292 {
2293     d->controlWheelAccumulatedDelta = 0;
2294 
2295     // don't perform any mouse action when no document is shown
2296     if (d->items.isEmpty())
2297         return;
2298 
2299     // if performing a selection or dyn zooming, disable mouse press
2300     if (d->mouseSelecting || (e->button() != Qt::MiddleButton && (e->buttons() & Qt::MiddleButton)))
2301         return;
2302 
2303     // if the page is scrolling, stop it
2304     if (d->autoScrollTimer) {
2305         d->scrollIncrement = 0;
2306         d->autoScrollTimer->stop();
2307     }
2308 
2309     // if pressing mid mouse button while not doing other things, begin 'continuous zoom' mode
2310     if (e->button() == Qt::MiddleButton) {
2311         d->mouseMidLastY = e->globalPos().y();
2312         setCursor(Qt::SizeVerCursor);
2313         CursorWrapHelper::startDrag();
2314         return;
2315     }
2316 
2317     const QPoint eventPos = contentAreaPoint(e->pos());
2318 
2319     // if we're editing an annotation, dispatch event to it
2320     if (d->annotator && d->annotator->active()) {
2321         d->scroller->stop();
2322         PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2323         d->annotator->routeMouseEvent(e, pageItem);
2324         return;
2325     }
2326 
2327     // trigger history navigation for additional mouse buttons
2328     if (e->button() == Qt::XButton1) {
2329         emit mouseBackButtonClick();
2330         return;
2331     }
2332     if (e->button() == Qt::XButton2) {
2333         emit mouseForwardButtonClick();
2334         return;
2335     }
2336 
2337     // update press / 'start drag' mouse position
2338     d->mousePressPos = e->globalPos();
2339     CursorWrapHelper::startDrag();
2340 
2341     // handle mode dependent mouse press actions
2342     bool leftButton = e->button() == Qt::LeftButton, rightButton = e->button() == Qt::RightButton;
2343 
2344     //   Not sure we should erase the selection when clicking with left.
2345     if (d->mouseMode != Okular::Settings::EnumMouseMode::TextSelect)
2346         textSelectionClear();
2347 
2348     switch (d->mouseMode) {
2349     case Okular::Settings::EnumMouseMode::Browse: // drag start / click / link following
2350     {
2351         PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2352         if (leftButton) {
2353             if (pageItem) {
2354                 d->mouseAnnotation->routeMousePressEvent(pageItem, eventPos);
2355             }
2356 
2357             if (!d->mouseOnRect) {
2358                 d->mouseGrabOffset = QPoint(0, 0);
2359                 d->scroller->handleInput(QScroller::InputPress, e->pos(), e->timestamp());
2360                 d->leftClickTimer.start(QApplication::doubleClickInterval() + 10);
2361             }
2362         }
2363     } break;
2364 
2365     case Okular::Settings::EnumMouseMode::Zoom: // set first corner of the zoom rect
2366         if (leftButton)
2367             selectionStart(eventPos, palette().color(QPalette::Active, QPalette::Highlight), false);
2368         else if (rightButton)
2369             updateZoom(ZoomOut);
2370         break;
2371 
2372     case Okular::Settings::EnumMouseMode::Magnifier:
2373         moveMagnifier(e->pos());
2374         d->magnifierView->show();
2375         updateMagnifier(eventPos);
2376         break;
2377 
2378     case Okular::Settings::EnumMouseMode::RectSelect: // set first corner of the selection rect
2379     case Okular::Settings::EnumMouseMode::TrimSelect:
2380         if (leftButton) {
2381             selectionStart(eventPos, palette().color(QPalette::Active, QPalette::Highlight).lighter(120), false);
2382         }
2383         break;
2384     case Okular::Settings::EnumMouseMode::TableSelect:
2385         if (leftButton) {
2386             if (d->tableSelectionParts.isEmpty()) {
2387                 selectionStart(eventPos, palette().color(QPalette::Active, QPalette::Highlight).lighter(120), false);
2388             } else {
2389                 QRect updatedRect;
2390                 for (const TableSelectionPart &tsp : qAsConst(d->tableSelectionParts)) {
2391                     QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight());
2392                     selectionPartRect.translate(tsp.item->uncroppedGeometry().topLeft());
2393 
2394                     // This will update the whole table rather than just the added/removed divider
2395                     // (which can span more than one part).
2396                     updatedRect = updatedRect.united(selectionPartRect);
2397 
2398                     if (!selectionPartRect.contains(eventPos))
2399                         continue;
2400 
2401                     // 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).
2402                     d->tableDividersGuessed = false;
2403 
2404                     // There's probably a neat trick to finding which edge it's closest to,
2405                     // but this way has the advantage of simplicity.
2406                     const int fromLeft = abs(selectionPartRect.left() - eventPos.x());
2407                     const int fromRight = abs(selectionPartRect.left() + selectionPartRect.width() - eventPos.x());
2408                     const int fromTop = abs(selectionPartRect.top() - eventPos.y());
2409                     const int fromBottom = abs(selectionPartRect.top() + selectionPartRect.height() - eventPos.y());
2410                     const int colScore = fromTop < fromBottom ? fromTop : fromBottom;
2411                     const int rowScore = fromLeft < fromRight ? fromLeft : fromRight;
2412 
2413                     if (colScore < rowScore) {
2414                         bool deleted = false;
2415                         for (int i = 0; i < d->tableSelectionCols.length(); i++) {
2416                             const double col = (d->tableSelectionCols[i] - tsp.rectInSelection.left) / (tsp.rectInSelection.right - tsp.rectInSelection.left);
2417                             const int colX = selectionPartRect.left() + col * selectionPartRect.width() + 0.5;
2418                             if (abs(colX - eventPos.x()) <= 3) {
2419                                 d->tableSelectionCols.removeAt(i);
2420                                 deleted = true;
2421 
2422                                 break;
2423                             }
2424                         }
2425                         if (!deleted) {
2426                             double col = eventPos.x() - selectionPartRect.left();
2427                             col /= selectionPartRect.width(); // at this point, it's normalised within the part
2428                             col *= (tsp.rectInSelection.right - tsp.rectInSelection.left);
2429                             col += tsp.rectInSelection.left; // at this point, it's normalised within the whole table
2430 
2431                             d->tableSelectionCols.append(col);
2432                             std::sort(d->tableSelectionCols.begin(), d->tableSelectionCols.end());
2433                         }
2434                     } else {
2435                         bool deleted = false;
2436                         for (int i = 0; i < d->tableSelectionRows.length(); i++) {
2437                             const double row = (d->tableSelectionRows[i] - tsp.rectInSelection.top) / (tsp.rectInSelection.bottom - tsp.rectInSelection.top);
2438                             const int rowY = selectionPartRect.top() + row * selectionPartRect.height() + 0.5;
2439                             if (abs(rowY - eventPos.y()) <= 3) {
2440                                 d->tableSelectionRows.removeAt(i);
2441                                 deleted = true;
2442 
2443                                 break;
2444                             }
2445                         }
2446                         if (!deleted) {
2447                             double row = eventPos.y() - selectionPartRect.top();
2448                             row /= selectionPartRect.height(); // at this point, it's normalised within the part
2449                             row *= (tsp.rectInSelection.bottom - tsp.rectInSelection.top);
2450                             row += tsp.rectInSelection.top; // at this point, it's normalised within the whole table
2451 
2452                             d->tableSelectionRows.append(row);
2453                             std::sort(d->tableSelectionRows.begin(), d->tableSelectionRows.end());
2454                         }
2455                     }
2456                 }
2457                 updatedRect.translate(-contentAreaPosition());
2458                 viewport()->update(updatedRect);
2459             }
2460         } else if (rightButton && !d->tableSelectionParts.isEmpty()) {
2461             QMenu menu(this);
2462             QAction *copyToClipboard = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy Table Contents to Clipboard"));
2463             const bool copyAllowed = d->document->isAllowed(Okular::AllowCopy);
2464 
2465             if (!copyAllowed) {
2466                 copyToClipboard->setEnabled(false);
2467                 copyToClipboard->setText(i18n("Copy forbidden by DRM"));
2468             }
2469 
2470             QAction *choice = menu.exec(e->globalPos());
2471             if (choice == copyToClipboard)
2472                 copyTextSelection();
2473         }
2474         break;
2475     case Okular::Settings::EnumMouseMode::TextSelect:
2476         d->mouseSelectPos = eventPos;
2477         if (!rightButton) {
2478             textSelectionClear();
2479         }
2480         break;
2481     }
2482 }
2483 
mouseReleaseEvent(QMouseEvent * e)2484 void PageView::mouseReleaseEvent(QMouseEvent *e)
2485 {
2486     d->controlWheelAccumulatedDelta = 0;
2487 
2488     // stop the drag scrolling
2489     d->dragScrollTimer.stop();
2490 
2491     d->leftClickTimer.stop();
2492 
2493     const bool leftButton = e->button() == Qt::LeftButton;
2494     const bool rightButton = e->button() == Qt::RightButton;
2495 
2496     if (d->mouseAnnotation->isActive() && leftButton) {
2497         // Just finished to move the annotation
2498         d->mouseAnnotation->routeMouseReleaseEvent();
2499     }
2500 
2501     // don't perform any mouse action when no document is shown..
2502     if (d->items.isEmpty()) {
2503         // ..except for right Clicks (emitted even it viewport is empty)
2504         if (e->button() == Qt::RightButton)
2505             emit rightClick(nullptr, e->globalPos());
2506         return;
2507     }
2508 
2509     const QPoint eventPos = contentAreaPoint(e->pos());
2510 
2511     // handle mode independent mid bottom zoom
2512     if (e->button() == Qt::MiddleButton) {
2513         // request pixmaps since it was disabled during drag
2514         slotRequestVisiblePixmaps();
2515         // the cursor may now be over a link.. update it
2516         updateCursor(eventPos);
2517         return;
2518     }
2519 
2520     // if we're editing an annotation, dispatch event to it
2521     if (d->annotator && d->annotator->active()) {
2522         PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2523         d->annotator->routeMouseEvent(e, pageItem);
2524         return;
2525     }
2526 
2527     switch (d->mouseMode) {
2528     case Okular::Settings::EnumMouseMode::Browse: {
2529         d->scroller->handleInput(QScroller::InputRelease, e->pos() + d->mouseGrabOffset, e->timestamp());
2530 
2531         // return the cursor to its normal state after dragging
2532         if (cursor().shape() == Qt::ClosedHandCursor)
2533             updateCursor(eventPos);
2534 
2535         PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2536         const QPoint pressPos = contentAreaPoint(mapFromGlobal(d->mousePressPos));
2537         const PageViewItem *pageItemPressPos = pickItemOnPoint(pressPos.x(), pressPos.y());
2538 
2539         // if the mouse has not moved since the press, that's a -click-
2540         if (leftButton && pageItem && pageItem == pageItemPressPos && ((d->mousePressPos - e->globalPos()).manhattanLength() < QApplication::startDragDistance())) {
2541             if (!mouseReleaseOverLink(d->mouseOverLinkObject) && (e->modifiers() == Qt::ShiftModifier)) {
2542                 const double nX = pageItem->absToPageX(eventPos.x());
2543                 const double nY = pageItem->absToPageY(eventPos.y());
2544                 const Okular::ObjectRect *rect;
2545                 // TODO: find a better way to activate the source reference "links"
2546                 // for the moment they are activated with Shift + left click
2547                 // Search the nearest source reference.
2548                 rect = pageItem->page()->objectRect(Okular::ObjectRect::SourceRef, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight());
2549                 if (!rect) {
2550                     static const double s_minDistance = 0.025; // FIXME?: empirical value?
2551                     double distance = 0.0;
2552                     rect = pageItem->page()->nearestObjectRect(Okular::ObjectRect::SourceRef, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight(), &distance);
2553                     // distance is distanceSqr, adapt it to a normalized value
2554                     distance = distance / (pow(pageItem->uncroppedWidth(), 2) + pow(pageItem->uncroppedHeight(), 2));
2555                     if (rect && (distance > s_minDistance))
2556                         rect = nullptr;
2557                 }
2558                 if (rect) {
2559                     const Okular::SourceReference *ref = static_cast<const Okular::SourceReference *>(rect->object());
2560                     d->document->processSourceReference(ref);
2561                 } else {
2562                     const Okular::SourceReference *ref = d->document->dynamicSourceReference(pageItem->pageNumber(), nX * pageItem->page()->width(), nY * pageItem->page()->height());
2563                     if (ref) {
2564                         d->document->processSourceReference(ref);
2565                         delete ref;
2566                     }
2567                 }
2568             }
2569         } else if (rightButton && !d->mouseAnnotation->isModified()) {
2570             if (pageItem && pageItem == pageItemPressPos && ((d->mousePressPos - e->globalPos()).manhattanLength() < QApplication::startDragDistance())) {
2571                 QMenu *menu = createProcessLinkMenu(pageItem, eventPos);
2572 
2573                 const QRect &itemRect = pageItem->uncroppedGeometry();
2574                 const double nX = pageItem->absToPageX(eventPos.x());
2575                 const double nY = pageItem->absToPageY(eventPos.y());
2576 
2577                 const QLinkedList<const Okular::ObjectRect *> annotRects = pageItem->page()->objectRects(Okular::ObjectRect::OAnnotation, nX, nY, itemRect.width(), itemRect.height());
2578 
2579                 AnnotationPopup annotPopup(d->document, AnnotationPopup::MultiAnnotationMode, this);
2580                 // Do not move annotPopup inside the if, it needs to live until menu->exec()
2581                 if (!annotRects.isEmpty()) {
2582                     for (const Okular::ObjectRect *annotRect : annotRects) {
2583                         Okular::Annotation *ann = ((Okular::AnnotationObjectRect *)annotRect)->annotation();
2584                         if (ann && (ann->subType() != Okular::Annotation::AWidget)) {
2585                             annotPopup.addAnnotation(ann, pageItem->pageNumber());
2586                         }
2587                     }
2588 
2589                     connect(&annotPopup, &AnnotationPopup::openAnnotationWindow, this, &PageView::openAnnotationWindow);
2590 
2591                     if (!menu) {
2592                         menu = new QMenu(this);
2593                     }
2594                     annotPopup.addActionsToMenu(menu);
2595                 }
2596 
2597                 if (menu) {
2598                     menu->exec(e->globalPos());
2599                     menu->deleteLater();
2600                 } else {
2601                     // a link can move us to another page or even to another document, there's no point in trying to
2602                     //  process the click on the image once we have processes the click on the link
2603                     const Okular::ObjectRect *rect = pageItem->page()->objectRect(Okular::ObjectRect::Image, nX, nY, itemRect.width(), itemRect.height());
2604                     if (rect) {
2605                         // handle right click over a image
2606                     } else {
2607                         // right click (if not within 5 px of the press point, the mode
2608                         // had been already changed to 'Selection' instead of 'Normal')
2609                         emit rightClick(pageItem->page(), e->globalPos());
2610                     }
2611                 }
2612             } else {
2613                 // right click (if not within 5 px of the press point, the mode
2614                 // had been already changed to 'Selection' instead of 'Normal')
2615                 emit rightClick(pageItem ? pageItem->page() : nullptr, e->globalPos());
2616             }
2617         }
2618     } break;
2619 
2620     case Okular::Settings::EnumMouseMode::Zoom:
2621         // if a selection rect has been defined, zoom into it
2622         if (leftButton && d->mouseSelecting) {
2623             QRect selRect = d->mouseSelectionRect.normalized();
2624             if (selRect.width() <= 8 && selRect.height() <= 8) {
2625                 selectionClear();
2626                 break;
2627             }
2628 
2629             // find out new zoom ratio and normalized view center (relative to the contentsRect)
2630             double zoom = qMin((double)viewport()->width() / (double)selRect.width(), (double)viewport()->height() / (double)selRect.height());
2631             double nX = (double)(selRect.left() + selRect.right()) / (2.0 * (double)contentAreaWidth());
2632             double nY = (double)(selRect.top() + selRect.bottom()) / (2.0 * (double)contentAreaHeight());
2633 
2634             const float upperZoomLimit = d->document->supportsTiles() ? 100.0 : 4.0;
2635             if (d->zoomFactor <= upperZoomLimit || zoom <= 1.0) {
2636                 d->zoomFactor *= zoom;
2637                 viewport()->setUpdatesEnabled(false);
2638                 updateZoom(ZoomRefreshCurrent);
2639                 viewport()->setUpdatesEnabled(true);
2640             }
2641 
2642             // recenter view and update the viewport
2643             center((int)(nX * contentAreaWidth()), (int)(nY * contentAreaHeight()));
2644             viewport()->update();
2645 
2646             // hide message box and delete overlay window
2647             selectionClear();
2648         }
2649         break;
2650 
2651     case Okular::Settings::EnumMouseMode::Magnifier:
2652         d->magnifierView->hide();
2653         break;
2654 
2655     case Okular::Settings::EnumMouseMode::TrimSelect: {
2656         // if it is a left release checks if is over a previous link press
2657         if (leftButton && mouseReleaseOverLink(d->mouseOverLinkObject)) {
2658             selectionClear();
2659             break;
2660         }
2661 
2662         // if mouse is released and selection is null this is a rightClick
2663         if (rightButton && !d->mouseSelecting) {
2664             break;
2665         }
2666         PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2667         // ensure end point rests within a page, or ignore
2668         if (!pageItem) {
2669             break;
2670         }
2671         QRect selectionRect = d->mouseSelectionRect.normalized();
2672 
2673         double nLeft = pageItem->absToPageX(selectionRect.left());
2674         double nRight = pageItem->absToPageX(selectionRect.right());
2675         double nTop = pageItem->absToPageY(selectionRect.top());
2676         double nBottom = pageItem->absToPageY(selectionRect.bottom());
2677         if (nLeft < 0)
2678             nLeft = 0;
2679         if (nTop < 0)
2680             nTop = 0;
2681         if (nRight > 1)
2682             nRight = 1;
2683         if (nBottom > 1)
2684             nBottom = 1;
2685         d->trimBoundingBox = Okular::NormalizedRect(nLeft, nTop, nRight, nBottom);
2686 
2687         // Trim Selection successfully done, hide prompt
2688         d->messageWindow->hide();
2689 
2690         // clear widget selection and invalidate rect
2691         selectionClear();
2692 
2693         // When Trim selection bbox interaction is over, we should switch to another mousemode.
2694         if (d->aPrevAction) {
2695             d->aPrevAction->trigger();
2696             d->aPrevAction = nullptr;
2697         } else {
2698             d->aMouseNormal->trigger();
2699         }
2700 
2701         // with d->trimBoundingBox defined, redraw for trim to take visual effect
2702         if (d->document->pages() > 0) {
2703             slotRelayoutPages();
2704             slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already!
2705         }
2706 
2707         break;
2708     }
2709     case Okular::Settings::EnumMouseMode::RectSelect: {
2710         // if it is a left release checks if is over a previous link press
2711         if (leftButton && mouseReleaseOverLink(d->mouseOverLinkObject)) {
2712             selectionClear();
2713             break;
2714         }
2715 
2716         // if mouse is released and selection is null this is a rightClick
2717         if (rightButton && !d->mouseSelecting) {
2718             PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2719             emit rightClick(pageItem ? pageItem->page() : nullptr, e->globalPos());
2720             break;
2721         }
2722 
2723         // if a selection is defined, display a popup
2724         if ((!leftButton && !d->aPrevAction) || (!rightButton && d->aPrevAction) || !d->mouseSelecting)
2725             break;
2726 
2727         QRect selectionRect = d->mouseSelectionRect.normalized();
2728         if (selectionRect.width() <= 8 && selectionRect.height() <= 8) {
2729             selectionClear();
2730             if (d->aPrevAction) {
2731                 d->aPrevAction->trigger();
2732                 d->aPrevAction = nullptr;
2733             }
2734             break;
2735         }
2736 
2737         // if we support text generation
2738         QString selectedText;
2739         if (d->document->supportsSearching()) {
2740             // grab text in selection by extracting it from all intersected pages
2741             const Okular::Page *okularPage = nullptr;
2742             for (const PageViewItem *item : qAsConst(d->items)) {
2743                 if (!item->isVisible())
2744                     continue;
2745 
2746                 const QRect &itemRect = item->croppedGeometry();
2747                 if (selectionRect.intersects(itemRect)) {
2748                     // request the textpage if there isn't one
2749                     okularPage = item->page();
2750                     qCDebug(OkularUiDebug) << "checking if page" << item->pageNumber() << "has text:" << okularPage->hasTextPage();
2751                     if (!okularPage->hasTextPage())
2752                         d->document->requestTextPage(okularPage->number());
2753                     // grab text in the rect that intersects itemRect
2754                     QRect relativeRect = selectionRect.intersected(itemRect);
2755                     relativeRect.translate(-item->uncroppedGeometry().topLeft());
2756                     Okular::RegularAreaRect rects;
2757                     rects.append(Okular::NormalizedRect(relativeRect, item->uncroppedWidth(), item->uncroppedHeight()));
2758                     selectedText += okularPage->text(&rects);
2759                 }
2760             }
2761         }
2762 
2763         // popup that ask to copy:text and copy/save:image
2764         QMenu menu(this);
2765         menu.setObjectName(QStringLiteral("PopupMenu"));
2766         QAction *textToClipboard = nullptr;
2767 #ifdef HAVE_SPEECH
2768         QAction *speakText = nullptr;
2769 #endif
2770         QAction *imageToClipboard = nullptr;
2771         QAction *imageToFile = nullptr;
2772         if (d->document->supportsSearching() && !selectedText.isEmpty()) {
2773             menu.addAction(new OKMenuTitle(&menu, i18np("Text (1 character)", "Text (%1 characters)", selectedText.length())));
2774             textToClipboard = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy to Clipboard"));
2775             textToClipboard->setObjectName(QStringLiteral("CopyTextToClipboard"));
2776             bool copyAllowed = d->document->isAllowed(Okular::AllowCopy);
2777             if (!copyAllowed) {
2778                 textToClipboard->setEnabled(false);
2779                 textToClipboard->setText(i18n("Copy forbidden by DRM"));
2780             }
2781 #ifdef HAVE_SPEECH
2782             if (Okular::Settings::useTTS())
2783                 speakText = menu.addAction(QIcon::fromTheme(QStringLiteral("text-speak")), i18n("Speak Text"));
2784 #endif
2785             if (copyAllowed) {
2786                 addSearchWithinDocumentAction(&menu, selectedText);
2787                 addWebShortcutsMenu(&menu, selectedText);
2788             }
2789         }
2790         menu.addAction(new OKMenuTitle(&menu, i18n("Image (%1 by %2 pixels)", selectionRect.width(), selectionRect.height())));
2791         imageToClipboard = menu.addAction(QIcon::fromTheme(QStringLiteral("image-x-generic")), i18n("Copy to Clipboard"));
2792         imageToFile = menu.addAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save to File..."));
2793         QAction *choice = menu.exec(e->globalPos());
2794         // check if the user really selected an action
2795         if (choice) {
2796             // IMAGE operation chosen
2797             if (choice == imageToClipboard || choice == imageToFile) {
2798                 // renders page into a pixmap
2799                 QPixmap copyPix(selectionRect.width(), selectionRect.height());
2800                 QPainter copyPainter(&copyPix);
2801                 copyPainter.translate(-selectionRect.left(), -selectionRect.top());
2802                 drawDocumentOnPainter(selectionRect, &copyPainter);
2803                 copyPainter.end();
2804 
2805                 if (choice == imageToClipboard) {
2806                     // [2] copy pixmap to clipboard
2807                     QClipboard *cb = QApplication::clipboard();
2808                     cb->setPixmap(copyPix, QClipboard::Clipboard);
2809                     if (cb->supportsSelection())
2810                         cb->setPixmap(copyPix, QClipboard::Selection);
2811                     d->messageWindow->display(i18n("Image [%1x%2] copied to clipboard.", copyPix.width(), copyPix.height()));
2812                 } else if (choice == imageToFile) {
2813                     // [3] save pixmap to file
2814                     QString fileName = QFileDialog::getSaveFileName(this, i18n("Save file"), QString(), i18n("Images (*.png *.jpeg)"));
2815                     if (fileName.isEmpty())
2816                         d->messageWindow->display(i18n("File not saved."), QString(), PageViewMessage::Warning);
2817                     else {
2818                         QMimeDatabase db;
2819                         QMimeType mime = db.mimeTypeForUrl(QUrl::fromLocalFile(fileName));
2820                         QString type;
2821                         if (!mime.isDefault())
2822                             type = QStringLiteral("PNG");
2823                         else
2824                             type = mime.name().section(QLatin1Char('/'), -1).toUpper();
2825                         copyPix.save(fileName, qPrintable(type));
2826                         d->messageWindow->display(i18n("Image [%1x%2] saved to %3 file.", copyPix.width(), copyPix.height(), type));
2827                     }
2828                 }
2829             }
2830             // TEXT operation chosen
2831             else {
2832                 if (choice == textToClipboard) {
2833                     // [1] copy text to clipboard
2834                     QClipboard *cb = QApplication::clipboard();
2835                     cb->setText(selectedText, QClipboard::Clipboard);
2836                     if (cb->supportsSelection())
2837                         cb->setText(selectedText, QClipboard::Selection);
2838                 }
2839 #ifdef HAVE_SPEECH
2840                 else if (choice == speakText) {
2841                     // [2] speech selection using TTS
2842                     d->tts()->say(selectedText);
2843                 }
2844 #endif
2845             }
2846         }
2847         // clear widget selection and invalidate rect
2848         selectionClear();
2849 
2850         // restore previous action if came from it using right button
2851         if (d->aPrevAction) {
2852             d->aPrevAction->trigger();
2853             d->aPrevAction = nullptr;
2854         }
2855     } break;
2856 
2857     case Okular::Settings::EnumMouseMode::TableSelect: {
2858         // if it is a left release checks if is over a previous link press
2859         if (leftButton && mouseReleaseOverLink(d->mouseOverLinkObject)) {
2860             selectionClear();
2861             break;
2862         }
2863 
2864         // if mouse is released and selection is null this is a rightClick
2865         if (rightButton && !d->mouseSelecting) {
2866             PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
2867             emit rightClick(pageItem ? pageItem->page() : nullptr, e->globalPos());
2868             break;
2869         }
2870 
2871         QRect selectionRect = d->mouseSelectionRect.normalized();
2872         if (selectionRect.width() <= 8 && selectionRect.height() <= 8 && d->tableSelectionParts.isEmpty()) {
2873             selectionClear();
2874             if (d->aPrevAction) {
2875                 d->aPrevAction->trigger();
2876                 d->aPrevAction = nullptr;
2877             }
2878             break;
2879         }
2880 
2881         if (d->mouseSelecting) {
2882             // break up the selection into page-relative pieces
2883             d->tableSelectionParts.clear();
2884             const Okular::Page *okularPage = nullptr;
2885             for (PageViewItem *item : qAsConst(d->items)) {
2886                 if (!item->isVisible())
2887                     continue;
2888 
2889                 const QRect &itemRect = item->croppedGeometry();
2890                 if (selectionRect.intersects(itemRect)) {
2891                     // request the textpage if there isn't one
2892                     okularPage = item->page();
2893                     qCDebug(OkularUiDebug) << "checking if page" << item->pageNumber() << "has text:" << okularPage->hasTextPage();
2894                     if (!okularPage->hasTextPage())
2895                         d->document->requestTextPage(okularPage->number());
2896                     // grab text in the rect that intersects itemRect
2897                     QRect rectInItem = selectionRect.intersected(itemRect);
2898                     rectInItem.translate(-item->uncroppedGeometry().topLeft());
2899                     QRect rectInSelection = selectionRect.intersected(itemRect);
2900                     rectInSelection.translate(-selectionRect.topLeft());
2901                     d->tableSelectionParts.append(
2902                         TableSelectionPart(item, Okular::NormalizedRect(rectInItem, item->uncroppedWidth(), item->uncroppedHeight()), Okular::NormalizedRect(rectInSelection, selectionRect.width(), selectionRect.height())));
2903                 }
2904             }
2905 
2906             QRect updatedRect = d->mouseSelectionRect.normalized().adjusted(0, 0, 1, 1);
2907             updatedRect.translate(-contentAreaPosition());
2908             d->mouseSelecting = false;
2909             d->mouseSelectionRect.setCoords(0, 0, 0, 0);
2910             d->tableSelectionCols.clear();
2911             d->tableSelectionRows.clear();
2912             guessTableDividers();
2913             viewport()->update(updatedRect);
2914         }
2915 
2916         if (!d->document->isAllowed(Okular::AllowCopy)) {
2917             d->messageWindow->display(i18n("Copy forbidden by DRM"), QString(), PageViewMessage::Info, -1);
2918             break;
2919         }
2920 
2921         QClipboard *cb = QApplication::clipboard();
2922         if (cb->supportsSelection())
2923             cb->setMimeData(getTableContents(), QClipboard::Selection);
2924 
2925     } break;
2926 
2927     case Okular::Settings::EnumMouseMode::TextSelect:
2928         // if it is a left release checks if is over a previous link press
2929         if (leftButton && mouseReleaseOverLink(d->mouseOverLinkObject)) {
2930             selectionClear();
2931             break;
2932         }
2933 
2934         if (d->mouseTextSelecting) {
2935             d->mouseTextSelecting = false;
2936             //                    textSelectionClear();
2937             if (d->document->isAllowed(Okular::AllowCopy)) {
2938                 const QString text = d->selectedText();
2939                 if (!text.isEmpty()) {
2940                     QClipboard *cb = QApplication::clipboard();
2941                     if (cb->supportsSelection())
2942                         cb->setText(text, QClipboard::Selection);
2943                 }
2944             }
2945         } else if (!d->mousePressPos.isNull() && rightButton) {
2946             PageViewItem *item = pickItemOnPoint(eventPos.x(), eventPos.y());
2947             const Okular::Page *page;
2948             // if there is text selected in the page
2949             if (item) {
2950                 QAction *httpLink = nullptr;
2951                 QAction *textToClipboard = nullptr;
2952                 QString url;
2953 
2954                 QMenu *menu = createProcessLinkMenu(item, eventPos);
2955                 const bool mouseClickOverLink = (menu != nullptr);
2956 #ifdef HAVE_SPEECH
2957                 QAction *speakText = nullptr;
2958 #endif
2959                 if ((page = item->page())->textSelection()) {
2960                     if (!menu) {
2961                         menu = new QMenu(this);
2962                     }
2963                     textToClipboard = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy Text"));
2964 
2965 #ifdef HAVE_SPEECH
2966                     if (Okular::Settings::useTTS())
2967                         speakText = menu->addAction(QIcon::fromTheme(QStringLiteral("text-speak")), i18n("Speak Text"));
2968 #endif
2969                     if (!d->document->isAllowed(Okular::AllowCopy)) {
2970                         textToClipboard->setEnabled(false);
2971                         textToClipboard->setText(i18n("Copy forbidden by DRM"));
2972                     } else {
2973                         addSearchWithinDocumentAction(menu, d->selectedText());
2974                         addWebShortcutsMenu(menu, d->selectedText());
2975                     }
2976 
2977                     // if the right-click was over a link add "Follow This link" instead of "Go to"
2978                     if (!mouseClickOverLink) {
2979                         url = UrlUtils::getUrl(d->selectedText());
2980                         if (!url.isEmpty()) {
2981                             const QString squeezedText = KStringHandler::rsqueeze(url, linkTextPreviewLength);
2982                             httpLink = menu->addAction(i18n("Go to '%1'", squeezedText));
2983                             httpLink->setObjectName(QStringLiteral("GoToAction"));
2984                         }
2985                     }
2986                 }
2987 
2988                 if (menu) {
2989                     menu->setObjectName(QStringLiteral("PopupMenu"));
2990 
2991                     QAction *choice = menu->exec(e->globalPos());
2992                     // check if the user really selected an action
2993                     if (choice) {
2994                         if (choice == textToClipboard)
2995                             copyTextSelection();
2996 #ifdef HAVE_SPEECH
2997                         else if (choice == speakText) {
2998                             const QString text = d->selectedText();
2999                             d->tts()->say(text);
3000                         }
3001 #endif
3002                         else if (choice == httpLink) {
3003                             new KRun(QUrl(url), this);
3004                         }
3005                     }
3006 
3007                     menu->deleteLater();
3008                 }
3009             }
3010         }
3011         break;
3012     }
3013 
3014     // reset mouse press / 'drag start' position
3015     d->mousePressPos = QPoint();
3016 }
3017 
guessTableDividers()3018 void PageView::guessTableDividers()
3019 {
3020     QList<QPair<double, int>> colTicks, rowTicks, colSelectionTicks, rowSelectionTicks;
3021 
3022     for (const TableSelectionPart &tsp : qAsConst(d->tableSelectionParts)) {
3023         // add ticks for the edges of this area...
3024         colSelectionTicks.append(qMakePair(tsp.rectInSelection.left, +1));
3025         colSelectionTicks.append(qMakePair(tsp.rectInSelection.right, -1));
3026         rowSelectionTicks.append(qMakePair(tsp.rectInSelection.top, +1));
3027         rowSelectionTicks.append(qMakePair(tsp.rectInSelection.bottom, -1));
3028 
3029         // get the words in this part
3030         Okular::RegularAreaRect rects;
3031         rects.append(tsp.rectInItem);
3032         const Okular::TextEntity::List words = tsp.item->page()->words(&rects, Okular::TextPage::CentralPixelTextAreaInclusionBehaviour);
3033 
3034         for (const Okular::TextEntity *te : words) {
3035             if (te->text().isEmpty()) {
3036                 delete te;
3037                 continue;
3038             }
3039 
3040             Okular::NormalizedRect wordArea = *te->area();
3041 
3042             // convert it from item coordinates to part coordinates
3043             wordArea.left -= tsp.rectInItem.left;
3044             wordArea.left /= (tsp.rectInItem.right - tsp.rectInItem.left);
3045             wordArea.right -= tsp.rectInItem.left;
3046             wordArea.right /= (tsp.rectInItem.right - tsp.rectInItem.left);
3047             wordArea.top -= tsp.rectInItem.top;
3048             wordArea.top /= (tsp.rectInItem.bottom - tsp.rectInItem.top);
3049             wordArea.bottom -= tsp.rectInItem.top;
3050             wordArea.bottom /= (tsp.rectInItem.bottom - tsp.rectInItem.top);
3051 
3052             // convert from part coordinates to table coordinates
3053             wordArea.left *= (tsp.rectInSelection.right - tsp.rectInSelection.left);
3054             wordArea.left += tsp.rectInSelection.left;
3055             wordArea.right *= (tsp.rectInSelection.right - tsp.rectInSelection.left);
3056             wordArea.right += tsp.rectInSelection.left;
3057             wordArea.top *= (tsp.rectInSelection.bottom - tsp.rectInSelection.top);
3058             wordArea.top += tsp.rectInSelection.top;
3059             wordArea.bottom *= (tsp.rectInSelection.bottom - tsp.rectInSelection.top);
3060             wordArea.bottom += tsp.rectInSelection.top;
3061 
3062             // add to the ticks arrays...
3063             colTicks.append(qMakePair(wordArea.left, +1));
3064             colTicks.append(qMakePair(wordArea.right, -1));
3065             rowTicks.append(qMakePair(wordArea.top, +1));
3066             rowTicks.append(qMakePair(wordArea.bottom, -1));
3067 
3068             delete te;
3069         }
3070     }
3071 
3072     int tally = 0;
3073 
3074     std::sort(colSelectionTicks.begin(), colSelectionTicks.end());
3075     std::sort(rowSelectionTicks.begin(), rowSelectionTicks.end());
3076 
3077     for (int i = 0; i < colSelectionTicks.length(); ++i) {
3078         tally += colSelectionTicks[i].second;
3079         if (tally == 0 && i + 1 < colSelectionTicks.length() && colSelectionTicks[i + 1].first != colSelectionTicks[i].first) {
3080             colTicks.append(qMakePair(colSelectionTicks[i].first, +1));
3081             colTicks.append(qMakePair(colSelectionTicks[i + 1].first, -1));
3082         }
3083     }
3084     Q_ASSERT(tally == 0);
3085 
3086     for (int i = 0; i < rowSelectionTicks.length(); ++i) {
3087         tally += rowSelectionTicks[i].second;
3088         if (tally == 0 && i + 1 < rowSelectionTicks.length() && rowSelectionTicks[i + 1].first != rowSelectionTicks[i].first) {
3089             rowTicks.append(qMakePair(rowSelectionTicks[i].first, +1));
3090             rowTicks.append(qMakePair(rowSelectionTicks[i + 1].first, -1));
3091         }
3092     }
3093     Q_ASSERT(tally == 0);
3094 
3095     std::sort(colTicks.begin(), colTicks.end());
3096     std::sort(rowTicks.begin(), rowTicks.end());
3097 
3098     for (int i = 0; i < colTicks.length(); ++i) {
3099         tally += colTicks[i].second;
3100         if (tally == 0 && i + 1 < colTicks.length() && colTicks[i + 1].first != colTicks[i].first) {
3101             d->tableSelectionCols.append((colTicks[i].first + colTicks[i + 1].first) / 2);
3102             d->tableDividersGuessed = true;
3103         }
3104     }
3105     Q_ASSERT(tally == 0);
3106 
3107     for (int i = 0; i < rowTicks.length(); ++i) {
3108         tally += rowTicks[i].second;
3109         if (tally == 0 && i + 1 < rowTicks.length() && rowTicks[i + 1].first != rowTicks[i].first) {
3110             d->tableSelectionRows.append((rowTicks[i].first + rowTicks[i + 1].first) / 2);
3111             d->tableDividersGuessed = true;
3112         }
3113     }
3114     Q_ASSERT(tally == 0);
3115 }
3116 
mouseDoubleClickEvent(QMouseEvent * e)3117 void PageView::mouseDoubleClickEvent(QMouseEvent *e)
3118 {
3119     d->controlWheelAccumulatedDelta = 0;
3120 
3121     if (e->button() == Qt::LeftButton) {
3122         const QPoint eventPos = contentAreaPoint(e->pos());
3123         PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
3124         if (pageItem) {
3125             // find out normalized mouse coords inside current item
3126             double nX = pageItem->absToPageX(eventPos.x());
3127             double nY = pageItem->absToPageY(eventPos.y());
3128 
3129             if (d->mouseMode == Okular::Settings::EnumMouseMode::TextSelect) {
3130                 textSelectionClear();
3131 
3132                 Okular::RegularAreaRect *wordRect = pageItem->page()->wordAt(Okular::NormalizedPoint(nX, nY));
3133                 if (wordRect) {
3134                     // TODO words with hyphens across pages
3135                     d->document->setPageTextSelection(pageItem->pageNumber(), wordRect, palette().color(QPalette::Active, QPalette::Highlight));
3136                     d->pagesWithTextSelection << pageItem->pageNumber();
3137                     if (d->document->isAllowed(Okular::AllowCopy)) {
3138                         const QString text = d->selectedText();
3139                         if (!text.isEmpty()) {
3140                             QClipboard *cb = QApplication::clipboard();
3141                             if (cb->supportsSelection())
3142                                 cb->setText(text, QClipboard::Selection);
3143                         }
3144                     }
3145                     return;
3146                 }
3147             }
3148 
3149             const QRect &itemRect = pageItem->uncroppedGeometry();
3150             Okular::Annotation *ann = nullptr;
3151             const Okular::ObjectRect *orect = pageItem->page()->objectRect(Okular::ObjectRect::OAnnotation, nX, nY, itemRect.width(), itemRect.height());
3152             if (orect)
3153                 ann = ((Okular::AnnotationObjectRect *)orect)->annotation();
3154             if (ann && ann->subType() != Okular::Annotation::AWidget) {
3155                 openAnnotationWindow(ann, pageItem->pageNumber());
3156             }
3157         }
3158     }
3159 }
3160 
wheelEvent(QWheelEvent * e)3161 void PageView::wheelEvent(QWheelEvent *e)
3162 {
3163     if (!d->document->isOpened()) {
3164         QAbstractScrollArea::wheelEvent(e);
3165         return;
3166     }
3167 
3168     int delta = e->angleDelta().y(), vScroll = verticalScrollBar()->value();
3169     e->accept();
3170     if ((e->modifiers() & Qt::ControlModifier) == Qt::ControlModifier) {
3171         d->controlWheelAccumulatedDelta += delta;
3172         if (d->controlWheelAccumulatedDelta <= -QWheelEvent::DefaultDeltasPerStep) {
3173             slotZoomOut();
3174             d->controlWheelAccumulatedDelta = 0;
3175         } else if (d->controlWheelAccumulatedDelta >= QWheelEvent::DefaultDeltasPerStep) {
3176             slotZoomIn();
3177             d->controlWheelAccumulatedDelta = 0;
3178         }
3179     } else {
3180         d->controlWheelAccumulatedDelta = 0;
3181 
3182         if (delta <= -QWheelEvent::DefaultDeltasPerStep && !getContinuousMode() && vScroll == verticalScrollBar()->maximum()) {
3183             // go to next page
3184             if ((int)d->document->currentPage() < d->items.count() - 1) {
3185                 // more optimized than document->setNextPage and then move view to top
3186                 Okular::DocumentViewport newViewport = d->document->viewport();
3187                 newViewport.pageNumber += viewColumns();
3188                 if (newViewport.pageNumber >= (int)d->items.count())
3189                     newViewport.pageNumber = d->items.count() - 1;
3190                 newViewport.rePos.enabled = true;
3191                 newViewport.rePos.normalizedY = 0.0;
3192                 d->document->setViewport(newViewport);
3193                 d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); // sync scroller with scrollbar
3194             }
3195         } else if (delta >= QWheelEvent::DefaultDeltasPerStep && !getContinuousMode() && vScroll == verticalScrollBar()->minimum()) {
3196             // go to prev page
3197             if (d->document->currentPage() > 0) {
3198                 // more optimized than document->setPrevPage and then move view to bottom
3199                 Okular::DocumentViewport newViewport = d->document->viewport();
3200                 newViewport.pageNumber -= viewColumns();
3201                 if (newViewport.pageNumber < 0)
3202                     newViewport.pageNumber = 0;
3203                 newViewport.rePos.enabled = true;
3204                 newViewport.rePos.normalizedY = 1.0;
3205                 d->document->setViewport(newViewport);
3206                 d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); // sync scroller with scrollbar
3207             }
3208         } else {
3209             // When the shift key is held down, scroll ten times faster
3210             int multiplier = e->modifiers() & Qt::ShiftModifier ? 10 : 1;
3211 
3212             if (delta != 0 && delta % QWheelEvent::DefaultDeltasPerStep == 0) {
3213                 // number of scroll wheel steps Qt gives to us at the same time
3214                 int count = abs(delta / QWheelEvent::DefaultDeltasPerStep) * multiplier;
3215                 if (delta < 0) {
3216                     slotScrollDown(count);
3217                 } else {
3218                     slotScrollUp(count);
3219                 }
3220             } else {
3221                 d->scroller->scrollTo(d->scroller->finalPosition() - e->angleDelta() * multiplier / 4.0, 0);
3222             }
3223         }
3224     }
3225 }
3226 
viewportEvent(QEvent * e)3227 bool PageView::viewportEvent(QEvent *e)
3228 {
3229     if (e->type() == QEvent::ToolTip
3230         // Show tool tips only for those modes that change the cursor
3231         // to a hand when hovering over the link.
3232         && (d->mouseMode == Okular::Settings::EnumMouseMode::Browse || d->mouseMode == Okular::Settings::EnumMouseMode::RectSelect || d->mouseMode == Okular::Settings::EnumMouseMode::TextSelect ||
3233             d->mouseMode == Okular::Settings::EnumMouseMode::TrimSelect)) {
3234         QHelpEvent *he = static_cast<QHelpEvent *>(e);
3235         if (d->mouseAnnotation->isMouseOver()) {
3236             d->mouseAnnotation->routeTooltipEvent(he);
3237         } else {
3238             const QPoint eventPos = contentAreaPoint(he->pos());
3239             PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y());
3240             const Okular::ObjectRect *rect = nullptr;
3241             const Okular::Action *link = nullptr;
3242             if (pageItem) {
3243                 double nX = pageItem->absToPageX(eventPos.x());
3244                 double nY = pageItem->absToPageY(eventPos.y());
3245                 rect = pageItem->page()->objectRect(Okular::ObjectRect::Action, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight());
3246                 if (rect)
3247                     link = static_cast<const Okular::Action *>(rect->object());
3248             }
3249 
3250             if (link) {
3251                 QRect r = rect->boundingRect(pageItem->uncroppedWidth(), pageItem->uncroppedHeight());
3252                 r.translate(pageItem->uncroppedGeometry().topLeft());
3253                 r.translate(-contentAreaPosition());
3254                 QString tip = link->actionTip();
3255                 if (!tip.isEmpty())
3256                     QToolTip::showText(he->globalPos(), tip, viewport(), r);
3257             }
3258         }
3259         e->accept();
3260         return true;
3261     } else
3262         // do not stop the event
3263         return QAbstractScrollArea::viewportEvent(e);
3264 }
3265 
scrollContentsBy(int dx,int dy)3266 void PageView::scrollContentsBy(int dx, int dy)
3267 {
3268     const QRect r = viewport()->rect();
3269     viewport()->scroll(dx, dy, r);
3270     // HACK manually repaint the damaged regions, as it seems some updates are missed
3271     // thus leaving artifacts around
3272     QRegion rgn(r);
3273     rgn -= rgn & r.translated(dx, dy);
3274 
3275     for (const QRect &rect : rgn)
3276         viewport()->update(rect);
3277 
3278     updateCursor();
3279 }
3280 // END widget events
3281 
textSelections(const QPoint start,const QPoint end,int & firstpage)3282 QList<Okular::RegularAreaRect *> PageView::textSelections(const QPoint start, const QPoint end, int &firstpage)
3283 {
3284     firstpage = -1;
3285     QList<Okular::RegularAreaRect *> ret;
3286     QSet<int> affectedItemsSet;
3287     QRect selectionRect = QRect(start, end).normalized();
3288     for (const PageViewItem *item : qAsConst(d->items)) {
3289         if (item->isVisible() && selectionRect.intersects(item->croppedGeometry()))
3290             affectedItemsSet.insert(item->pageNumber());
3291     }
3292 #ifdef PAGEVIEW_DEBUG
3293     qCDebug(OkularUiDebug) << ">>>> item selected by mouse:" << affectedItemsSet.count();
3294 #endif
3295 
3296     if (!affectedItemsSet.isEmpty()) {
3297         // is the mouse drag line the ne-sw diagonal of the selection rect?
3298         bool direction_ne_sw = start == selectionRect.topRight() || start == selectionRect.bottomLeft();
3299 
3300         int tmpmin = d->document->pages();
3301         int tmpmax = 0;
3302         for (const int p : qAsConst(affectedItemsSet)) {
3303             if (p < tmpmin)
3304                 tmpmin = p;
3305             if (p > tmpmax)
3306                 tmpmax = p;
3307         }
3308 
3309         PageViewItem *a = pickItemOnPoint((int)(direction_ne_sw ? selectionRect.right() : selectionRect.left()), (int)selectionRect.top());
3310         int min = a && (a->pageNumber() != tmpmax) ? a->pageNumber() : tmpmin;
3311         PageViewItem *b = pickItemOnPoint((int)(direction_ne_sw ? selectionRect.left() : selectionRect.right()), (int)selectionRect.bottom());
3312         int max = b && (b->pageNumber() != tmpmin) ? b->pageNumber() : tmpmax;
3313 
3314         QList<int> affectedItemsIds;
3315         for (int i = min; i <= max; ++i)
3316             affectedItemsIds.append(i);
3317 #ifdef PAGEVIEW_DEBUG
3318         qCDebug(OkularUiDebug) << ">>>> pages:" << affectedItemsIds;
3319 #endif
3320         firstpage = affectedItemsIds.first();
3321 
3322         if (affectedItemsIds.count() == 1) {
3323             PageViewItem *item = d->items[affectedItemsIds.first()];
3324             selectionRect.translate(-item->uncroppedGeometry().topLeft());
3325             ret.append(textSelectionForItem(item, direction_ne_sw ? selectionRect.topRight() : selectionRect.topLeft(), direction_ne_sw ? selectionRect.bottomLeft() : selectionRect.bottomRight()));
3326         } else if (affectedItemsIds.count() > 1) {
3327             // first item
3328             PageViewItem *first = d->items[affectedItemsIds.first()];
3329             QRect geom = first->croppedGeometry().intersected(selectionRect).translated(-first->uncroppedGeometry().topLeft());
3330             ret.append(textSelectionForItem(first, selectionRect.bottom() > geom.height() ? (direction_ne_sw ? geom.topRight() : geom.topLeft()) : (direction_ne_sw ? geom.bottomRight() : geom.bottomLeft()), QPoint()));
3331             // last item
3332             PageViewItem *last = d->items[affectedItemsIds.last()];
3333             geom = last->croppedGeometry().intersected(selectionRect).translated(-last->uncroppedGeometry().topLeft());
3334             // the last item needs to appended at last...
3335             Okular::RegularAreaRect *lastArea =
3336                 textSelectionForItem(last, QPoint(), selectionRect.bottom() > geom.height() ? (direction_ne_sw ? geom.bottomLeft() : geom.bottomRight()) : (direction_ne_sw ? geom.topLeft() : geom.topRight()));
3337             affectedItemsIds.removeFirst();
3338             affectedItemsIds.removeLast();
3339             // item between the two above
3340             for (const int page : qAsConst(affectedItemsIds)) {
3341                 ret.append(textSelectionForItem(d->items[page]));
3342             }
3343             ret.append(lastArea);
3344         }
3345     }
3346     return ret;
3347 }
3348 
drawDocumentOnPainter(const QRect contentsRect,QPainter * p)3349 void PageView::drawDocumentOnPainter(const QRect contentsRect, QPainter *p)
3350 {
3351     QColor backColor;
3352 
3353     if (Okular::Settings::useCustomBackgroundColor())
3354         backColor = Okular::Settings::backgroundColor();
3355     else
3356         backColor = viewport()->palette().color(QPalette::Dark);
3357 
3358     // create a region from which we'll subtract painted rects
3359     QRegion remainingArea(contentsRect);
3360 
3361     // This loop draws the actual pages
3362     // iterate over all items painting the ones intersecting contentsRect
3363     for (const PageViewItem *item : qAsConst(d->items)) {
3364         // check if a piece of the page intersects the contents rect
3365         if (!item->isVisible() || !item->croppedGeometry().intersects(contentsRect))
3366             continue;
3367 
3368         // get item and item's outline geometries
3369         QRect itemGeometry = item->croppedGeometry();
3370 
3371         // move the painter to the top-left corner of the real page
3372         p->save();
3373         p->translate(itemGeometry.left(), itemGeometry.top());
3374 
3375         // draw the page using the PagePainter with all flags active
3376         if (contentsRect.intersects(itemGeometry)) {
3377             Okular::NormalizedPoint *viewPortPoint = nullptr;
3378             Okular::NormalizedPoint point(d->lastSourceLocationViewportNormalizedX, d->lastSourceLocationViewportNormalizedY);
3379             if (Okular::Settings::showSourceLocationsGraphically() && item->pageNumber() == d->lastSourceLocationViewportPageNumber) {
3380                 viewPortPoint = &point;
3381             }
3382             QRect pixmapRect = contentsRect.intersected(itemGeometry);
3383             pixmapRect.translate(-item->croppedGeometry().topLeft());
3384             PagePainter::paintCroppedPageOnPainter(p, item->page(), this, pageflags, item->uncroppedWidth(), item->uncroppedHeight(), pixmapRect, item->crop(), viewPortPoint);
3385         }
3386 
3387         // remove painted area from 'remainingArea' and restore painter
3388         remainingArea -= itemGeometry;
3389         p->restore();
3390     }
3391 
3392     // fill the visible area around the page with the background color
3393     for (const QRect &backRect : remainingArea)
3394         p->fillRect(backRect, backColor);
3395 
3396     // take outline and shadow into account when testing whether a repaint is necessary
3397     auto dpr = devicePixelRatioF();
3398     QRect checkRect = contentsRect;
3399     checkRect.adjust(-3, -3, 1, 1);
3400 
3401     // Method to linearly interpolate between black (=(0,0,0), omitted) and the background color
3402     auto interpolateColor = [&backColor](double t) { return QColor(t * backColor.red(), t * backColor.green(), t * backColor.blue()); };
3403 
3404     // width of the shadow in device pixels
3405     static const int shadowWidth = 2 * dpr;
3406 
3407     // iterate over all items painting a black outline and a simple bottom/right gradient
3408     for (const PageViewItem *item : qAsConst(d->items)) {
3409         // check if a piece of the page intersects the contents rect
3410         if (!item->isVisible() || !item->croppedGeometry().intersects(checkRect))
3411             continue;
3412 
3413         // get item and item's outline geometries
3414         QRect itemGeometry = item->croppedGeometry();
3415 
3416         // move the painter to the top-left corner of the real page
3417         p->save();
3418         p->translate(itemGeometry.left(), itemGeometry.top());
3419 
3420         // draw the page outline (black border and bottom-right shadow)
3421         if (!itemGeometry.contains(contentsRect)) {
3422             int itemWidth = itemGeometry.width();
3423             int itemHeight = itemGeometry.height();
3424             // draw simple outline
3425             QPen pen(Qt::black);
3426             pen.setWidth(0);
3427             p->setPen(pen);
3428 
3429             QRectF outline(-1.0 / dpr, -1.0 / dpr, itemWidth + 1.0 / dpr, itemHeight + 1.0 / dpr);
3430             p->drawRect(outline);
3431 
3432             // draw bottom/right gradient
3433             for (int i = 1; i <= shadowWidth; i++) {
3434                 pen.setColor(interpolateColor(double(i) / (shadowWidth + 1)));
3435                 p->setPen(pen);
3436                 QPointF left((i - 1) / dpr, itemHeight + i / dpr);
3437                 QPointF up(itemWidth + i / dpr, (i - 1) / dpr);
3438                 QPointF corner(itemWidth + i / dpr, itemHeight + i / dpr);
3439                 p->drawLine(left, corner);
3440                 p->drawLine(up, corner);
3441             }
3442         }
3443 
3444         p->restore();
3445     }
3446 }
3447 
updateItemSize(PageViewItem * item,int colWidth,int rowHeight)3448 void PageView::updateItemSize(PageViewItem *item, int colWidth, int rowHeight)
3449 {
3450     const Okular::Page *okularPage = item->page();
3451     double width = okularPage->width(), height = okularPage->height(), zoom = d->zoomFactor;
3452     Okular::NormalizedRect crop(0., 0., 1., 1.);
3453 
3454     // Handle cropping, due to either "Trim Margin" or "Trim to Selection" cases
3455     if ((Okular::Settings::trimMargins() && okularPage->isBoundingBoxKnown() && !okularPage->boundingBox().isNull()) || (d->aTrimToSelection && d->aTrimToSelection->isChecked() && !d->trimBoundingBox.isNull())) {
3456         crop = Okular::Settings::trimMargins() ? okularPage->boundingBox() : d->trimBoundingBox;
3457 
3458         // Rotate the bounding box
3459         for (int i = okularPage->rotation(); i > 0; --i) {
3460             Okular::NormalizedRect rot = crop;
3461             crop.left = 1 - rot.bottom;
3462             crop.top = rot.left;
3463             crop.right = 1 - rot.top;
3464             crop.bottom = rot.right;
3465         }
3466 
3467         // Expand the crop slightly beyond the bounding box (for Trim Margins only)
3468         if (Okular::Settings::trimMargins()) {
3469             static const double cropExpandRatio = 0.04;
3470             const double cropExpand = cropExpandRatio * ((crop.right - crop.left) + (crop.bottom - crop.top)) / 2;
3471             crop = Okular::NormalizedRect(crop.left - cropExpand, crop.top - cropExpand, crop.right + cropExpand, crop.bottom + cropExpand) & Okular::NormalizedRect(0, 0, 1, 1);
3472         }
3473 
3474         // We currently generate a larger image and then crop it, so if the
3475         // crop rect is very small the generated image is huge. Hence, we shouldn't
3476         // let the crop rect become too small.
3477         static double minCropRatio;
3478         if (Okular::Settings::trimMargins()) {
3479             // Make sure we crop by at most 50% in either dimension:
3480             minCropRatio = 0.5;
3481         } else {
3482             // Looser Constraint for "Trim Selection"
3483             minCropRatio = 0.20;
3484         }
3485         if ((crop.right - crop.left) < minCropRatio) {
3486             const double newLeft = (crop.left + crop.right) / 2 - minCropRatio / 2;
3487             crop.left = qMax(0.0, qMin(1.0 - minCropRatio, newLeft));
3488             crop.right = crop.left + minCropRatio;
3489         }
3490         if ((crop.bottom - crop.top) < minCropRatio) {
3491             const double newTop = (crop.top + crop.bottom) / 2 - minCropRatio / 2;
3492             crop.top = qMax(0.0, qMin(1.0 - minCropRatio, newTop));
3493             crop.bottom = crop.top + minCropRatio;
3494         }
3495 
3496         width *= (crop.right - crop.left);
3497         height *= (crop.bottom - crop.top);
3498 #ifdef PAGEVIEW_DEBUG
3499         qCDebug(OkularUiDebug) << "Cropped page" << okularPage->number() << "to" << crop << "width" << width << "height" << height << "by bbox" << okularPage->boundingBox();
3500 #endif
3501     }
3502 
3503     if (d->zoomMode == ZoomFixed) {
3504         width *= zoom;
3505         height *= zoom;
3506         item->setWHZC((int)width, (int)height, d->zoomFactor, crop);
3507     } else if (d->zoomMode == ZoomFitWidth) {
3508         height = (height / width) * colWidth;
3509         zoom = (double)colWidth / width;
3510         item->setWHZC(colWidth, (int)height, zoom, crop);
3511         if ((uint)item->pageNumber() == d->document->currentPage())
3512             d->zoomFactor = zoom;
3513     } else if (d->zoomMode == ZoomFitPage) {
3514         const double scaleW = (double)colWidth / (double)width;
3515         const double scaleH = (double)rowHeight / (double)height;
3516         zoom = qMin(scaleW, scaleH);
3517         item->setWHZC((int)(zoom * width), (int)(zoom * height), zoom, crop);
3518         if ((uint)item->pageNumber() == d->document->currentPage())
3519             d->zoomFactor = zoom;
3520     } else if (d->zoomMode == ZoomFitAuto) {
3521         const double aspectRatioRelation = 1.25; // relation between aspect ratios for "auto fit"
3522         const double uiAspect = (double)rowHeight / (double)colWidth;
3523         const double pageAspect = (double)height / (double)width;
3524         const double rel = uiAspect / pageAspect;
3525 
3526         if (!getContinuousMode() && rel > aspectRatioRelation) {
3527             // UI space is relatively much higher than the page
3528             zoom = (double)rowHeight / (double)height;
3529         } else if (rel < 1.0 / aspectRatioRelation) {
3530             // UI space is relatively much wider than the page in relation
3531             zoom = (double)colWidth / (double)width;
3532         } else {
3533             // aspect ratios of page and UI space are very similar
3534             const double scaleW = (double)colWidth / (double)width;
3535             const double scaleH = (double)rowHeight / (double)height;
3536             zoom = qMin(scaleW, scaleH);
3537         }
3538         item->setWHZC((int)(zoom * width), (int)(zoom * height), zoom, crop);
3539         if ((uint)item->pageNumber() == d->document->currentPage())
3540             d->zoomFactor = zoom;
3541     }
3542 #ifndef NDEBUG
3543     else
3544         qCDebug(OkularUiDebug) << "calling updateItemSize with unrecognized d->zoomMode!";
3545 #endif
3546 }
3547 
pickItemOnPoint(int x,int y)3548 PageViewItem *PageView::pickItemOnPoint(int x, int y)
3549 {
3550     PageViewItem *item = nullptr;
3551     for (PageViewItem *i : qAsConst(d->visibleItems)) {
3552         const QRect &r = i->croppedGeometry();
3553         if (x < r.right() && x > r.left() && y < r.bottom()) {
3554             if (y > r.top())
3555                 item = i;
3556             break;
3557         }
3558     }
3559     return item;
3560 }
3561 
textSelectionClear()3562 void PageView::textSelectionClear()
3563 {
3564     // something to clear
3565     if (!d->pagesWithTextSelection.isEmpty()) {
3566         for (const int page : qAsConst(d->pagesWithTextSelection))
3567             d->document->setPageTextSelection(page, nullptr, QColor());
3568         d->pagesWithTextSelection.clear();
3569     }
3570 }
3571 
selectionStart(const QPoint pos,const QColor & color,bool)3572 void PageView::selectionStart(const QPoint pos, const QColor &color, bool /*aboveAll*/)
3573 {
3574     selectionClear();
3575     d->mouseSelecting = true;
3576     d->mouseSelectionRect.setRect(pos.x(), pos.y(), 1, 1);
3577     d->mouseSelectionColor = color;
3578     // ensures page doesn't scroll
3579     if (d->autoScrollTimer) {
3580         d->scrollIncrement = 0;
3581         d->autoScrollTimer->stop();
3582     }
3583 }
3584 
scrollPosIntoView(const QPoint pos)3585 void PageView::scrollPosIntoView(const QPoint pos)
3586 {
3587     // 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
3588     const int damping = 6;
3589 
3590     if (pos.x() < horizontalScrollBar()->value())
3591         d->dragScrollVector.setX((pos.x() - horizontalScrollBar()->value()) / damping);
3592     else if (horizontalScrollBar()->value() + viewport()->width() < pos.x())
3593         d->dragScrollVector.setX((pos.x() - horizontalScrollBar()->value() - viewport()->width()) / damping);
3594     else
3595         d->dragScrollVector.setX(0);
3596 
3597     if (pos.y() < verticalScrollBar()->value())
3598         d->dragScrollVector.setY((pos.y() - verticalScrollBar()->value()) / damping);
3599     else if (verticalScrollBar()->value() + viewport()->height() < pos.y())
3600         d->dragScrollVector.setY((pos.y() - verticalScrollBar()->value() - viewport()->height()) / damping);
3601     else
3602         d->dragScrollVector.setY(0);
3603 
3604     if (d->dragScrollVector != QPoint(0, 0)) {
3605         if (!d->dragScrollTimer.isActive())
3606             d->dragScrollTimer.start(1000 / 60); // 60 fps
3607     } else
3608         d->dragScrollTimer.stop();
3609 }
3610 
viewportToContentArea(const Okular::DocumentViewport & vp) const3611 QPoint PageView::viewportToContentArea(const Okular::DocumentViewport &vp) const
3612 {
3613     Q_ASSERT(vp.pageNumber >= 0);
3614 
3615     const QRect &r = d->items[vp.pageNumber]->croppedGeometry();
3616     QPoint c {r.left(), r.top()};
3617 
3618     if (vp.rePos.enabled) {
3619         // Convert the coordinates of vp to normalized coordinates on the cropped page.
3620         // This is a no-op if the page isn't cropped.
3621         const Okular::NormalizedRect &crop = d->items[vp.pageNumber]->crop();
3622         const double normalized_on_crop_x = (vp.rePos.normalizedX - crop.left) / (crop.right - crop.left);
3623         const double normalized_on_crop_y = (vp.rePos.normalizedY - crop.top) / (crop.bottom - crop.top);
3624 
3625         if (vp.rePos.pos == Okular::DocumentViewport::Center) {
3626             c.rx() += qRound(normClamp(normalized_on_crop_x, 0.5) * (double)r.width());
3627             c.ry() += qRound(normClamp(normalized_on_crop_y, 0.0) * (double)r.height());
3628         } else {
3629             // TopLeft
3630             c.rx() += qRound(normClamp(normalized_on_crop_x, 0.0) * (double)r.width() + viewport()->width() / 2.0);
3631             c.ry() += qRound(normClamp(normalized_on_crop_y, 0.0) * (double)r.height() + viewport()->height() / 2.0);
3632         }
3633     } else {
3634         // exact repositioning disabled, align page top margin with viewport top border by default
3635         c.rx() += r.width() / 2;
3636         c.ry() += viewport()->height() / 2 - 10;
3637     }
3638     return c;
3639 }
3640 
updateSelection(const QPoint pos)3641 void PageView::updateSelection(const QPoint pos)
3642 {
3643     if (d->mouseSelecting) {
3644         scrollPosIntoView(pos);
3645         // update the selection rect
3646         QRect updateRect = d->mouseSelectionRect;
3647         d->mouseSelectionRect.setBottomLeft(pos);
3648         updateRect |= d->mouseSelectionRect;
3649         updateRect.translate(-contentAreaPosition());
3650         viewport()->update(updateRect.adjusted(-1, -2, 2, 1));
3651     } else if (d->mouseTextSelecting) {
3652         scrollPosIntoView(pos);
3653         int first = -1;
3654         const QList<Okular::RegularAreaRect *> selections = textSelections(pos, d->mouseSelectPos, first);
3655         QSet<int> pagesWithSelectionSet;
3656         for (int i = 0; i < selections.count(); ++i)
3657             pagesWithSelectionSet.insert(i + first);
3658 
3659         const QSet<int> noMoreSelectedPages = d->pagesWithTextSelection - pagesWithSelectionSet;
3660         // clear the selection from pages not selected anymore
3661         for (int p : noMoreSelectedPages) {
3662             d->document->setPageTextSelection(p, nullptr, QColor());
3663         }
3664         // set the new selection for the selected pages
3665         for (int p : qAsConst(pagesWithSelectionSet)) {
3666             d->document->setPageTextSelection(p, selections[p - first], palette().color(QPalette::Active, QPalette::Highlight));
3667         }
3668         d->pagesWithTextSelection = pagesWithSelectionSet;
3669     }
3670 }
3671 
rotateInNormRect(const QPoint rotated,const QRect rect,Okular::Rotation rotation)3672 static Okular::NormalizedPoint rotateInNormRect(const QPoint rotated, const QRect rect, Okular::Rotation rotation)
3673 {
3674     Okular::NormalizedPoint ret;
3675 
3676     switch (rotation) {
3677     case Okular::Rotation0:
3678         ret = Okular::NormalizedPoint(rotated.x(), rotated.y(), rect.width(), rect.height());
3679         break;
3680     case Okular::Rotation90:
3681         ret = Okular::NormalizedPoint(rotated.y(), rect.width() - rotated.x(), rect.height(), rect.width());
3682         break;
3683     case Okular::Rotation180:
3684         ret = Okular::NormalizedPoint(rect.width() - rotated.x(), rect.height() - rotated.y(), rect.width(), rect.height());
3685         break;
3686     case Okular::Rotation270:
3687         ret = Okular::NormalizedPoint(rect.height() - rotated.y(), rotated.x(), rect.height(), rect.width());
3688         break;
3689     }
3690 
3691     return ret;
3692 }
3693 
textSelectionForItem(const PageViewItem * item,const QPoint startPoint,const QPoint endPoint)3694 Okular::RegularAreaRect *PageView::textSelectionForItem(const PageViewItem *item, const QPoint startPoint, const QPoint endPoint)
3695 {
3696     const QRect &geometry = item->uncroppedGeometry();
3697     Okular::NormalizedPoint startCursor(0.0, 0.0);
3698     if (!startPoint.isNull()) {
3699         startCursor = rotateInNormRect(startPoint, geometry, item->page()->rotation());
3700     }
3701     Okular::NormalizedPoint endCursor(1.0, 1.0);
3702     if (!endPoint.isNull()) {
3703         endCursor = rotateInNormRect(endPoint, geometry, item->page()->rotation());
3704     }
3705     Okular::TextSelection mouseTextSelectionInfo(startCursor, endCursor);
3706 
3707     const Okular::Page *okularPage = item->page();
3708 
3709     if (!okularPage->hasTextPage())
3710         d->document->requestTextPage(okularPage->number());
3711 
3712     Okular::RegularAreaRect *selectionArea = okularPage->textArea(&mouseTextSelectionInfo);
3713 #ifdef PAGEVIEW_DEBUG
3714     qCDebug(OkularUiDebug).nospace() << "text areas (" << okularPage->number() << "): " << (selectionArea ? QString::number(selectionArea->count()) : "(none)");
3715 #endif
3716     return selectionArea;
3717 }
3718 
selectionClear(const ClearMode mode)3719 void PageView::selectionClear(const ClearMode mode)
3720 {
3721     QRect updatedRect = d->mouseSelectionRect.normalized().adjusted(-2, -2, 2, 2);
3722     d->mouseSelecting = false;
3723     d->mouseSelectionRect.setCoords(0, 0, 0, 0);
3724     d->tableSelectionCols.clear();
3725     d->tableSelectionRows.clear();
3726     d->tableDividersGuessed = false;
3727     for (const TableSelectionPart &tsp : qAsConst(d->tableSelectionParts)) {
3728         QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight());
3729         selectionPartRect.translate(tsp.item->uncroppedGeometry().topLeft());
3730         // should check whether this is on-screen here?
3731         updatedRect = updatedRect.united(selectionPartRect);
3732     }
3733     if (mode != ClearOnlyDividers) {
3734         d->tableSelectionParts.clear();
3735     }
3736     d->tableSelectionParts.clear();
3737     updatedRect.translate(-contentAreaPosition());
3738     viewport()->update(updatedRect);
3739 }
3740 
3741 // const to be used for both zoomFactorFitMode function and slotRelayoutPages.
3742 static const int kcolWidthMargin = 6;
3743 static const int krowHeightMargin = 12;
3744 
zoomFactorFitMode(ZoomMode mode)3745 double PageView::zoomFactorFitMode(ZoomMode mode)
3746 {
3747     const int pageCount = d->items.count();
3748     if (pageCount == 0)
3749         return 0;
3750     const bool facingCentered = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered || (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount == 1);
3751     const bool overrideCentering = facingCentered && pageCount < 3;
3752     const int nCols = overrideCentering ? 1 : viewColumns();
3753     const int colWidth = viewport()->width() / nCols - kcolWidthMargin;
3754     const double rowHeight = viewport()->height() - krowHeightMargin;
3755     const PageViewItem *currentItem = d->items[qMax(0, (int)d->document->currentPage())];
3756     // prevent segmentation fault when opening a new document;
3757     if (!currentItem)
3758         return 0;
3759 
3760     // We need the real width/height of the cropped page.
3761     const Okular::Page *okularPage = currentItem->page();
3762     const double width = okularPage->width() * currentItem->crop().width();
3763     const double height = okularPage->height() * currentItem->crop().height();
3764 
3765     if (mode == ZoomFitWidth)
3766         return (double)colWidth / width;
3767     if (mode == ZoomFitPage) {
3768         const double scaleW = (double)colWidth / (double)width;
3769         const double scaleH = (double)rowHeight / (double)height;
3770         return qMin(scaleW, scaleH);
3771     }
3772     return 0;
3773 }
3774 
updateZoom(ZoomMode newZoomMode)3775 void PageView::updateZoom(ZoomMode newZoomMode)
3776 {
3777     if (newZoomMode == ZoomFixed) {
3778         if (d->aZoom->currentItem() == 0)
3779             newZoomMode = ZoomFitWidth;
3780         else if (d->aZoom->currentItem() == 1)
3781             newZoomMode = ZoomFitPage;
3782         else if (d->aZoom->currentItem() == 2)
3783             newZoomMode = ZoomFitAuto;
3784     }
3785 
3786     float newFactor = d->zoomFactor;
3787     QAction *checkedZoomAction = nullptr;
3788     switch (newZoomMode) {
3789     case ZoomFixed: { // ZoomFixed case
3790         QString z = d->aZoom->currentText();
3791         // kdelibs4 sometimes adds accelerators to actions' text directly :(
3792         z.remove(QLatin1Char('&'));
3793         z.remove(QLatin1Char('%'));
3794         newFactor = QLocale().toDouble(z) / 100.0;
3795     } break;
3796     case ZoomIn:
3797     case ZoomOut: {
3798         const float zoomFactorFitWidth = zoomFactorFitMode(ZoomFitWidth);
3799         const float zoomFactorFitPage = zoomFactorFitMode(ZoomFitPage);
3800 
3801         QVector<float> zoomValue(kZoomValues.size());
3802 
3803         std::copy(kZoomValues.begin(), kZoomValues.end(), zoomValue.begin());
3804         zoomValue.append(zoomFactorFitWidth);
3805         zoomValue.append(zoomFactorFitPage);
3806         std::sort(zoomValue.begin(), zoomValue.end());
3807 
3808         QVector<float>::iterator i;
3809         if (newZoomMode == ZoomOut) {
3810             if (newFactor <= zoomValue.first())
3811                 return;
3812             i = std::lower_bound(zoomValue.begin(), zoomValue.end(), newFactor) - 1;
3813         } else {
3814             if (newFactor >= zoomValue.last())
3815                 return;
3816             i = std::upper_bound(zoomValue.begin(), zoomValue.end(), newFactor);
3817         }
3818         const float tmpFactor = *i;
3819         if (tmpFactor == zoomFactorFitWidth) {
3820             newZoomMode = ZoomFitWidth;
3821             checkedZoomAction = d->aZoomFitWidth;
3822         } else if (tmpFactor == zoomFactorFitPage) {
3823             newZoomMode = ZoomFitPage;
3824             checkedZoomAction = d->aZoomFitPage;
3825         } else {
3826             newFactor = tmpFactor;
3827             newZoomMode = ZoomFixed;
3828         }
3829     } break;
3830     case ZoomActual:
3831         newZoomMode = ZoomFixed;
3832         newFactor = 1.0;
3833         break;
3834     case ZoomFitWidth:
3835         checkedZoomAction = d->aZoomFitWidth;
3836         break;
3837     case ZoomFitPage:
3838         checkedZoomAction = d->aZoomFitPage;
3839         break;
3840     case ZoomFitAuto:
3841         checkedZoomAction = d->aZoomAutoFit;
3842         break;
3843     case ZoomRefreshCurrent:
3844         newZoomMode = ZoomFixed;
3845         d->zoomFactor = -1;
3846         break;
3847     }
3848     const float upperZoomLimit = d->document->supportsTiles() ? 100.0 : 4.0;
3849     if (newFactor > upperZoomLimit)
3850         newFactor = upperZoomLimit;
3851     if (newFactor < kZoomValues[0])
3852         newFactor = kZoomValues[0];
3853 
3854     if (newZoomMode != d->zoomMode || (newZoomMode == ZoomFixed && newFactor != d->zoomFactor)) {
3855         // rebuild layout and update the whole viewport
3856         d->zoomMode = newZoomMode;
3857         d->zoomFactor = newFactor;
3858         // be sure to block updates to document's viewport
3859         bool prevState = d->blockViewport;
3860         d->blockViewport = true;
3861         slotRelayoutPages();
3862         d->blockViewport = prevState;
3863         // request pixmaps
3864         slotRequestVisiblePixmaps();
3865         // update zoom text
3866         updateZoomText();
3867         // update actions checked state
3868         if (d->aZoomFitWidth) {
3869             d->aZoomFitWidth->setChecked(checkedZoomAction == d->aZoomFitWidth);
3870             d->aZoomFitPage->setChecked(checkedZoomAction == d->aZoomFitPage);
3871             d->aZoomAutoFit->setChecked(checkedZoomAction == d->aZoomAutoFit);
3872         }
3873     } else if (newZoomMode == ZoomFixed && newFactor == d->zoomFactor)
3874         updateZoomText();
3875 
3876     updateZoomActionsEnabledStatus();
3877 }
3878 
updateZoomActionsEnabledStatus()3879 void PageView::updateZoomActionsEnabledStatus()
3880 {
3881     const float upperZoomLimit = d->document->supportsTiles() ? kZoomValues.back() : 4.0;
3882     const bool hasPages = d->document && d->document->pages() > 0;
3883 
3884     if (d->aZoomFitWidth) {
3885         d->aZoomFitWidth->setEnabled(hasPages);
3886     }
3887     if (d->aZoomFitPage) {
3888         d->aZoomFitPage->setEnabled(hasPages);
3889     }
3890     if (d->aZoomAutoFit) {
3891         d->aZoomAutoFit->setEnabled(hasPages);
3892     }
3893     if (d->aZoom) {
3894         d->aZoom->selectableActionGroup()->setEnabled(hasPages);
3895         d->aZoom->setEnabled(hasPages);
3896     }
3897     if (d->aZoomIn) {
3898         d->aZoomIn->setEnabled(hasPages && d->zoomFactor < upperZoomLimit - 0.001);
3899     }
3900     if (d->aZoomOut) {
3901         d->aZoomOut->setEnabled(hasPages && d->zoomFactor > (kZoomValues[0] + 0.001));
3902     }
3903     if (d->aZoomActual) {
3904         d->aZoomActual->setEnabled(hasPages && d->zoomFactor != 1.0);
3905     }
3906 }
3907 
updateZoomText()3908 void PageView::updateZoomText()
3909 {
3910     // use current page zoom as zoomFactor if in ZoomFit/* mode
3911     if (d->zoomMode != ZoomFixed && d->items.count() > 0)
3912         d->zoomFactor = d->items[qMax(0, (int)d->document->currentPage())]->zoomFactor();
3913     float newFactor = d->zoomFactor;
3914     d->aZoom->removeAllActions();
3915 
3916     // add items that describe fit actions
3917     QStringList translated;
3918     translated << i18n("Fit Width") << i18n("Fit Page") << i18n("Auto Fit");
3919 
3920     // add percent items
3921     int idx = 0, selIdx = 3;
3922     bool inserted = false; // use: "d->zoomMode != ZoomFixed" to hide Fit/* zoom ratio
3923     int zoomValueCount = 11;
3924     if (d->document->supportsTiles())
3925         zoomValueCount = kZoomValues.size();
3926     while (idx < zoomValueCount || !inserted) {
3927         float value = idx < zoomValueCount ? kZoomValues[idx] : newFactor;
3928         if (!inserted && newFactor < (value - 0.0001))
3929             value = newFactor;
3930         else
3931             idx++;
3932         if (value > (newFactor - 0.0001) && value < (newFactor + 0.0001))
3933             inserted = true;
3934         if (!inserted)
3935             selIdx++;
3936         // we do not need to display 2-digit precision
3937         QString localValue(QLocale().toString(value * 100.0, 'f', 1));
3938         localValue.remove(QLocale().decimalPoint() + QLatin1Char('0'));
3939         // remove a trailing zero in numbers like 66.70
3940         if (localValue.right(1) == QLatin1String("0") && localValue.indexOf(QLocale().decimalPoint()) > -1)
3941             localValue.chop(1);
3942         translated << QStringLiteral("%1%").arg(localValue);
3943     }
3944     d->aZoom->setItems(translated);
3945 
3946     // select current item in list
3947     if (d->zoomMode == ZoomFitWidth)
3948         selIdx = 0;
3949     else if (d->zoomMode == ZoomFitPage)
3950         selIdx = 1;
3951     else if (d->zoomMode == ZoomFitAuto)
3952         selIdx = 2;
3953     // we have to temporarily enable the actions as otherwise we can't set a new current item
3954     d->aZoom->setEnabled(true);
3955     d->aZoom->selectableActionGroup()->setEnabled(true);
3956     d->aZoom->setCurrentItem(selIdx);
3957     d->aZoom->setEnabled(d->items.size() > 0);
3958     d->aZoom->selectableActionGroup()->setEnabled(d->items.size() > 0);
3959 }
3960 
updateViewMode(const int nr)3961 void PageView::updateViewMode(const int nr)
3962 {
3963     const QList<QAction *> actions = d->viewModeActionGroup->actions();
3964     for (QAction *action : actions) {
3965         QVariant mode_id = action->data();
3966         if (mode_id.toInt() == nr) {
3967             action->trigger();
3968         }
3969     }
3970 }
3971 
updateCursor()3972 void PageView::updateCursor()
3973 {
3974     const QPoint p = contentAreaPosition() + viewport()->mapFromGlobal(QCursor::pos());
3975     updateCursor(p);
3976 }
3977 
updateCursor(const QPoint p)3978 void PageView::updateCursor(const QPoint p)
3979 {
3980     // reset mouse over link it will be re-set if that still valid
3981     d->mouseOverLinkObject = nullptr;
3982 
3983     // detect the underlaying page (if present)
3984     PageViewItem *pageItem = pickItemOnPoint(p.x(), p.y());
3985     QScroller::State scrollerState = d->scroller->state();
3986 
3987     if (d->annotator && d->annotator->active()) {
3988         if (pageItem || d->annotator->annotating())
3989             setCursor(d->annotator->cursor());
3990         else
3991             setCursor(Qt::ForbiddenCursor);
3992     } else if (scrollerState == QScroller::Pressed || scrollerState == QScroller::Dragging) {
3993         setCursor(Qt::ClosedHandCursor);
3994     } else if (pageItem) {
3995         double nX = pageItem->absToPageX(p.x());
3996         double nY = pageItem->absToPageY(p.y());
3997         Qt::CursorShape cursorShapeFallback;
3998 
3999         // if over a ObjectRect (of type Link) change cursor to hand
4000         switch (d->mouseMode) {
4001         case Okular::Settings::EnumMouseMode::TextSelect:
4002             if (d->mouseTextSelecting) {
4003                 setCursor(Qt::IBeamCursor);
4004                 return;
4005             }
4006             cursorShapeFallback = Qt::IBeamCursor;
4007             break;
4008         case Okular::Settings::EnumMouseMode::Magnifier:
4009             setCursor(Qt::CrossCursor);
4010             return;
4011         case Okular::Settings::EnumMouseMode::RectSelect:
4012         case Okular::Settings::EnumMouseMode::TrimSelect:
4013             if (d->mouseSelecting) {
4014                 setCursor(Qt::CrossCursor);
4015                 return;
4016             }
4017             cursorShapeFallback = Qt::CrossCursor;
4018             break;
4019         case Okular::Settings::EnumMouseMode::Browse:
4020             d->mouseOnRect = false;
4021             if (d->mouseAnnotation->isMouseOver()) {
4022                 d->mouseOnRect = true;
4023                 setCursor(d->mouseAnnotation->cursor());
4024                 return;
4025             } else {
4026                 cursorShapeFallback = Qt::OpenHandCursor;
4027             }
4028             break;
4029         default:
4030             setCursor(Qt::ArrowCursor);
4031             return;
4032         }
4033 
4034         const Okular::ObjectRect *linkobj = pageItem->page()->objectRect(Okular::ObjectRect::Action, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight());
4035         if (linkobj) {
4036             d->mouseOverLinkObject = linkobj;
4037             d->mouseOnRect = true;
4038             setCursor(Qt::PointingHandCursor);
4039         } else {
4040             setCursor(cursorShapeFallback);
4041         }
4042     } else {
4043         // if there's no page over the cursor and we were showing the pointingHandCursor
4044         // go back to the normal one
4045         d->mouseOnRect = false;
4046         setCursor(Qt::ArrowCursor);
4047     }
4048 }
4049 
reloadForms()4050 void PageView::reloadForms()
4051 {
4052     if (d->m_formsVisible) {
4053         for (PageViewItem *item : qAsConst(d->visibleItems)) {
4054             item->reloadFormWidgetsState();
4055         }
4056     }
4057 }
4058 
moveMagnifier(const QPoint p)4059 void PageView::moveMagnifier(const QPoint p) // non scaled point
4060 {
4061     const int w = d->magnifierView->width() * 0.5;
4062     const int h = d->magnifierView->height() * 0.5;
4063 
4064     int x = p.x() - w;
4065     int y = p.y() - h;
4066 
4067     const int max_x = viewport()->width();
4068     const int max_y = viewport()->height();
4069 
4070     QPoint scroll(0, 0);
4071 
4072     if (x < 0) {
4073         if (horizontalScrollBar()->value() > 0)
4074             scroll.setX(x - w);
4075         x = 0;
4076     }
4077 
4078     if (y < 0) {
4079         if (verticalScrollBar()->value() > 0)
4080             scroll.setY(y - h);
4081         y = 0;
4082     }
4083 
4084     if (p.x() + w > max_x) {
4085         if (horizontalScrollBar()->value() < horizontalScrollBar()->maximum())
4086             scroll.setX(p.x() + 2 * w - max_x);
4087         x = max_x - d->magnifierView->width() - 1;
4088     }
4089 
4090     if (p.y() + h > max_y) {
4091         if (verticalScrollBar()->value() < verticalScrollBar()->maximum())
4092             scroll.setY(p.y() + 2 * h - max_y);
4093         y = max_y - d->magnifierView->height() - 1;
4094     }
4095 
4096     if (!scroll.isNull())
4097         scrollPosIntoView(contentAreaPoint(p + scroll));
4098 
4099     d->magnifierView->move(x, y);
4100 }
4101 
updateMagnifier(const QPoint p)4102 void PageView::updateMagnifier(const QPoint p) // scaled point
4103 {
4104     /* translate mouse coordinates to page coordinates and inform the magnifier of the situation */
4105     PageViewItem *item = pickItemOnPoint(p.x(), p.y());
4106     if (item) {
4107         Okular::NormalizedPoint np(item->absToPageX(p.x()), item->absToPageY(p.y()));
4108         d->magnifierView->updateView(np, item->page());
4109     }
4110 }
4111 
viewColumns() const4112 int PageView::viewColumns() const
4113 {
4114     int vm = Okular::Settings::viewMode();
4115     if (vm == Okular::Settings::EnumViewMode::Single)
4116         return 1;
4117     else if (vm == Okular::Settings::EnumViewMode::Facing || vm == Okular::Settings::EnumViewMode::FacingFirstCentered)
4118         return 2;
4119     else if (vm == Okular::Settings::EnumViewMode::Summary && d->document->pages() < Okular::Settings::viewColumns())
4120         return d->document->pages();
4121     else
4122         return Okular::Settings::viewColumns();
4123 }
4124 
center(int cx,int cy,bool smoothMove)4125 void PageView::center(int cx, int cy, bool smoothMove)
4126 {
4127     scrollTo(cx - viewport()->width() / 2, cy - viewport()->height() / 2, smoothMove);
4128 }
4129 
scrollTo(int x,int y,bool smoothMove)4130 void PageView::scrollTo(int x, int y, bool smoothMove)
4131 {
4132 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
4133     // Workaround for QTBUG-88288, (KDE bug 425188): To avoid a crash in QScroller,
4134     // we need to make sure the target widget intersects a physical screen.
4135     // QScroller queries QDesktopWidget::screenNumber().
4136 
4137     // If we are not on a physical screen, we try to make our widget big enough.
4138     // The geometry will be restored to a sensible value once the Part is shown.
4139 
4140     // It should be enough to add this workaround ony in PageView::scrollTo(),
4141     // because we don’t expect other QScroller::scrollTo() calls before PageView is shown.
4142     if (QApplication::desktop()->screenNumber(this) < 0) {
4143         setGeometry(QRect(-1000, -1000, 5000, 5000).united(QApplication::desktop()->availableGeometry()));
4144     }
4145 #endif
4146 
4147     bool prevState = d->blockPixmapsRequest;
4148 
4149     int newValue = -1;
4150     if (x != horizontalScrollBar()->value() || y != verticalScrollBar()->value())
4151         newValue = 1; // Pretend this call is the result of a scrollbar event
4152 
4153     d->blockPixmapsRequest = true;
4154 
4155     if (smoothMove)
4156         d->scroller->scrollTo(QPoint(x, y), d->currentLongScrollDuration);
4157     else
4158         d->scroller->scrollTo(QPoint(x, y), 0);
4159 
4160     d->blockPixmapsRequest = prevState;
4161 
4162     slotRequestVisiblePixmaps(newValue);
4163 }
4164 
toggleFormWidgets(bool on)4165 void PageView::toggleFormWidgets(bool on)
4166 {
4167     bool somehadfocus = false;
4168     for (PageViewItem *item : qAsConst(d->items)) {
4169         const bool hadfocus = item->setFormWidgetsVisible(on);
4170         somehadfocus = somehadfocus || hadfocus;
4171     }
4172     if (somehadfocus)
4173         setFocus();
4174     d->m_formsVisible = on;
4175 }
4176 
resizeContentArea(const QSize newSize)4177 void PageView::resizeContentArea(const QSize newSize)
4178 {
4179     const QSize vs = viewport()->size();
4180     int hRange = newSize.width() - vs.width();
4181     int vRange = newSize.height() - vs.height();
4182     if (horizontalScrollBar()->isVisible() && hRange == verticalScrollBar()->width() && verticalScrollBar()->isVisible() && vRange == horizontalScrollBar()->height() && Okular::Settings::showScrollBars()) {
4183         hRange = 0;
4184         vRange = 0;
4185     }
4186     horizontalScrollBar()->setRange(0, hRange);
4187     verticalScrollBar()->setRange(0, vRange);
4188     updatePageStep();
4189 }
4190 
updatePageStep()4191 void PageView::updatePageStep()
4192 {
4193     const QSize vs = viewport()->size();
4194     horizontalScrollBar()->setPageStep(vs.width());
4195     verticalScrollBar()->setPageStep(vs.height() * (100 - Okular::Settings::scrollOverlap()) / 100);
4196 }
4197 
addWebShortcutsMenu(QMenu * menu,const QString & text)4198 void PageView::addWebShortcutsMenu(QMenu *menu, const QString &text)
4199 {
4200     if (text.isEmpty()) {
4201         return;
4202     }
4203 
4204     QString searchText = text;
4205     searchText = searchText.replace(QLatin1Char('\n'), QLatin1Char(' ')).replace(QLatin1Char('\r'), QLatin1Char(' ')).simplified();
4206 
4207     if (searchText.isEmpty()) {
4208         return;
4209     }
4210 
4211     KUriFilterData filterData(searchText);
4212 
4213     filterData.setSearchFilteringOptions(KUriFilterData::RetrievePreferredSearchProvidersOnly);
4214 
4215     if (KUriFilter::self()->filterSearchUri(filterData, KUriFilter::NormalTextFilter)) {
4216         const QStringList searchProviders = filterData.preferredSearchProviders();
4217 
4218         if (!searchProviders.isEmpty()) {
4219             QMenu *webShortcutsMenu = new QMenu(menu);
4220             webShortcutsMenu->setIcon(QIcon::fromTheme(QStringLiteral("preferences-web-browser-shortcuts")));
4221 
4222             const QString squeezedText = KStringHandler::rsqueeze(searchText, searchTextPreviewLength);
4223             webShortcutsMenu->setTitle(i18n("Search for '%1' with", squeezedText));
4224 
4225             QAction *action = nullptr;
4226 
4227             for (const QString &searchProvider : searchProviders) {
4228                 action = new QAction(searchProvider, webShortcutsMenu);
4229                 action->setIcon(QIcon::fromTheme(filterData.iconNameForPreferredSearchProvider(searchProvider)));
4230                 action->setData(filterData.queryForPreferredSearchProvider(searchProvider));
4231                 connect(action, &QAction::triggered, this, &PageView::slotHandleWebShortcutAction);
4232                 webShortcutsMenu->addAction(action);
4233             }
4234 
4235             webShortcutsMenu->addSeparator();
4236 
4237             action = new QAction(i18n("Configure Web Shortcuts..."), webShortcutsMenu);
4238             action->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
4239             connect(action, &QAction::triggered, this, &PageView::slotConfigureWebShortcuts);
4240             webShortcutsMenu->addAction(action);
4241 
4242             menu->addMenu(webShortcutsMenu);
4243         }
4244     }
4245 }
4246 
createProcessLinkMenu(PageViewItem * item,const QPoint eventPos)4247 QMenu *PageView::createProcessLinkMenu(PageViewItem *item, const QPoint eventPos)
4248 {
4249     // check if the right-click was over a link
4250     const double nX = item->absToPageX(eventPos.x());
4251     const double nY = item->absToPageY(eventPos.y());
4252     const Okular::ObjectRect *rect = item->page()->objectRect(Okular::ObjectRect::Action, nX, nY, item->uncroppedWidth(), item->uncroppedHeight());
4253     if (rect) {
4254         const Okular::Action *link = static_cast<const Okular::Action *>(rect->object());
4255 
4256         if (!link)
4257             return nullptr;
4258 
4259         QMenu *menu = new QMenu(this);
4260 
4261         // creating the menu and its actions
4262         QAction *processLink = menu->addAction(i18n("Follow This Link"));
4263         processLink->setObjectName(QStringLiteral("ProcessLinkAction"));
4264         if (link->actionType() == Okular::Action::Sound) {
4265             processLink->setText(i18n("Play this Sound"));
4266             if (Okular::AudioPlayer::instance()->state() == Okular::AudioPlayer::PlayingState) {
4267                 QAction *actStopSound = menu->addAction(i18n("Stop Sound"));
4268                 connect(actStopSound, &QAction::triggered, []() { Okular::AudioPlayer::instance()->stopPlaybacks(); });
4269             }
4270         }
4271 
4272         if (dynamic_cast<const Okular::BrowseAction *>(link)) {
4273             QAction *actCopyLinkLocation = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy Link Address"));
4274             actCopyLinkLocation->setObjectName(QStringLiteral("CopyLinkLocationAction"));
4275             connect(actCopyLinkLocation, &QAction::triggered, menu, [link]() {
4276                 const Okular::BrowseAction *browseLink = static_cast<const Okular::BrowseAction *>(link);
4277                 QClipboard *cb = QApplication::clipboard();
4278                 cb->setText(browseLink->url().toDisplayString(), QClipboard::Clipboard);
4279                 if (cb->supportsSelection())
4280                     cb->setText(browseLink->url().toDisplayString(), QClipboard::Selection);
4281             });
4282         }
4283 
4284         connect(processLink, &QAction::triggered, this, [this, link]() { d->document->processAction(link); });
4285         return menu;
4286     }
4287     return nullptr;
4288 }
4289 
addSearchWithinDocumentAction(QMenu * menu,const QString & searchText)4290 void PageView::addSearchWithinDocumentAction(QMenu *menu, const QString &searchText)
4291 {
4292     const QString squeezedText = KStringHandler::rsqueeze(searchText, searchTextPreviewLength);
4293     QAction *action = new QAction(i18n("Search for '%1' in this document", squeezedText), menu);
4294     action->setIcon(QIcon::fromTheme(QStringLiteral("document-preview")));
4295     connect(action, &QAction::triggered, this, [this, searchText] { Q_EMIT triggerSearch(searchText); });
4296     menu->addAction(action);
4297 }
4298 
updateSmoothScrollAnimationSpeed()4299 void PageView::updateSmoothScrollAnimationSpeed()
4300 {
4301     // If it's turned off in Okular's own settings, don't bother to look at the
4302     // global settings
4303     if (!Okular::Settings::smoothScrolling()) {
4304         d->currentShortScrollDuration = 0;
4305         d->currentLongScrollDuration = 0;
4306         return;
4307     }
4308 
4309     // If we are using smooth scrolling, scale the speed of the animated
4310     // transitions according to the global animation speed setting
4311     KConfigGroup kdeglobalsConfig = KConfigGroup(KSharedConfig::openConfig(), QStringLiteral("KDE"));
4312     const qreal globalAnimationScale = qMax(0.0, kdeglobalsConfig.readEntry("AnimationDurationFactor", 1.0));
4313     d->currentShortScrollDuration = d->baseShortScrollDuration * globalAnimationScale;
4314     d->currentLongScrollDuration = d->baseLongScrollDuration * globalAnimationScale;
4315 }
4316 
getContinuousMode() const4317 bool PageView::getContinuousMode() const
4318 {
4319     return d->aViewContinuous ? d->aViewContinuous->isChecked() : Okular::Settings::viewContinuous();
4320 }
4321 
4322 // BEGIN private SLOTS
slotRelayoutPages()4323 void PageView::slotRelayoutPages()
4324 // called by: notifySetup, viewportResizeEvent, slotViewMode, slotContinuousToggled, updateZoom
4325 {
4326     // set an empty container if we have no pages
4327     const int pageCount = d->items.count();
4328     if (pageCount < 1) {
4329         return;
4330     }
4331 
4332     int viewportWidth = viewport()->width(), viewportHeight = viewport()->height(), fullWidth = 0, fullHeight = 0;
4333 
4334     // handle the 'center first page in row' stuff
4335     const bool facing = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount > 1;
4336     const bool facingCentered = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered || (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount == 1);
4337     const bool overrideCentering = facingCentered && pageCount < 3;
4338     const bool centerFirstPage = facingCentered && !overrideCentering;
4339     const bool facingPages = facing || centerFirstPage;
4340     const bool centerLastPage = centerFirstPage && pageCount % 2 == 0;
4341     const bool continuousView = getContinuousMode();
4342     const int nCols = overrideCentering ? 1 : viewColumns();
4343     const bool singlePageViewMode = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Single;
4344 
4345     if (d->aFitWindowToPage)
4346         d->aFitWindowToPage->setEnabled(!continuousView && singlePageViewMode);
4347 
4348     // set all items geometry and resize contents. handle 'continuous' and 'single' modes separately
4349 
4350     PageViewItem *currentItem = d->items[qMax(0, (int)d->document->currentPage())];
4351 
4352     // Here we find out column's width and row's height to compute a table
4353     // so we can place widgets 'centered in virtual cells'.
4354     const int nRows = (int)ceil((float)(centerFirstPage ? (pageCount + nCols - 1) : pageCount) / (float)nCols);
4355 
4356     int *colWidth = new int[nCols], *rowHeight = new int[nRows], cIdx = 0, rIdx = 0;
4357     for (int i = 0; i < nCols; i++)
4358         colWidth[i] = viewportWidth / nCols;
4359     for (int i = 0; i < nRows; i++)
4360         rowHeight[i] = 0;
4361     // handle the 'centering on first row' stuff
4362     if (centerFirstPage)
4363         cIdx += nCols - 1;
4364 
4365     // 1) find the maximum columns width and rows height for a grid in
4366     // which each page must well-fit inside a cell
4367     for (PageViewItem *item : qAsConst(d->items)) {
4368         // update internal page size (leaving a little margin in case of Fit* modes)
4369         updateItemSize(item, colWidth[cIdx] - kcolWidthMargin, viewportHeight - krowHeightMargin);
4370         // find row's maximum height and column's max width
4371         if (item->croppedWidth() + kcolWidthMargin > colWidth[cIdx])
4372             colWidth[cIdx] = item->croppedWidth() + kcolWidthMargin;
4373         if (item->croppedHeight() + krowHeightMargin > rowHeight[rIdx])
4374             rowHeight[rIdx] = item->croppedHeight() + krowHeightMargin;
4375         // handle the 'centering on first row' stuff
4376         // update col/row indices
4377         if (++cIdx == nCols) {
4378             cIdx = 0;
4379             rIdx++;
4380         }
4381     }
4382 
4383     const int pageRowIdx = ((centerFirstPage ? nCols - 1 : 0) + currentItem->pageNumber()) / nCols;
4384 
4385     // 2) compute full size
4386     for (int i = 0; i < nCols; i++)
4387         fullWidth += colWidth[i];
4388     if (continuousView) {
4389         for (int i = 0; i < nRows; i++)
4390             fullHeight += rowHeight[i];
4391     } else
4392         fullHeight = rowHeight[pageRowIdx];
4393 
4394     // 3) arrange widgets inside cells (and refine fullHeight if needed)
4395     int insertX = 0, insertY = fullHeight < viewportHeight ? (viewportHeight - fullHeight) / 2 : 0;
4396     const int origInsertY = insertY;
4397     cIdx = 0;
4398     rIdx = 0;
4399     if (centerFirstPage) {
4400         cIdx += nCols - 1;
4401         for (int i = 0; i < cIdx; ++i)
4402             insertX += colWidth[i];
4403     }
4404     for (PageViewItem *item : qAsConst(d->items)) {
4405         int cWidth = colWidth[cIdx], rHeight = rowHeight[rIdx];
4406         if (continuousView || rIdx == pageRowIdx) {
4407             const bool reallyDoCenterFirst = item->pageNumber() == 0 && centerFirstPage;
4408             const bool reallyDoCenterLast = item->pageNumber() == pageCount - 1 && centerLastPage;
4409             int actualX = 0;
4410             if (reallyDoCenterFirst || reallyDoCenterLast) {
4411                 // page is centered across entire viewport
4412                 actualX = (fullWidth - item->croppedWidth()) / 2;
4413             } else if (facingPages) {
4414                 if (Okular::Settings::rtlReadingDirection()) {
4415                     // RTL reading mode
4416                     actualX = ((centerFirstPage && item->pageNumber() % 2 == 0) || (!centerFirstPage && item->pageNumber() % 2 == 1)) ? (fullWidth / 2) - item->croppedWidth() - 1 : (fullWidth / 2) + 1;
4417                 } else {
4418                     // page edges 'touch' the center of the viewport
4419                     actualX = ((centerFirstPage && item->pageNumber() % 2 == 1) || (!centerFirstPage && item->pageNumber() % 2 == 0)) ? (fullWidth / 2) - item->croppedWidth() - 1 : (fullWidth / 2) + 1;
4420                 }
4421             } else {
4422                 // page is centered within its virtual column
4423                 // actualX = insertX + (cWidth - item->croppedWidth()) / 2;
4424                 if (Okular::Settings::rtlReadingDirection()) {
4425                     actualX = fullWidth - insertX - cWidth + ((cWidth - item->croppedWidth()) / 2);
4426                 } else {
4427                     actualX = insertX + (cWidth - item->croppedWidth()) / 2;
4428                 }
4429             }
4430             item->moveTo(actualX, (continuousView ? insertY : origInsertY) + (rHeight - item->croppedHeight()) / 2);
4431             item->setVisible(true);
4432         } else {
4433             item->moveTo(0, 0);
4434             item->setVisible(false);
4435         }
4436         item->setFormWidgetsVisible(d->m_formsVisible);
4437         // advance col/row index
4438         insertX += cWidth;
4439         if (++cIdx == nCols) {
4440             cIdx = 0;
4441             rIdx++;
4442             insertX = 0;
4443             insertY += rHeight;
4444         }
4445 #ifdef PAGEVIEW_DEBUG
4446         qWarning() << "updating size for pageno" << item->pageNumber() << "cropped" << item->croppedGeometry() << "uncropped" << item->uncroppedGeometry();
4447 #endif
4448     }
4449 
4450     delete[] colWidth;
4451     delete[] rowHeight;
4452 
4453     // 3) reset dirty state
4454     d->dirtyLayout = false;
4455 
4456     // 4) update scrollview's contents size and recenter view
4457     bool wasUpdatesEnabled = viewport()->updatesEnabled();
4458     if (fullWidth != contentAreaWidth() || fullHeight != contentAreaHeight()) {
4459         const Okular::DocumentViewport vp = d->document->viewport();
4460         // disable updates and resize the viewportContents
4461         if (wasUpdatesEnabled)
4462             viewport()->setUpdatesEnabled(false);
4463         resizeContentArea(QSize(fullWidth, fullHeight));
4464         // restore previous viewport if defined and updates enabled
4465         if (wasUpdatesEnabled) {
4466             if (vp.pageNumber >= 0) {
4467                 int prevX = horizontalScrollBar()->value(), prevY = verticalScrollBar()->value();
4468 
4469                 const QPoint centerPos = viewportToContentArea(vp);
4470                 center(centerPos.x(), centerPos.y());
4471 
4472                 // center() usually moves the viewport, that requests pixmaps too.
4473                 // if that doesn't happen we have to request them by hand
4474                 if (prevX == horizontalScrollBar()->value() && prevY == verticalScrollBar()->value())
4475                     slotRequestVisiblePixmaps();
4476             }
4477             // or else go to center page
4478             else
4479                 center(fullWidth / 2, 0);
4480             viewport()->setUpdatesEnabled(true);
4481         }
4482     } else {
4483         slotRequestVisiblePixmaps();
4484     }
4485 
4486     // 5) update the whole viewport if updated enabled
4487     if (wasUpdatesEnabled)
4488         viewport()->update();
4489 }
4490 
delayedResizeEvent()4491 void PageView::delayedResizeEvent()
4492 {
4493     // If we already got here we don't need to execute the timer slot again
4494     d->delayResizeEventTimer->stop();
4495     slotRelayoutPages();
4496     slotRequestVisiblePixmaps();
4497 }
4498 
slotRequestPreloadPixmap(PageView * pageView,const PageViewItem * i,const QRect expandedViewportRect,QLinkedList<Okular::PixmapRequest * > * requestedPixmaps)4499 static void slotRequestPreloadPixmap(PageView *pageView, const PageViewItem *i, const QRect expandedViewportRect, QLinkedList<Okular::PixmapRequest *> *requestedPixmaps)
4500 {
4501     Okular::NormalizedRect preRenderRegion;
4502     const QRect intersectionRect = expandedViewportRect.intersected(i->croppedGeometry());
4503     if (!intersectionRect.isEmpty())
4504         preRenderRegion = Okular::NormalizedRect(intersectionRect.translated(-i->uncroppedGeometry().topLeft()), i->uncroppedWidth(), i->uncroppedHeight());
4505 
4506     // request the pixmap if not already present
4507     if (!i->page()->hasPixmap(pageView, i->uncroppedWidth(), i->uncroppedHeight(), preRenderRegion) && i->uncroppedWidth() > 0) {
4508         Okular::PixmapRequest::PixmapRequestFeatures requestFeatures = Okular::PixmapRequest::Preload;
4509         requestFeatures |= Okular::PixmapRequest::Asynchronous;
4510         const bool pageHasTilesManager = i->page()->hasTilesManager(pageView);
4511         if (pageHasTilesManager && !preRenderRegion.isNull()) {
4512             Okular::PixmapRequest *p = new Okular::PixmapRequest(pageView, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), pageView->devicePixelRatioF(), PAGEVIEW_PRELOAD_PRIO, requestFeatures);
4513             requestedPixmaps->push_back(p);
4514 
4515             p->setNormalizedRect(preRenderRegion);
4516             p->setTile(true);
4517         } else if (!pageHasTilesManager) {
4518             Okular::PixmapRequest *p = new Okular::PixmapRequest(pageView, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), pageView->devicePixelRatioF(), PAGEVIEW_PRELOAD_PRIO, requestFeatures);
4519             requestedPixmaps->push_back(p);
4520             p->setNormalizedRect(preRenderRegion);
4521         }
4522     }
4523 }
4524 
slotRequestVisiblePixmaps(int newValue)4525 void PageView::slotRequestVisiblePixmaps(int newValue)
4526 {
4527     // if requests are blocked (because raised by an unwanted event), exit
4528     if (d->blockPixmapsRequest)
4529         return;
4530 
4531     // precalc view limits for intersecting with page coords inside the loop
4532     const bool isEvent = newValue != -1 && !d->blockViewport;
4533     const QRect viewportRect(horizontalScrollBar()->value(), verticalScrollBar()->value(), viewport()->width(), viewport()->height());
4534     const QRect viewportRectAtZeroZero(0, 0, viewport()->width(), viewport()->height());
4535 
4536     // some variables used to determine the viewport
4537     int nearPageNumber = -1;
4538     const double viewportCenterX = (viewportRect.left() + viewportRect.right()) / 2.0;
4539     const double viewportCenterY = (viewportRect.top() + viewportRect.bottom()) / 2.0;
4540     double focusedX = 0.5, focusedY = 0.0, minDistance = -1.0;
4541     // Margin (in pixels) around the viewport to preload
4542     const int pixelsToExpand = 512;
4543 
4544     // iterate over all items
4545     d->visibleItems.clear();
4546     QLinkedList<Okular::PixmapRequest *> requestedPixmaps;
4547     QVector<Okular::VisiblePageRect *> visibleRects;
4548     for (PageViewItem *i : qAsConst(d->items)) {
4549         const QSet<FormWidgetIface *> formWidgetsList = i->formWidgets();
4550         for (FormWidgetIface *fwi : formWidgetsList) {
4551             Okular::NormalizedRect r = fwi->rect();
4552             fwi->moveTo(qRound(i->uncroppedGeometry().left() + i->uncroppedWidth() * r.left) + 1 - viewportRect.left(), qRound(i->uncroppedGeometry().top() + i->uncroppedHeight() * r.top) + 1 - viewportRect.top());
4553         }
4554         const QHash<Okular::Movie *, VideoWidget *> videoWidgets = i->videoWidgets();
4555         for (VideoWidget *vw : videoWidgets) {
4556             const Okular::NormalizedRect r = vw->normGeometry();
4557             vw->move(qRound(i->uncroppedGeometry().left() + i->uncroppedWidth() * r.left) + 1 - viewportRect.left(), qRound(i->uncroppedGeometry().top() + i->uncroppedHeight() * r.top) + 1 - viewportRect.top());
4558 
4559             if (vw->isPlaying() && viewportRectAtZeroZero.intersected(vw->geometry()).isEmpty()) {
4560                 vw->stop();
4561                 vw->pageLeft();
4562             }
4563         }
4564 
4565         if (!i->isVisible())
4566             continue;
4567 #ifdef PAGEVIEW_DEBUG
4568         qWarning() << "checking page" << i->pageNumber();
4569         qWarning().nospace() << "viewportRect is " << viewportRect << ", page item is " << i->croppedGeometry() << " intersect : " << viewportRect.intersects(i->croppedGeometry());
4570 #endif
4571         // if the item doesn't intersect the viewport, skip it
4572         QRect intersectionRect = viewportRect.intersected(i->croppedGeometry());
4573         if (intersectionRect.isEmpty()) {
4574             continue;
4575         }
4576 
4577         // add the item to the 'visible list'
4578         d->visibleItems.push_back(i);
4579         Okular::VisiblePageRect *vItem = new Okular::VisiblePageRect(i->pageNumber(), Okular::NormalizedRect(intersectionRect.translated(-i->uncroppedGeometry().topLeft()), i->uncroppedWidth(), i->uncroppedHeight()));
4580         visibleRects.push_back(vItem);
4581 #ifdef PAGEVIEW_DEBUG
4582         qWarning() << "checking for pixmap for page" << i->pageNumber() << "=" << i->page()->hasPixmap(this, i->uncroppedWidth(), i->uncroppedHeight());
4583         qWarning() << "checking for text for page" << i->pageNumber() << "=" << i->page()->hasTextPage();
4584 #endif
4585 
4586         Okular::NormalizedRect expandedVisibleRect = vItem->rect;
4587         if (i->page()->hasTilesManager(this) && Okular::Settings::memoryLevel() != Okular::Settings::EnumMemoryLevel::Low) {
4588             double rectMargin = pixelsToExpand / (double)i->uncroppedHeight();
4589             expandedVisibleRect.left = qMax(0.0, vItem->rect.left - rectMargin);
4590             expandedVisibleRect.top = qMax(0.0, vItem->rect.top - rectMargin);
4591             expandedVisibleRect.right = qMin(1.0, vItem->rect.right + rectMargin);
4592             expandedVisibleRect.bottom = qMin(1.0, vItem->rect.bottom + rectMargin);
4593         }
4594 
4595         // if the item has not the right pixmap, add a request for it
4596         if (!i->page()->hasPixmap(this, i->uncroppedWidth(), i->uncroppedHeight(), expandedVisibleRect)) {
4597 #ifdef PAGEVIEW_DEBUG
4598             qWarning() << "rerequesting visible pixmaps for page" << i->pageNumber() << "!";
4599 #endif
4600             Okular::PixmapRequest *p = new Okular::PixmapRequest(this, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), devicePixelRatioF(), PAGEVIEW_PRIO, Okular::PixmapRequest::Asynchronous);
4601             requestedPixmaps.push_back(p);
4602 
4603             if (i->page()->hasTilesManager(this)) {
4604                 p->setNormalizedRect(expandedVisibleRect);
4605                 p->setTile(true);
4606             } else
4607                 p->setNormalizedRect(vItem->rect);
4608         }
4609 
4610         // look for the item closest to viewport center and the relative
4611         // position between the item and the viewport center
4612         if (isEvent) {
4613             const QRect &geometry = i->croppedGeometry();
4614             // compute distance between item center and viewport center (slightly moved left)
4615             const double distance = hypot((geometry.left() + geometry.right()) / 2.0 - (viewportCenterX - 4), (geometry.top() + geometry.bottom()) / 2.0 - viewportCenterY);
4616             if (distance >= minDistance && nearPageNumber != -1)
4617                 continue;
4618             nearPageNumber = i->pageNumber();
4619             minDistance = distance;
4620             if (geometry.height() > 0 && geometry.width() > 0) {
4621                 // Compute normalized coordinates w.r.t. cropped page
4622                 focusedX = (viewportCenterX - (double)geometry.left()) / (double)geometry.width();
4623                 focusedY = (viewportCenterY - (double)geometry.top()) / (double)geometry.height();
4624                 // Convert to normalized coordinates w.r.t. full page (no-op if not cropped)
4625                 focusedX = i->crop().left + focusedX * i->crop().width();
4626                 focusedY = i->crop().top + focusedY * i->crop().height();
4627             }
4628         }
4629     }
4630 
4631     // if preloading is enabled, add the pages before and after in preloading
4632     if (!d->visibleItems.isEmpty() && Okular::SettingsCore::memoryLevel() != Okular::SettingsCore::EnumMemoryLevel::Low) {
4633         // as the requests are done in the order as they appear in the list,
4634         // request first the next page and then the previous
4635 
4636         int pagesToPreload = viewColumns();
4637 
4638         // if the greedy option is set, preload all pages
4639         if (Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Greedy)
4640             pagesToPreload = d->items.count();
4641 
4642         const QRect expandedViewportRect = viewportRect.adjusted(0, -pixelsToExpand, 0, pixelsToExpand);
4643 
4644         for (int j = 1; j <= pagesToPreload; j++) {
4645             // add the page after the 'visible series' in preload
4646             const int tailRequest = d->visibleItems.last()->pageNumber() + j;
4647             if (tailRequest < (int)d->items.count()) {
4648                 slotRequestPreloadPixmap(this, d->items[tailRequest], expandedViewportRect, &requestedPixmaps);
4649             }
4650 
4651             // add the page before the 'visible series' in preload
4652             const int headRequest = d->visibleItems.first()->pageNumber() - j;
4653             if (headRequest >= 0) {
4654                 slotRequestPreloadPixmap(this, d->items[headRequest], expandedViewportRect, &requestedPixmaps);
4655             }
4656 
4657             // stop if we've already reached both ends of the document
4658             if (headRequest < 0 && tailRequest >= (int)d->items.count())
4659                 break;
4660         }
4661     }
4662 
4663     // send requests to the document
4664     if (!requestedPixmaps.isEmpty()) {
4665         d->document->requestPixmaps(requestedPixmaps);
4666     }
4667     // if this functions was invoked by viewport events, send update to document
4668     if (isEvent && nearPageNumber != -1) {
4669         // determine the document viewport
4670         Okular::DocumentViewport newViewport(nearPageNumber);
4671         newViewport.rePos.enabled = true;
4672         newViewport.rePos.normalizedX = focusedX;
4673         newViewport.rePos.normalizedY = focusedY;
4674         // set the viewport to other observers
4675         // do not update history if the viewport is autoscrolling
4676         d->document->setViewportWithHistory(newViewport, this, false, d->scroller->state() != QScroller::Scrolling);
4677     }
4678     d->document->setVisiblePageRects(visibleRects, this);
4679 }
4680 
slotAutoScroll()4681 void PageView::slotAutoScroll()
4682 {
4683     // the first time create the timer
4684     if (!d->autoScrollTimer) {
4685         d->autoScrollTimer = new QTimer(this);
4686         d->autoScrollTimer->setSingleShot(true);
4687         connect(d->autoScrollTimer, &QTimer::timeout, this, &PageView::slotAutoScroll);
4688     }
4689 
4690     // if scrollIncrement is zero, stop the timer
4691     if (!d->scrollIncrement) {
4692         d->autoScrollTimer->stop();
4693         return;
4694     }
4695 
4696     // compute delay between timer ticks and scroll amount per tick
4697     int index = abs(d->scrollIncrement) - 1; // 0..9
4698     const int scrollDelay[10] = {200, 100, 50, 30, 20, 30, 25, 20, 30, 20};
4699     const int scrollOffset[10] = {1, 1, 1, 1, 1, 2, 2, 2, 4, 4};
4700     d->autoScrollTimer->start(scrollDelay[index]);
4701     int delta = d->scrollIncrement > 0 ? scrollOffset[index] : -scrollOffset[index];
4702     d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, delta), scrollDelay[index]);
4703 }
4704 
slotDragScroll()4705 void PageView::slotDragScroll()
4706 {
4707     scrollTo(horizontalScrollBar()->value() + d->dragScrollVector.x(), verticalScrollBar()->value() + d->dragScrollVector.y());
4708     QPoint p = contentAreaPosition() + viewport()->mapFromGlobal(QCursor::pos());
4709     updateSelection(p);
4710 }
4711 
slotShowWelcome()4712 void PageView::slotShowWelcome()
4713 {
4714     // show initial welcome text
4715     d->messageWindow->display(i18n("Welcome"), QString(), PageViewMessage::Info, 2000);
4716 }
4717 
slotShowSizeAllCursor()4718 void PageView::slotShowSizeAllCursor()
4719 {
4720     setCursor(Qt::SizeAllCursor);
4721 }
4722 
slotHandleWebShortcutAction()4723 void PageView::slotHandleWebShortcutAction()
4724 {
4725     QAction *action = qobject_cast<QAction *>(sender());
4726 
4727     if (action) {
4728         KUriFilterData filterData(action->data().toString());
4729 
4730         if (KUriFilter::self()->filterSearchUri(filterData, KUriFilter::WebShortcutFilter)) {
4731             QDesktopServices::openUrl(filterData.uri());
4732         }
4733     }
4734 }
4735 
slotConfigureWebShortcuts()4736 void PageView::slotConfigureWebShortcuts()
4737 {
4738     KToolInvocation::kdeinitExec(QStringLiteral("kcmshell5"), QStringList() << QStringLiteral("webshortcuts"));
4739 }
4740 
slotZoom()4741 void PageView::slotZoom()
4742 {
4743     if (!d->aZoom->selectableActionGroup()->isEnabled())
4744         return;
4745 
4746     setFocus();
4747     updateZoom(ZoomFixed);
4748 }
4749 
slotZoomIn()4750 void PageView::slotZoomIn()
4751 {
4752     updateZoom(ZoomIn);
4753 }
4754 
slotZoomOut()4755 void PageView::slotZoomOut()
4756 {
4757     updateZoom(ZoomOut);
4758 }
4759 
slotZoomActual()4760 void PageView::slotZoomActual()
4761 {
4762     updateZoom(ZoomActual);
4763 }
4764 
slotFitToWidthToggled(bool on)4765 void PageView::slotFitToWidthToggled(bool on)
4766 {
4767     if (on)
4768         updateZoom(ZoomFitWidth);
4769 }
4770 
slotFitToPageToggled(bool on)4771 void PageView::slotFitToPageToggled(bool on)
4772 {
4773     if (on)
4774         updateZoom(ZoomFitPage);
4775 }
4776 
slotAutoFitToggled(bool on)4777 void PageView::slotAutoFitToggled(bool on)
4778 {
4779     if (on)
4780         updateZoom(ZoomFitAuto);
4781 }
4782 
slotViewMode(QAction * action)4783 void PageView::slotViewMode(QAction *action)
4784 {
4785     const int nr = action->data().toInt();
4786     if ((int)Okular::Settings::viewMode() != nr) {
4787         Okular::Settings::setViewMode(nr);
4788         Okular::Settings::self()->save();
4789         if (d->document->pages() > 0)
4790             slotRelayoutPages();
4791     }
4792 }
4793 
slotContinuousToggled()4794 void PageView::slotContinuousToggled()
4795 {
4796     if (d->document->pages() > 0)
4797         slotRelayoutPages();
4798 }
4799 
slotReadingDirectionToggled(bool leftToRight)4800 void PageView::slotReadingDirectionToggled(bool leftToRight)
4801 {
4802     Okular::Settings::setRtlReadingDirection(leftToRight);
4803     Okular::Settings::self()->save();
4804 }
4805 
slotUpdateReadingDirectionAction()4806 void PageView::slotUpdateReadingDirectionAction()
4807 {
4808     d->aReadingDirection->setChecked(Okular::Settings::rtlReadingDirection());
4809 }
4810 
slotSetMouseNormal()4811 void PageView::slotSetMouseNormal()
4812 {
4813     d->mouseMode = Okular::Settings::EnumMouseMode::Browse;
4814     Okular::Settings::setMouseMode(d->mouseMode);
4815     // hide the messageWindow
4816     d->messageWindow->hide();
4817     // force an update of the cursor
4818     updateCursor();
4819     Okular::Settings::self()->save();
4820     d->annotator->detachAnnotation();
4821 }
4822 
slotSetMouseZoom()4823 void PageView::slotSetMouseZoom()
4824 {
4825     d->mouseMode = Okular::Settings::EnumMouseMode::Zoom;
4826     Okular::Settings::setMouseMode(d->mouseMode);
4827     // change the text in messageWindow (and show it if hidden)
4828     d->messageWindow->display(i18n("Select zooming area. Right-click to zoom out."), QString(), PageViewMessage::Info, -1);
4829     // force an update of the cursor
4830     updateCursor();
4831     Okular::Settings::self()->save();
4832     d->annotator->detachAnnotation();
4833 }
4834 
slotSetMouseMagnifier()4835 void PageView::slotSetMouseMagnifier()
4836 {
4837     d->mouseMode = Okular::Settings::EnumMouseMode::Magnifier;
4838     Okular::Settings::setMouseMode(d->mouseMode);
4839     d->messageWindow->display(i18n("Click to see the magnified view."), QString());
4840 
4841     // force an update of the cursor
4842     updateCursor();
4843     Okular::Settings::self()->save();
4844     d->annotator->detachAnnotation();
4845 }
4846 
slotSetMouseSelect()4847 void PageView::slotSetMouseSelect()
4848 {
4849     d->mouseMode = Okular::Settings::EnumMouseMode::RectSelect;
4850     Okular::Settings::setMouseMode(d->mouseMode);
4851     // change the text in messageWindow (and show it if hidden)
4852     d->messageWindow->display(i18n("Draw a rectangle around the text/graphics to copy."), QString(), PageViewMessage::Info, -1);
4853     // force an update of the cursor
4854     updateCursor();
4855     Okular::Settings::self()->save();
4856     d->annotator->detachAnnotation();
4857 }
4858 
slotSetMouseTextSelect()4859 void PageView::slotSetMouseTextSelect()
4860 {
4861     d->mouseMode = Okular::Settings::EnumMouseMode::TextSelect;
4862     Okular::Settings::setMouseMode(d->mouseMode);
4863     // change the text in messageWindow (and show it if hidden)
4864     d->messageWindow->display(i18n("Select text"), QString(), PageViewMessage::Info, -1);
4865     // force an update of the cursor
4866     updateCursor();
4867     Okular::Settings::self()->save();
4868     d->annotator->detachAnnotation();
4869 }
4870 
slotSetMouseTableSelect()4871 void PageView::slotSetMouseTableSelect()
4872 {
4873     d->mouseMode = Okular::Settings::EnumMouseMode::TableSelect;
4874     Okular::Settings::setMouseMode(d->mouseMode);
4875     // change the text in messageWindow (and show it if hidden)
4876     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);
4877     // force an update of the cursor
4878     updateCursor();
4879     Okular::Settings::self()->save();
4880     d->annotator->detachAnnotation();
4881 }
4882 
slotSignature()4883 void PageView::slotSignature()
4884 {
4885     if (!d->document->isHistoryClean()) {
4886         KMessageBox::information(this, i18n("You have unsaved changes. Please save the document before signing it."));
4887         return;
4888     }
4889 
4890     d->messageWindow->display(i18n("Draw a rectangle to insert the signature field"), QString(), PageViewMessage::Info, -1);
4891 
4892     d->annotator->setSignatureMode(true);
4893 
4894     // force an update of the cursor
4895     updateCursor();
4896     Okular::Settings::self()->save();
4897 }
4898 
slotAutoScrollUp()4899 void PageView::slotAutoScrollUp()
4900 {
4901     if (d->scrollIncrement < -9)
4902         return;
4903     d->scrollIncrement--;
4904     slotAutoScroll();
4905     setFocus();
4906 }
4907 
slotAutoScrollDown()4908 void PageView::slotAutoScrollDown()
4909 {
4910     if (d->scrollIncrement > 9)
4911         return;
4912     d->scrollIncrement++;
4913     slotAutoScroll();
4914     setFocus();
4915 }
4916 
slotScrollUp(int nSteps)4917 void PageView::slotScrollUp(int nSteps)
4918 {
4919     if (verticalScrollBar()->value() > verticalScrollBar()->minimum()) {
4920         if (nSteps) {
4921             d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, -100 * nSteps), d->currentShortScrollDuration);
4922         } else {
4923             if (d->scroller->finalPosition().y() > verticalScrollBar()->minimum())
4924                 d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, -(1 - Okular::Settings::scrollOverlap() / 100.0) * viewport()->height()), d->currentLongScrollDuration);
4925         }
4926     } else if (!getContinuousMode() && d->document->currentPage() > 0) {
4927         // Since we are in single page mode and at the top of the page, go to previous page.
4928         // setViewport() is more optimized than document->setPrevPage and then move view to bottom.
4929         Okular::DocumentViewport newViewport = d->document->viewport();
4930         newViewport.pageNumber -= viewColumns();
4931         if (newViewport.pageNumber < 0)
4932             newViewport.pageNumber = 0;
4933         newViewport.rePos.enabled = true;
4934         newViewport.rePos.normalizedY = 1.0;
4935         d->document->setViewport(newViewport);
4936     }
4937 }
4938 
slotScrollDown(int nSteps)4939 void PageView::slotScrollDown(int nSteps)
4940 {
4941     if (verticalScrollBar()->value() < verticalScrollBar()->maximum()) {
4942         if (nSteps) {
4943             d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, 100 * nSteps), d->currentShortScrollDuration);
4944         } else {
4945             if (d->scroller->finalPosition().y() < verticalScrollBar()->maximum())
4946                 d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, (1 - Okular::Settings::scrollOverlap() / 100.0) * viewport()->height()), d->currentLongScrollDuration);
4947         }
4948     } else if (!getContinuousMode() && (int)d->document->currentPage() < d->items.count() - 1) {
4949         // Since we are in single page mode and at the bottom of the page, go to next page.
4950         // setViewport() is more optimized than document->setNextPage and then move view to top
4951         Okular::DocumentViewport newViewport = d->document->viewport();
4952         newViewport.pageNumber += viewColumns();
4953         if (newViewport.pageNumber >= (int)d->items.count())
4954             newViewport.pageNumber = d->items.count() - 1;
4955         newViewport.rePos.enabled = true;
4956         newViewport.rePos.normalizedY = 0.0;
4957         d->document->setViewport(newViewport);
4958     }
4959 }
4960 
slotRotateClockwise()4961 void PageView::slotRotateClockwise()
4962 {
4963     int id = ((int)d->document->rotation() + 1) % 4;
4964     d->document->setRotation(id);
4965 }
4966 
slotRotateCounterClockwise()4967 void PageView::slotRotateCounterClockwise()
4968 {
4969     int id = ((int)d->document->rotation() + 3) % 4;
4970     d->document->setRotation(id);
4971 }
4972 
slotRotateOriginal()4973 void PageView::slotRotateOriginal()
4974 {
4975     d->document->setRotation(0);
4976 }
4977 
4978 // Enforce mutual-exclusion between trim modes
4979 // Each mode is uniquely identified by a single value
4980 // From Okular::Settings::EnumTrimMode
updateTrimMode(int except_id)4981 void PageView::updateTrimMode(int except_id)
4982 {
4983     const QList<QAction *> trimModeActions = d->aTrimMode->menu()->actions();
4984     for (QAction *trimModeAction : trimModeActions) {
4985         if (trimModeAction->data().toInt() != except_id)
4986             trimModeAction->setChecked(false);
4987     }
4988 }
4989 
mouseReleaseOverLink(const Okular::ObjectRect * rect) const4990 bool PageView::mouseReleaseOverLink(const Okular::ObjectRect *rect) const
4991 {
4992     if (rect) {
4993         // handle click over a link
4994         const Okular::Action *action = static_cast<const Okular::Action *>(rect->object());
4995         d->document->processAction(action);
4996         return true;
4997     }
4998     return false;
4999 }
5000 
slotTrimMarginsToggled(bool on)5001 void PageView::slotTrimMarginsToggled(bool on)
5002 {
5003     if (on) { // Turn off any other Trim modes
5004         updateTrimMode(d->aTrimMargins->data().toInt());
5005     }
5006 
5007     if (Okular::Settings::trimMargins() != on) {
5008         Okular::Settings::setTrimMargins(on);
5009         Okular::Settings::self()->save();
5010         if (d->document->pages() > 0) {
5011             slotRelayoutPages();
5012             slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already!
5013         }
5014     }
5015 }
5016 
slotTrimToSelectionToggled(bool on)5017 void PageView::slotTrimToSelectionToggled(bool on)
5018 {
5019     if (on) { // Turn off any other Trim modes
5020         updateTrimMode(d->aTrimToSelection->data().toInt());
5021 
5022         // Change the mouse mode
5023         d->mouseMode = Okular::Settings::EnumMouseMode::TrimSelect;
5024         d->aMouseNormal->setChecked(false);
5025 
5026         // change the text in messageWindow (and show it if hidden)
5027         d->messageWindow->display(i18n("Draw a rectangle around the page area you wish to keep visible"), QString(), PageViewMessage::Info, -1);
5028         // force an update of the cursor
5029         updateCursor();
5030     } else {
5031         // toggled off while making selection
5032         if (Okular::Settings::EnumMouseMode::TrimSelect == d->mouseMode) {
5033             // clear widget selection and invalidate rect
5034             selectionClear();
5035 
5036             // When Trim selection bbox interaction is over, we should switch to another mousemode.
5037             if (d->aPrevAction) {
5038                 d->aPrevAction->trigger();
5039                 d->aPrevAction = nullptr;
5040             } else {
5041                 d->aMouseNormal->trigger();
5042             }
5043         }
5044 
5045         d->trimBoundingBox = Okular::NormalizedRect(); // invalidate box
5046         if (d->document->pages() > 0) {
5047             slotRelayoutPages();
5048             slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already!
5049         }
5050     }
5051 }
5052 
slotToggleForms()5053 void PageView::slotToggleForms()
5054 {
5055     toggleFormWidgets(!d->m_formsVisible);
5056 }
5057 
slotFormChanged(int pageNumber)5058 void PageView::slotFormChanged(int pageNumber)
5059 {
5060     if (!d->refreshTimer) {
5061         d->refreshTimer = new QTimer(this);
5062         d->refreshTimer->setSingleShot(true);
5063         connect(d->refreshTimer, &QTimer::timeout, this, &PageView::slotRefreshPage);
5064     }
5065     d->refreshPages << pageNumber;
5066     int delay = 0;
5067     if (d->m_formsVisible) {
5068         delay = 1000;
5069     }
5070     d->refreshTimer->start(delay);
5071 }
5072 
slotRefreshPage()5073 void PageView::slotRefreshPage()
5074 {
5075     for (int req : qAsConst(d->refreshPages)) {
5076         QTimer::singleShot(0, this, [this, req] { d->document->refreshPixmaps(req); });
5077     }
5078     d->refreshPages.clear();
5079 }
5080 
5081 #ifdef HAVE_SPEECH
slotSpeakDocument()5082 void PageView::slotSpeakDocument()
5083 {
5084     QString text;
5085     for (const PageViewItem *item : qAsConst(d->items)) {
5086         Okular::RegularAreaRect *area = textSelectionForItem(item);
5087         text.append(item->page()->text(area));
5088         text.append('\n');
5089         delete area;
5090     }
5091 
5092     d->tts()->say(text);
5093 }
5094 
slotSpeakCurrentPage()5095 void PageView::slotSpeakCurrentPage()
5096 {
5097     const int currentPage = d->document->viewport().pageNumber;
5098 
5099     PageViewItem *item = d->items.at(currentPage);
5100     Okular::RegularAreaRect *area = textSelectionForItem(item);
5101     const QString text = item->page()->text(area);
5102     delete area;
5103 
5104     d->tts()->say(text);
5105 }
5106 
slotStopSpeaks()5107 void PageView::slotStopSpeaks()
5108 {
5109     if (!d->m_tts)
5110         return;
5111 
5112     d->m_tts->stopAllSpeechs();
5113 }
5114 
slotPauseResumeSpeech()5115 void PageView::slotPauseResumeSpeech()
5116 {
5117     if (!d->m_tts)
5118         return;
5119 
5120     d->m_tts->pauseResumeSpeech();
5121 }
5122 
5123 #endif
5124 
slotAction(Okular::Action * action)5125 void PageView::slotAction(Okular::Action *action)
5126 {
5127     d->document->processAction(action);
5128 }
5129 
externalKeyPressEvent(QKeyEvent * e)5130 void PageView::externalKeyPressEvent(QKeyEvent *e)
5131 {
5132     keyPressEvent(e);
5133 }
5134 
slotProcessMovieAction(const Okular::MovieAction * action)5135 void PageView::slotProcessMovieAction(const Okular::MovieAction *action)
5136 {
5137     const Okular::MovieAnnotation *movieAnnotation = action->annotation();
5138     if (!movieAnnotation)
5139         return;
5140 
5141     Okular::Movie *movie = movieAnnotation->movie();
5142     if (!movie)
5143         return;
5144 
5145     const int currentPage = d->document->viewport().pageNumber;
5146 
5147     PageViewItem *item = d->items.at(currentPage);
5148     if (!item)
5149         return;
5150 
5151     VideoWidget *vw = item->videoWidgets().value(movie);
5152     if (!vw)
5153         return;
5154 
5155     vw->show();
5156 
5157     switch (action->operation()) {
5158     case Okular::MovieAction::Play:
5159         vw->stop();
5160         vw->play();
5161         break;
5162     case Okular::MovieAction::Stop:
5163         vw->stop();
5164         break;
5165     case Okular::MovieAction::Pause:
5166         vw->pause();
5167         break;
5168     case Okular::MovieAction::Resume:
5169         vw->play();
5170         break;
5171     };
5172 }
5173 
slotProcessRenditionAction(const Okular::RenditionAction * action)5174 void PageView::slotProcessRenditionAction(const Okular::RenditionAction *action)
5175 {
5176     Okular::Movie *movie = action->movie();
5177     if (!movie)
5178         return;
5179 
5180     const int currentPage = d->document->viewport().pageNumber;
5181 
5182     PageViewItem *item = d->items.at(currentPage);
5183     if (!item)
5184         return;
5185 
5186     VideoWidget *vw = item->videoWidgets().value(movie);
5187     if (!vw)
5188         return;
5189 
5190     if (action->operation() == Okular::RenditionAction::None)
5191         return;
5192 
5193     vw->show();
5194 
5195     switch (action->operation()) {
5196     case Okular::RenditionAction::Play:
5197         vw->stop();
5198         vw->play();
5199         break;
5200     case Okular::RenditionAction::Stop:
5201         vw->stop();
5202         break;
5203     case Okular::RenditionAction::Pause:
5204         vw->pause();
5205         break;
5206     case Okular::RenditionAction::Resume:
5207         vw->play();
5208         break;
5209     default:
5210         return;
5211     };
5212 }
5213 
slotFitWindowToPage()5214 void PageView::slotFitWindowToPage()
5215 {
5216     const PageViewItem *currentPageItem = nullptr;
5217     QSize viewportSize = viewport()->size();
5218     for (const PageViewItem *pageItem : qAsConst(d->items)) {
5219         if (pageItem->isVisible()) {
5220             currentPageItem = pageItem;
5221             break;
5222         }
5223     }
5224 
5225     if (!currentPageItem)
5226         return;
5227 
5228     const QSize pageSize = QSize(currentPageItem->uncroppedWidth() + kcolWidthMargin, currentPageItem->uncroppedHeight() + krowHeightMargin);
5229     if (verticalScrollBar()->isVisible())
5230         viewportSize.setWidth(viewportSize.width() + verticalScrollBar()->width());
5231     if (horizontalScrollBar()->isVisible())
5232         viewportSize.setHeight(viewportSize.height() + horizontalScrollBar()->height());
5233     emit fitWindowToPage(viewportSize, pageSize);
5234 }
5235 
slotSelectPage()5236 void PageView::slotSelectPage()
5237 {
5238     textSelectionClear();
5239     const int currentPage = d->document->viewport().pageNumber;
5240     PageViewItem *item = d->items.at(currentPage);
5241 
5242     if (item) {
5243         Okular::RegularAreaRect *area = textSelectionForItem(item);
5244         d->pagesWithTextSelection.insert(currentPage);
5245         d->document->setPageTextSelection(currentPage, area, palette().color(QPalette::Active, QPalette::Highlight));
5246     }
5247 }
5248 
highlightSignatureFormWidget(const Okular::FormFieldSignature * form)5249 void PageView::highlightSignatureFormWidget(const Okular::FormFieldSignature *form)
5250 {
5251     QVector<PageViewItem *>::const_iterator dIt = d->items.constBegin(), dEnd = d->items.constEnd();
5252     for (; dIt != dEnd; ++dIt) {
5253         const QSet<FormWidgetIface *> fwi = (*dIt)->formWidgets();
5254         for (FormWidgetIface *fw : fwi) {
5255             if (fw->formField() == form) {
5256                 SignatureEdit *widget = static_cast<SignatureEdit *>(fw);
5257                 widget->setDummyMode(true);
5258                 QTimer::singleShot(250, this, [=] { widget->setDummyMode(false); });
5259                 return;
5260             }
5261         }
5262     }
5263 }
5264 
5265 // END private SLOTS
5266 
5267 /* kate: replace-tabs on; indent-width 4; */
5268