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