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("&")).replace(QLatin1Char('<'), QLatin1String("<")).replace(QLatin1Char('>'), QLatin1String(">"));
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(©Pix);
2801 copyPainter.translate(-selectionRect.left(), -selectionRect.top());
2802 drawDocumentOnPainter(selectionRect, ©Painter);
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