File indexing completed on 2024-04-28 15:51:49
0001 /* 0002 SPDX-FileCopyrightText: 2004-2005 Enrico Ros <eros.kde@email.it> 0003 SPDX-FileCopyrightText: 2004-2006 Albert Astals Cid <aacid@kde.org> 0004 0005 Work sponsored by the LiMux project of the city of Munich: 0006 SPDX-FileCopyrightText: 2017 Klarälvdalens Datakonsult AB a KDAB Group company <info@kdab.com> 0007 0008 With portions of code from kpdf/kpdf_pagewidget.cc by: 0009 SPDX-FileCopyrightText: 2002 Wilco Greven <greven@kde.org> 0010 SPDX-FileCopyrightText: 2003 Christophe Devriese <Christophe.Devriese@student.kuleuven.ac.be> 0011 SPDX-FileCopyrightText: 2003 Laurent Montel <montel@kde.org> 0012 SPDX-FileCopyrightText: 2003 Dirk Mueller <mueller@kde.org> 0013 SPDX-FileCopyrightText: 2004 James Ots <kde@jamesots.com> 0014 SPDX-FileCopyrightText: 2011 Jiri Baum - NICTA <jiri@baum.com.au> 0015 0016 SPDX-License-Identifier: GPL-2.0-or-later 0017 */ 0018 0019 #include "pageview.h" 0020 0021 // qt/kde includes 0022 #include <QActionGroup> 0023 #include <QApplication> 0024 #include <QClipboard> 0025 #include <QCursor> 0026 #include <QDesktopServices> 0027 #include <QElapsedTimer> 0028 #include <QEvent> 0029 #include <QGestureEvent> 0030 #include <QImage> 0031 #include <QInputDialog> 0032 #include <QLoggingCategory> 0033 #include <QMenu> 0034 #include <QMimeData> 0035 #include <QMimeDatabase> 0036 #include <QPainter> 0037 #include <QScrollBar> 0038 #include <QScroller> 0039 #include <QScrollerProperties> 0040 #include <QSet> 0041 #include <QTimer> 0042 #include <QToolTip> 0043 0044 #include <KActionCollection> 0045 #include <KActionMenu> 0046 #include <KConfigWatcher> 0047 #include <KIO/CommandLauncherJob> 0048 #include <KIO/JobUiDelegate> 0049 #include <KIO/JobUiDelegateFactory> 0050 #include <KIO/OpenUrlJob> 0051 #include <KLocalizedString> 0052 #include <KMessageBox> 0053 #include <KSelectAction> 0054 #include <KStandardAction> 0055 #include <KStringHandler> 0056 #include <KToggleAction> 0057 #include <KUriFilter> 0058 #include <QAction> 0059 #include <QDebug> 0060 #include <QIcon> 0061 #include <kio_version.h> 0062 #include <kwidgetsaddons_version.h> 0063 0064 // system includes 0065 #include <array> 0066 #include <math.h> 0067 #include <stdlib.h> 0068 0069 // local includes 0070 #include "annotationpopup.h" 0071 #include "annotwindow.h" 0072 #include "colormodemenu.h" 0073 #include "core/annotations.h" 0074 #include "cursorwraphelper.h" 0075 #include "formwidgets.h" 0076 #include "gui/debug_ui.h" 0077 #include "gui/guiutils.h" 0078 #include "gui/pagepainter.h" 0079 #include "gui/priorities.h" 0080 #include "okmenutitle.h" 0081 #include "pageviewannotator.h" 0082 #include "pageviewmouseannotation.h" 0083 #include "pageviewutils.h" 0084 #include "toggleactionmenu.h" 0085 #if HAVE_SPEECH 0086 #include "tts.h" 0087 #endif 0088 #include "core/action.h" 0089 #include "core/audioplayer.h" 0090 #include "core/document_p.h" 0091 #include "core/form.h" 0092 #include "core/generator.h" 0093 #include "core/misc.h" 0094 #include "core/movie.h" 0095 #include "core/page.h" 0096 #include "core/page_p.h" 0097 #include "core/sourcereference.h" 0098 #include "core/tile.h" 0099 #include "magnifierview.h" 0100 #include "settings.h" 0101 #include "settings_core.h" 0102 #include "url_utils.h" 0103 #include "videowidget.h" 0104 0105 static const int pageflags = PagePainter::Accessibility | PagePainter::EnhanceLinks | PagePainter::EnhanceImages | PagePainter::Highlights | PagePainter::TextSelection | PagePainter::Annotations; 0106 0107 static const std::array<float, 16> kZoomValues {0.12, 0.25, 0.33, 0.50, 0.66, 0.75, 1.00, 1.25, 1.50, 2.00, 4.00, 8.00, 16.00, 25.00, 50.00, 100.00}; 0108 0109 // This is the length of the text that will be shown when the user is searching for a specific piece of text. 0110 static const int searchTextPreviewLength = 21; 0111 0112 // When following a link, only a preview of this length will be used to set the text of the action. 0113 static const int linkTextPreviewLength = 30; 0114 0115 static inline double normClamp(double value, double def) 0116 { 0117 return (value < 0.0 || value > 1.0) ? def : value; 0118 } 0119 0120 struct TableSelectionPart { 0121 PageViewItem *item; 0122 Okular::NormalizedRect rectInItem; 0123 Okular::NormalizedRect rectInSelection; 0124 0125 TableSelectionPart(PageViewItem *item_p, const Okular::NormalizedRect &rectInItem_p, const Okular::NormalizedRect &rectInSelection_p); 0126 }; 0127 0128 TableSelectionPart::TableSelectionPart(PageViewItem *item_p, const Okular::NormalizedRect &rectInItem_p, const Okular::NormalizedRect &rectInSelection_p) 0129 : item(item_p) 0130 , rectInItem(rectInItem_p) 0131 , rectInSelection(rectInSelection_p) 0132 { 0133 } 0134 0135 // structure used internally by PageView for data storage 0136 class PageViewPrivate 0137 { 0138 public: 0139 explicit PageViewPrivate(PageView *qq); 0140 0141 FormWidgetsController *formWidgetsController(); 0142 #if HAVE_SPEECH 0143 OkularTTS *tts(); 0144 #endif 0145 QString selectedText() const; 0146 0147 // the document, pageviewItems and the 'visible cache' 0148 PageView *q; 0149 Okular::Document *document; 0150 QVector<PageViewItem *> items; 0151 QList<PageViewItem *> visibleItems; 0152 MagnifierView *magnifierView; 0153 0154 // view layout (columns in Settings), zoom and mouse 0155 PageView::ZoomMode zoomMode; 0156 float zoomFactor; 0157 QPoint mouseGrabOffset; 0158 QPointF mousePressPos; 0159 QPointF mouseSelectPos; 0160 QPointF previousMouseMovePos; 0161 qreal mouseMidLastY; 0162 bool mouseSelecting; 0163 QRect mouseSelectionRect; 0164 QColor mouseSelectionColor; 0165 bool mouseTextSelecting; 0166 QSet<int> pagesWithTextSelection; 0167 bool mouseOnRect; 0168 int mouseMode; 0169 MouseAnnotation *mouseAnnotation; 0170 0171 // table selection 0172 QList<double> tableSelectionCols; 0173 QList<double> tableSelectionRows; 0174 QList<TableSelectionPart> tableSelectionParts; 0175 bool tableDividersGuessed; 0176 0177 int lastSourceLocationViewportPageNumber; 0178 double lastSourceLocationViewportNormalizedX; 0179 double lastSourceLocationViewportNormalizedY; 0180 0181 // for everything except PgUp/PgDn and scroll to arbitrary locations 0182 const int baseShortScrollDuration = 100; 0183 int currentShortScrollDuration; 0184 // for PgUp/PgDn and scroll to arbitrary locations 0185 const int baseLongScrollDuration = baseShortScrollDuration * 2; 0186 int currentLongScrollDuration; 0187 0188 // auto scroll 0189 int scrollIncrement; 0190 QTimer *autoScrollTimer; 0191 // annotations 0192 PageViewAnnotator *annotator; 0193 // text annotation dialogs list 0194 QSet<AnnotWindow *> m_annowindows; 0195 // other stuff 0196 QTimer *delayResizeEventTimer; 0197 bool dirtyLayout; 0198 bool blockViewport; // prevents changes to viewport 0199 bool blockPixmapsRequest; // prevent pixmap requests 0200 PageViewMessage *messageWindow; // in pageviewutils.h 0201 bool m_formsVisible; 0202 FormWidgetsController *formsWidgetController; 0203 #if HAVE_SPEECH 0204 OkularTTS *m_tts; 0205 #endif 0206 QTimer *refreshTimer; 0207 QSet<int> refreshPages; 0208 0209 // bbox state for Trim to Selection mode 0210 Okular::NormalizedRect trimBoundingBox; 0211 0212 // infinite resizing loop prevention 0213 bool verticalScrollBarVisible = false; 0214 bool horizontalScrollBarVisible = false; 0215 0216 // drag scroll 0217 QPoint dragScrollVector; 0218 QTimer dragScrollTimer; 0219 0220 // left click depress 0221 QTimer leftClickTimer; 0222 0223 // actions 0224 QAction *aRotateClockwise; 0225 QAction *aRotateCounterClockwise; 0226 QAction *aRotateOriginal; 0227 KActionMenu *aTrimMode; 0228 KToggleAction *aTrimMargins; 0229 KToggleAction *aReadingDirection; 0230 QAction *aMouseNormal; 0231 QAction *aMouseZoom; 0232 QAction *aMouseSelect; 0233 QAction *aMouseTextSelect; 0234 QAction *aMouseTableSelect; 0235 QAction *aMouseMagnifier; 0236 KToggleAction *aTrimToSelection; 0237 QAction *aSignature; 0238 KSelectAction *aZoom; 0239 QAction *aZoomIn; 0240 QAction *aZoomOut; 0241 QAction *aZoomActual; 0242 KToggleAction *aZoomFitWidth; 0243 KToggleAction *aZoomFitPage; 0244 KToggleAction *aZoomAutoFit; 0245 KActionMenu *aViewModeMenu; 0246 QActionGroup *viewModeActionGroup; 0247 ColorModeMenu *aColorModeMenu; 0248 KToggleAction *aViewContinuous; 0249 QAction *aPrevAction; 0250 KToggleAction *aToggleForms; 0251 QAction *aSpeakDoc; 0252 QAction *aSpeakPage; 0253 QAction *aSpeakStop; 0254 QAction *aSpeakPauseResume; 0255 KActionCollection *actionCollection; 0256 QActionGroup *mouseModeActionGroup; 0257 ToggleActionMenu *aMouseModeMenu; 0258 QAction *aFitWindowToPage; 0259 0260 int setting_viewCols; 0261 bool rtl_Mode; 0262 // Keep track of whether tablet pen is currently pressed down 0263 bool penDown; 0264 0265 // Keep track of mouse over link object 0266 const Okular::ObjectRect *mouseOverLinkObject; 0267 0268 QScroller *scroller; 0269 0270 bool pinchZoomActive; 0271 // The remaining scroll from the previous zoom event 0272 QPointF remainingScroll; 0273 }; 0274 0275 PageViewPrivate::PageViewPrivate(PageView *qq) 0276 : q(qq) 0277 #if HAVE_SPEECH 0278 , m_tts(nullptr) 0279 #endif 0280 { 0281 } 0282 0283 FormWidgetsController *PageViewPrivate::formWidgetsController() 0284 { 0285 if (!formsWidgetController) { 0286 formsWidgetController = new FormWidgetsController(document); 0287 QObject::connect(formsWidgetController, &FormWidgetsController::changed, q, &PageView::slotFormChanged); 0288 QObject::connect(formsWidgetController, &FormWidgetsController::action, q, &PageView::slotAction); 0289 QObject::connect(formsWidgetController, &FormWidgetsController::mouseUpAction, q, &PageView::slotMouseUpAction); 0290 } 0291 0292 return formsWidgetController; 0293 } 0294 0295 #if HAVE_SPEECH 0296 OkularTTS *PageViewPrivate::tts() 0297 { 0298 if (!m_tts) { 0299 m_tts = new OkularTTS(q); 0300 if (aSpeakStop) { 0301 QObject::connect(m_tts, &OkularTTS::canPauseOrResume, aSpeakStop, &QAction::setEnabled); 0302 } 0303 0304 if (aSpeakPauseResume) { 0305 QObject::connect(m_tts, &OkularTTS::canPauseOrResume, aSpeakPauseResume, &QAction::setEnabled); 0306 } 0307 } 0308 0309 return m_tts; 0310 } 0311 #endif 0312 0313 /* PageView. What's in this file? -> quick overview. 0314 * Code weight (in rows) and meaning: 0315 * 160 - constructor and creating actions plus their connected slots (empty stuff) 0316 * 70 - DocumentObserver inherited methodes (important) 0317 * 550 - events: mouse, keyboard, drag 0318 * 170 - slotRelayoutPages: set contents of the scrollview on continuous/single modes 0319 * 100 - zoom: zooming pages in different ways, keeping update the toolbar actions, etc.. 0320 * other misc functions: only slotRequestVisiblePixmaps and pickItemOnPoint noticeable, 0321 * and many insignificant stuff like this comment :-) 0322 */ 0323 PageView::PageView(QWidget *parent, Okular::Document *document) 0324 : QAbstractScrollArea(parent) 0325 , Okular::View(QStringLiteral("PageView")) 0326 { 0327 // create and initialize private storage structure 0328 d = new PageViewPrivate(this); 0329 d->document = document; 0330 d->aRotateClockwise = nullptr; 0331 d->aRotateCounterClockwise = nullptr; 0332 d->aRotateOriginal = nullptr; 0333 d->aViewModeMenu = nullptr; 0334 d->zoomMode = PageView::ZoomFitWidth; 0335 d->zoomFactor = 1.0; 0336 d->mouseSelecting = false; 0337 d->mouseTextSelecting = false; 0338 d->mouseOnRect = false; 0339 d->mouseMode = Okular::Settings::mouseMode(); 0340 d->mouseAnnotation = new MouseAnnotation(this, document); 0341 d->tableDividersGuessed = false; 0342 d->lastSourceLocationViewportPageNumber = -1; 0343 d->lastSourceLocationViewportNormalizedX = 0.0; 0344 d->lastSourceLocationViewportNormalizedY = 0.0; 0345 d->currentShortScrollDuration = d->baseShortScrollDuration; 0346 d->currentLongScrollDuration = d->baseLongScrollDuration; 0347 d->scrollIncrement = 0; 0348 d->autoScrollTimer = nullptr; 0349 d->annotator = nullptr; 0350 d->dirtyLayout = false; 0351 d->blockViewport = false; 0352 d->blockPixmapsRequest = false; 0353 d->messageWindow = new PageViewMessage(this); 0354 d->m_formsVisible = false; 0355 d->formsWidgetController = nullptr; 0356 #if HAVE_SPEECH 0357 d->m_tts = nullptr; 0358 #endif 0359 d->refreshTimer = nullptr; 0360 d->aRotateClockwise = nullptr; 0361 d->aRotateCounterClockwise = nullptr; 0362 d->aRotateOriginal = nullptr; 0363 d->aTrimMode = nullptr; 0364 d->aTrimMargins = nullptr; 0365 d->aTrimToSelection = nullptr; 0366 d->aReadingDirection = nullptr; 0367 d->aMouseNormal = nullptr; 0368 d->aMouseZoom = nullptr; 0369 d->aMouseSelect = nullptr; 0370 d->aMouseTextSelect = nullptr; 0371 d->aSignature = nullptr; 0372 d->aZoomFitWidth = nullptr; 0373 d->aZoomFitPage = nullptr; 0374 d->aZoomAutoFit = nullptr; 0375 d->aViewModeMenu = nullptr; 0376 d->aViewContinuous = nullptr; 0377 d->viewModeActionGroup = nullptr; 0378 d->aColorModeMenu = nullptr; 0379 d->aPrevAction = nullptr; 0380 d->aToggleForms = nullptr; 0381 d->aSpeakDoc = nullptr; 0382 d->aSpeakPage = nullptr; 0383 d->aSpeakStop = nullptr; 0384 d->aSpeakPauseResume = nullptr; 0385 d->actionCollection = nullptr; 0386 d->setting_viewCols = Okular::Settings::viewColumns(); 0387 d->rtl_Mode = Okular::Settings::rtlReadingDirection(); 0388 d->mouseModeActionGroup = nullptr; 0389 d->aMouseModeMenu = nullptr; 0390 d->penDown = false; 0391 d->aMouseMagnifier = nullptr; 0392 d->aFitWindowToPage = nullptr; 0393 d->trimBoundingBox = Okular::NormalizedRect(); // Null box 0394 d->pinchZoomActive = false; 0395 d->remainingScroll = QPointF(0.0, 0.0); 0396 0397 switch (Okular::Settings::zoomMode()) { 0398 case 0: { 0399 d->zoomFactor = 1; 0400 d->zoomMode = PageView::ZoomFixed; 0401 break; 0402 } 0403 case 1: { 0404 d->zoomMode = PageView::ZoomFitWidth; 0405 break; 0406 } 0407 case 2: { 0408 d->zoomMode = PageView::ZoomFitPage; 0409 break; 0410 } 0411 case 3: { 0412 d->zoomMode = PageView::ZoomFitAuto; 0413 break; 0414 } 0415 } 0416 0417 connect(Okular::Settings::self(), &Okular::Settings::viewContinuousChanged, this, [=]() { 0418 if (d->aViewContinuous && !d->document->isOpened()) { 0419 d->aViewContinuous->setChecked(Okular::Settings::viewContinuous()); 0420 } 0421 }); 0422 0423 d->delayResizeEventTimer = new QTimer(this); 0424 d->delayResizeEventTimer->setSingleShot(true); 0425 d->delayResizeEventTimer->setObjectName(QStringLiteral("delayResizeEventTimer")); 0426 connect(d->delayResizeEventTimer, &QTimer::timeout, this, &PageView::delayedResizeEvent); 0427 0428 setFrameStyle(QFrame::NoFrame); 0429 0430 setAttribute(Qt::WA_StaticContents); 0431 0432 setObjectName(QStringLiteral("okular::pageView")); 0433 0434 // viewport setup: setup focus, and track mouse 0435 viewport()->setFocusProxy(this); 0436 viewport()->setFocusPolicy(Qt::StrongFocus); 0437 viewport()->setAttribute(Qt::WA_OpaquePaintEvent); 0438 viewport()->setAttribute(Qt::WA_NoSystemBackground); 0439 viewport()->setMouseTracking(true); 0440 viewport()->setAutoFillBackground(false); 0441 0442 d->scroller = QScroller::scroller(viewport()); 0443 0444 QScrollerProperties prop; 0445 prop.setScrollMetric(QScrollerProperties::DecelerationFactor, 0.3); 0446 prop.setScrollMetric(QScrollerProperties::MaximumVelocity, 1); 0447 prop.setScrollMetric(QScrollerProperties::AcceleratingFlickMaximumTime, 0.2); // Workaround for QTBUG-88249 (non-flick gestures recognized as accelerating flick) 0448 prop.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff); 0449 prop.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff); 0450 prop.setScrollMetric(QScrollerProperties::DragStartDistance, 0.0); 0451 d->scroller->setScrollerProperties(prop); 0452 0453 connect(d->scroller, &QScroller::stateChanged, this, [this](QScroller::State s) { slotRequestVisiblePixmaps(s); }); 0454 0455 // the apparently "magic" value of 20 is the same used internally in QScrollArea 0456 verticalScrollBar()->setCursor(Qt::ArrowCursor); 0457 verticalScrollBar()->setSingleStep(20); 0458 horizontalScrollBar()->setCursor(Qt::ArrowCursor); 0459 horizontalScrollBar()->setSingleStep(20); 0460 0461 // make the smooth scroll animation durations respect the global animation 0462 // scale 0463 KConfigWatcher::Ptr animationSpeedWatcher = KConfigWatcher::create(KSharedConfig::openConfig()); 0464 connect(animationSpeedWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) { 0465 if (group.name() == QLatin1String("KDE") && names.contains(QByteArrayLiteral("AnimationDurationFactor"))) { 0466 PageView::updateSmoothScrollAnimationSpeed(); 0467 } 0468 }); 0469 0470 // connect the padding of the viewport to pixmaps requests 0471 connect(horizontalScrollBar(), &QAbstractSlider::valueChanged, this, &PageView::slotRequestVisiblePixmaps); 0472 connect(verticalScrollBar(), &QAbstractSlider::valueChanged, this, &PageView::slotRequestVisiblePixmaps); 0473 0474 // Keep the scroller in sync with user input on the scrollbars. 0475 // QAbstractSlider::sliderMoved() and sliderReleased are the intuitive signals, 0476 // but are only emitted when the “slider is down”, i. e. not when the user scrolls on the scrollbar. 0477 // QAbstractSlider::actionTriggered() is emitted in all user input cases, 0478 // but before the value() changes, so we need queued connection here. 0479 auto update_scroller = [=]() { 0480 d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); // sync scroller with scrollbar 0481 }; 0482 connect(verticalScrollBar(), &QAbstractSlider::actionTriggered, this, update_scroller, Qt::QueuedConnection); 0483 connect(horizontalScrollBar(), &QAbstractSlider::actionTriggered, this, update_scroller, Qt::QueuedConnection); 0484 0485 connect(&d->dragScrollTimer, &QTimer::timeout, this, &PageView::slotDragScroll); 0486 0487 d->leftClickTimer.setSingleShot(true); 0488 connect(&d->leftClickTimer, &QTimer::timeout, this, &PageView::slotShowSizeAllCursor); 0489 0490 // set a corner button to resize the view to the page size 0491 // QPushButton * resizeButton = new QPushButton( viewport() ); 0492 // resizeButton->setPixmap( SmallIcon("crop") ); 0493 // setCornerWidget( resizeButton ); 0494 // resizeButton->setEnabled( false ); 0495 // connect(...); 0496 setAttribute(Qt::WA_InputMethodEnabled, true); 0497 0498 // Grab pinch gestures to zoom and rotate the view 0499 grabGesture(Qt::PinchGesture); 0500 0501 d->magnifierView = new MagnifierView(document, this); 0502 d->magnifierView->hide(); 0503 d->magnifierView->setGeometry(0, 0, 351, 201); // TODO: more dynamic? 0504 0505 connect(document, &Okular::Document::processMovieAction, this, &PageView::slotProcessMovieAction); 0506 connect(document, &Okular::Document::processRenditionAction, this, &PageView::slotProcessRenditionAction); 0507 0508 // schedule the welcome message 0509 QMetaObject::invokeMethod(this, "slotShowWelcome", Qt::QueuedConnection); 0510 } 0511 0512 PageView::~PageView() 0513 { 0514 #if HAVE_SPEECH 0515 if (d->m_tts) { 0516 d->m_tts->stopAllSpeechs(); 0517 } 0518 #endif 0519 0520 delete d->mouseAnnotation; 0521 0522 // delete the local storage structure 0523 0524 // We need to assign it to a different list otherwise slotAnnotationWindowDestroyed 0525 // will bite us and clear d->m_annowindows 0526 QSet<AnnotWindow *> annowindows = d->m_annowindows; 0527 d->m_annowindows.clear(); 0528 qDeleteAll(annowindows); 0529 0530 // delete all widgets 0531 qDeleteAll(d->items); 0532 delete d->formsWidgetController; 0533 d->document->removeObserver(this); 0534 delete d; 0535 } 0536 0537 void PageView::setupBaseActions(KActionCollection *ac) 0538 { 0539 d->actionCollection = ac; 0540 0541 // Zoom actions ( higher scales takes lots of memory! ) 0542 d->aZoom = new KSelectAction(QIcon::fromTheme(QStringLiteral("page-zoom")), i18n("Zoom"), this); 0543 ac->addAction(QStringLiteral("zoom_to"), d->aZoom); 0544 d->aZoom->setEditable(true); 0545 d->aZoom->setMaxComboViewCount(kZoomValues.size() + 3); 0546 connect(d->aZoom, &KSelectAction::actionTriggered, this, &PageView::slotZoom); 0547 updateZoomText(); 0548 0549 d->aZoomIn = KStandardAction::zoomIn(this, SLOT(slotZoomIn()), ac); 0550 0551 d->aZoomOut = KStandardAction::zoomOut(this, SLOT(slotZoomOut()), ac); 0552 0553 d->aZoomActual = KStandardAction::actualSize(this, &PageView::slotZoomActual, ac); 0554 d->aZoomActual->setText(i18n("Zoom to 100%")); 0555 } 0556 0557 void PageView::setupViewerActions(KActionCollection *ac) 0558 { 0559 d->actionCollection = ac; 0560 0561 ac->setDefaultShortcut(d->aZoomIn, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_Plus)); 0562 ac->setDefaultShortcut(d->aZoomOut, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_Minus)); 0563 0564 // orientation menu actions 0565 d->aRotateClockwise = new QAction(QIcon::fromTheme(QStringLiteral("object-rotate-right")), i18n("Rotate &Right"), this); 0566 d->aRotateClockwise->setIconText(i18nc("Rotate right", "Right")); 0567 ac->addAction(QStringLiteral("view_orientation_rotate_cw"), d->aRotateClockwise); 0568 d->aRotateClockwise->setEnabled(false); 0569 connect(d->aRotateClockwise, &QAction::triggered, this, &PageView::slotRotateClockwise); 0570 d->aRotateCounterClockwise = new QAction(QIcon::fromTheme(QStringLiteral("object-rotate-left")), i18n("Rotate &Left"), this); 0571 d->aRotateCounterClockwise->setIconText(i18nc("Rotate left", "Left")); 0572 ac->addAction(QStringLiteral("view_orientation_rotate_ccw"), d->aRotateCounterClockwise); 0573 d->aRotateCounterClockwise->setEnabled(false); 0574 connect(d->aRotateCounterClockwise, &QAction::triggered, this, &PageView::slotRotateCounterClockwise); 0575 d->aRotateOriginal = new QAction(i18n("Original Orientation"), this); 0576 ac->addAction(QStringLiteral("view_orientation_original"), d->aRotateOriginal); 0577 d->aRotateOriginal->setEnabled(false); 0578 connect(d->aRotateOriginal, &QAction::triggered, this, &PageView::slotRotateOriginal); 0579 0580 // Trim View actions 0581 d->aTrimMode = new KActionMenu(i18n("&Trim View"), this); 0582 d->aTrimMode->setPopupMode(QToolButton::InstantPopup); 0583 ac->addAction(QStringLiteral("view_trim_mode"), d->aTrimMode); 0584 0585 d->aTrimMargins = new KToggleAction(QIcon::fromTheme(QStringLiteral("trim-margins")), i18n("&Trim Margins"), d->aTrimMode->menu()); 0586 d->aTrimMode->addAction(d->aTrimMargins); 0587 ac->addAction(QStringLiteral("view_trim_margins"), d->aTrimMargins); 0588 d->aTrimMargins->setData(QVariant::fromValue((int)Okular::Settings::EnumTrimMode::Margins)); 0589 connect(d->aTrimMargins, &QAction::toggled, this, &PageView::slotTrimMarginsToggled); 0590 d->aTrimMargins->setChecked(Okular::Settings::trimMargins()); 0591 0592 d->aTrimToSelection = new KToggleAction(QIcon::fromTheme(QStringLiteral("trim-to-selection")), i18n("Trim To &Selection"), d->aTrimMode->menu()); 0593 d->aTrimMode->addAction(d->aTrimToSelection); 0594 ac->addAction(QStringLiteral("view_trim_selection"), d->aTrimToSelection); 0595 d->aTrimToSelection->setData(QVariant::fromValue((int)Okular::Settings::EnumTrimMode::Selection)); 0596 connect(d->aTrimToSelection, &QAction::toggled, this, &PageView::slotTrimToSelectionToggled); 0597 0598 d->aZoomFitWidth = new KToggleAction(QIcon::fromTheme(QStringLiteral("zoom-fit-width")), i18n("Fit &Width"), this); 0599 ac->addAction(QStringLiteral("view_fit_to_width"), d->aZoomFitWidth); 0600 connect(d->aZoomFitWidth, &QAction::toggled, this, &PageView::slotFitToWidthToggled); 0601 0602 d->aZoomFitPage = new KToggleAction(QIcon::fromTheme(QStringLiteral("zoom-fit-best")), i18n("Fit &Page"), this); 0603 ac->addAction(QStringLiteral("view_fit_to_page"), d->aZoomFitPage); 0604 connect(d->aZoomFitPage, &QAction::toggled, this, &PageView::slotFitToPageToggled); 0605 0606 d->aZoomAutoFit = new KToggleAction(QIcon::fromTheme(QStringLiteral("zoom-fit-best")), i18n("&Auto Fit"), this); 0607 ac->addAction(QStringLiteral("view_auto_fit"), d->aZoomAutoFit); 0608 connect(d->aZoomAutoFit, &QAction::toggled, this, &PageView::slotAutoFitToggled); 0609 0610 d->aFitWindowToPage = new QAction(QIcon::fromTheme(QStringLiteral("zoom-fit-width")), i18n("Fit Wi&ndow to Page"), this); 0611 d->aFitWindowToPage->setEnabled(Okular::Settings::viewMode() == (int)Okular::Settings::EnumViewMode::Single); 0612 ac->setDefaultShortcut(d->aFitWindowToPage, QKeySequence(Qt::CTRL | Qt::Key_J)); 0613 ac->addAction(QStringLiteral("fit_window_to_page"), d->aFitWindowToPage); 0614 connect(d->aFitWindowToPage, &QAction::triggered, this, &PageView::slotFitWindowToPage); 0615 0616 // View Mode action menu (Single Page, Facing Pages,...(choose), and Continuous (on/off)) 0617 d->aViewModeMenu = new KActionMenu(QIcon::fromTheme(QStringLiteral("view-split-left-right")), i18n("&View Mode"), this); 0618 d->aViewModeMenu->setPopupMode(QToolButton::InstantPopup); 0619 ac->addAction(QStringLiteral("view_render_mode"), d->aViewModeMenu); 0620 0621 d->viewModeActionGroup = new QActionGroup(this); 0622 auto addViewMode = [=](QAction *a, const QString &name, Okular::Settings::EnumViewMode::type id) { 0623 a->setCheckable(true); 0624 a->setData(int(id)); 0625 d->aViewModeMenu->addAction(a); 0626 ac->addAction(name, a); 0627 d->viewModeActionGroup->addAction(a); 0628 }; 0629 addViewMode(new QAction(QIcon::fromTheme(QStringLiteral("view-pages-single")), i18nc("@item:inmenu", "&Single Page"), this), QStringLiteral("view_render_mode_single"), Okular::Settings::EnumViewMode::Single); 0630 addViewMode(new QAction(QIcon::fromTheme(QStringLiteral("view-pages-facing")), i18nc("@item:inmenu", "&Facing Pages"), this), QStringLiteral("view_render_mode_facing"), Okular::Settings::EnumViewMode::Facing); 0631 addViewMode(new QAction(QIcon::fromTheme(QStringLiteral("view-pages-facing-first-centered")), i18nc("@item:inmenu", "Facing Pages (&Center First Page)"), this), 0632 QStringLiteral("view_render_mode_facing_center_first"), 0633 Okular::Settings::EnumViewMode::FacingFirstCentered); 0634 addViewMode(new QAction(QIcon::fromTheme(QStringLiteral("view-pages-overview")), i18nc("@item:inmenu", "&Overview"), this), QStringLiteral("view_render_mode_overview"), Okular::Settings::EnumViewMode::Summary); 0635 const QList<QAction *> viewModeActions = d->viewModeActionGroup->actions(); 0636 for (QAction *viewModeAction : viewModeActions) { 0637 if (viewModeAction->data().toInt() == Okular::Settings::viewMode()) { 0638 viewModeAction->setChecked(true); 0639 break; 0640 } 0641 } 0642 connect(d->viewModeActionGroup, &QActionGroup::triggered, this, &PageView::slotViewMode); 0643 0644 // Continuous view action, add to view mode action menu. 0645 d->aViewModeMenu->addSeparator(); 0646 d->aViewContinuous = new KToggleAction(QIcon::fromTheme(QStringLiteral("view-pages-continuous")), i18n("&Continuous"), this); 0647 d->aViewModeMenu->addAction(d->aViewContinuous); 0648 ac->addAction(QStringLiteral("view_continuous"), d->aViewContinuous); 0649 connect(d->aViewContinuous, &QAction::toggled, this, &PageView::slotContinuousToggled); 0650 d->aViewContinuous->setChecked(Okular::Settings::viewContinuous()); 0651 0652 // Reading direction toggle action. (Checked means RTL, unchecked means LTR.) 0653 d->aReadingDirection = new KToggleAction(QIcon::fromTheme(QStringLiteral("format-text-direction-rtl")), i18nc("@action page layout", "Use Right to Left Reading Direction"), this); 0654 d->aReadingDirection->setChecked(Okular::Settings::rtlReadingDirection()); 0655 ac->addAction(QStringLiteral("rtl_page_layout"), d->aReadingDirection); 0656 connect(d->aReadingDirection, &QAction::toggled, this, &PageView::slotReadingDirectionToggled); 0657 connect(Okular::SettingsCore::self(), &Okular::SettingsCore::configChanged, this, &PageView::slotUpdateReadingDirectionAction); 0658 0659 // Mouse mode actions for viewer mode 0660 d->mouseModeActionGroup = new QActionGroup(this); 0661 d->mouseModeActionGroup->setExclusive(true); 0662 d->aMouseNormal = new QAction(QIcon::fromTheme(QStringLiteral("transform-browse")), i18n("&Browse"), this); 0663 ac->addAction(QStringLiteral("mouse_drag"), d->aMouseNormal); 0664 connect(d->aMouseNormal, &QAction::triggered, this, &PageView::slotSetMouseNormal); 0665 d->aMouseNormal->setCheckable(true); 0666 ac->setDefaultShortcut(d->aMouseNormal, QKeySequence(Qt::CTRL | Qt::Key_1)); 0667 d->aMouseNormal->setActionGroup(d->mouseModeActionGroup); 0668 d->aMouseNormal->setChecked(Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::Browse); 0669 0670 d->aMouseZoom = new QAction(QIcon::fromTheme(QStringLiteral("page-zoom")), i18n("&Zoom"), this); 0671 ac->addAction(QStringLiteral("mouse_zoom"), d->aMouseZoom); 0672 connect(d->aMouseZoom, &QAction::triggered, this, &PageView::slotSetMouseZoom); 0673 d->aMouseZoom->setCheckable(true); 0674 ac->setDefaultShortcut(d->aMouseZoom, QKeySequence(Qt::CTRL | Qt::Key_2)); 0675 d->aMouseZoom->setActionGroup(d->mouseModeActionGroup); 0676 d->aMouseZoom->setChecked(Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::Zoom); 0677 0678 d->aColorModeMenu = new ColorModeMenu(ac, this); 0679 } 0680 0681 // WARNING: 'setupViewerActions' must have been called before this method 0682 void PageView::setupActions(KActionCollection *ac) 0683 { 0684 d->actionCollection = ac; 0685 0686 ac->setDefaultShortcuts(d->aZoomIn, KStandardShortcut::zoomIn()); 0687 ac->setDefaultShortcuts(d->aZoomOut, KStandardShortcut::zoomOut()); 0688 0689 // Mouse-Mode actions 0690 d->aMouseSelect = new QAction(QIcon::fromTheme(QStringLiteral("select-rectangular")), i18n("Area &Selection"), this); 0691 ac->addAction(QStringLiteral("mouse_select"), d->aMouseSelect); 0692 connect(d->aMouseSelect, &QAction::triggered, this, &PageView::slotSetMouseSelect); 0693 d->aMouseSelect->setCheckable(true); 0694 ac->setDefaultShortcut(d->aMouseSelect, Qt::CTRL | Qt::Key_3); 0695 d->aMouseSelect->setActionGroup(d->mouseModeActionGroup); 0696 0697 d->aMouseTextSelect = new QAction(QIcon::fromTheme(QStringLiteral("edit-select-text")), i18n("&Text Selection"), this); 0698 ac->addAction(QStringLiteral("mouse_textselect"), d->aMouseTextSelect); 0699 connect(d->aMouseTextSelect, &QAction::triggered, this, &PageView::slotSetMouseTextSelect); 0700 d->aMouseTextSelect->setCheckable(true); 0701 ac->setDefaultShortcut(d->aMouseTextSelect, Qt::CTRL | Qt::Key_4); 0702 d->aMouseTextSelect->setActionGroup(d->mouseModeActionGroup); 0703 0704 d->aMouseTableSelect = new QAction(QIcon::fromTheme(QStringLiteral("table")), i18n("T&able Selection"), this); 0705 ac->addAction(QStringLiteral("mouse_tableselect"), d->aMouseTableSelect); 0706 connect(d->aMouseTableSelect, &QAction::triggered, this, &PageView::slotSetMouseTableSelect); 0707 d->aMouseTableSelect->setCheckable(true); 0708 ac->setDefaultShortcut(d->aMouseTableSelect, Qt::CTRL | Qt::Key_5); 0709 d->aMouseTableSelect->setActionGroup(d->mouseModeActionGroup); 0710 0711 d->aMouseMagnifier = new QAction(QIcon::fromTheme(QStringLiteral("document-preview")), i18n("&Magnifier"), this); 0712 ac->addAction(QStringLiteral("mouse_magnifier"), d->aMouseMagnifier); 0713 connect(d->aMouseMagnifier, &QAction::triggered, this, &PageView::slotSetMouseMagnifier); 0714 d->aMouseMagnifier->setCheckable(true); 0715 ac->setDefaultShortcut(d->aMouseMagnifier, Qt::CTRL | Qt::Key_6); 0716 d->aMouseMagnifier->setActionGroup(d->mouseModeActionGroup); 0717 d->aMouseMagnifier->setChecked(Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::Magnifier); 0718 0719 // Mouse mode selection tools menu 0720 d->aMouseModeMenu = new ToggleActionMenu(i18nc("@action", "Selection Tools"), this); 0721 d->aMouseModeMenu->setPopupMode(QToolButton::MenuButtonPopup); 0722 d->aMouseModeMenu->addAction(d->aMouseSelect); 0723 d->aMouseModeMenu->addAction(d->aMouseTextSelect); 0724 d->aMouseModeMenu->addAction(d->aMouseTableSelect); 0725 connect(d->aMouseModeMenu->menu(), &QMenu::triggered, d->aMouseModeMenu, &ToggleActionMenu::setDefaultAction); 0726 ac->addAction(QStringLiteral("mouse_selecttools"), d->aMouseModeMenu); 0727 0728 switch (Okular::Settings::mouseMode()) { 0729 case Okular::Settings::EnumMouseMode::TextSelect: 0730 d->aMouseTextSelect->setChecked(true); 0731 d->aMouseModeMenu->setDefaultAction(d->aMouseTextSelect); 0732 break; 0733 case Okular::Settings::EnumMouseMode::RectSelect: 0734 d->aMouseSelect->setChecked(true); 0735 d->aMouseModeMenu->setDefaultAction(d->aMouseSelect); 0736 break; 0737 case Okular::Settings::EnumMouseMode::TableSelect: 0738 d->aMouseTableSelect->setChecked(true); 0739 d->aMouseModeMenu->setDefaultAction(d->aMouseTableSelect); 0740 break; 0741 default: 0742 d->aMouseModeMenu->setDefaultAction(d->aMouseTextSelect); 0743 } 0744 0745 // Create signature action 0746 d->aSignature = new QAction(QIcon::fromTheme(QStringLiteral("document-edit-sign")), i18n("Digitally &Sign..."), this); 0747 ac->addAction(QStringLiteral("add_digital_signature"), d->aSignature); 0748 connect(d->aSignature, &QAction::triggered, this, &PageView::slotSignature); 0749 0750 // speak actions 0751 #if HAVE_SPEECH 0752 d->aSpeakDoc = new QAction(QIcon::fromTheme(QStringLiteral("text-speak")), i18n("Speak Whole Document"), this); 0753 ac->addAction(QStringLiteral("speak_document"), d->aSpeakDoc); 0754 d->aSpeakDoc->setEnabled(false); 0755 connect(d->aSpeakDoc, &QAction::triggered, this, &PageView::slotSpeakDocument); 0756 0757 d->aSpeakPage = new QAction(QIcon::fromTheme(QStringLiteral("text-speak")), i18n("Speak Current Page"), this); 0758 ac->addAction(QStringLiteral("speak_current_page"), d->aSpeakPage); 0759 d->aSpeakPage->setEnabled(false); 0760 connect(d->aSpeakPage, &QAction::triggered, this, &PageView::slotSpeakCurrentPage); 0761 0762 d->aSpeakStop = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-stop")), i18n("Stop Speaking"), this); 0763 ac->addAction(QStringLiteral("speak_stop_all"), d->aSpeakStop); 0764 d->aSpeakStop->setEnabled(false); 0765 connect(d->aSpeakStop, &QAction::triggered, this, &PageView::slotStopSpeaks); 0766 0767 d->aSpeakPauseResume = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-pause")), i18n("Pause/Resume Speaking"), this); 0768 ac->addAction(QStringLiteral("speak_pause_resume"), d->aSpeakPauseResume); 0769 d->aSpeakPauseResume->setEnabled(false); 0770 connect(d->aSpeakPauseResume, &QAction::triggered, this, &PageView::slotPauseResumeSpeech); 0771 #else 0772 d->aSpeakDoc = nullptr; 0773 d->aSpeakPage = nullptr; 0774 d->aSpeakStop = nullptr; 0775 d->aSpeakPauseResume = nullptr; 0776 #endif 0777 0778 // Other actions 0779 QAction *su = new QAction(i18n("Scroll Up"), this); 0780 ac->addAction(QStringLiteral("view_scroll_up"), su); 0781 connect(su, &QAction::triggered, this, &PageView::slotAutoScrollUp); 0782 ac->setDefaultShortcut(su, QKeySequence(Qt::SHIFT | Qt::Key_Up)); 0783 addAction(su); 0784 0785 QAction *sd = new QAction(i18n("Scroll Down"), this); 0786 ac->addAction(QStringLiteral("view_scroll_down"), sd); 0787 connect(sd, &QAction::triggered, this, &PageView::slotAutoScrollDown); 0788 ac->setDefaultShortcut(sd, QKeySequence(Qt::SHIFT | Qt::Key_Down)); 0789 addAction(sd); 0790 0791 QAction *spu = new QAction(i18n("Scroll Page Up"), this); 0792 ac->addAction(QStringLiteral("view_scroll_page_up"), spu); 0793 connect(spu, &QAction::triggered, this, &PageView::slotScrollUp); 0794 ac->setDefaultShortcut(spu, QKeySequence(Qt::SHIFT | Qt::Key_Space)); 0795 addAction(spu); 0796 0797 QAction *spd = new QAction(i18n("Scroll Page Down"), this); 0798 ac->addAction(QStringLiteral("view_scroll_page_down"), spd); 0799 connect(spd, &QAction::triggered, this, &PageView::slotScrollDown); 0800 ac->setDefaultShortcut(spd, QKeySequence(Qt::Key_Space)); 0801 addAction(spd); 0802 0803 d->aToggleForms = new KToggleAction(i18n("Show Forms"), this); 0804 ac->addAction(QStringLiteral("view_toggle_forms"), d->aToggleForms); 0805 connect(d->aToggleForms, &QAction::toggled, this, &PageView::slotToggleForms); 0806 d->aToggleForms->setEnabled(false); 0807 toggleFormWidgets(false); 0808 0809 // Setup undo and redo actions 0810 QAction *kundo = KStandardAction::create(KStandardAction::Undo, d->document, SLOT(undo()), ac); 0811 QAction *kredo = KStandardAction::create(KStandardAction::Redo, d->document, SLOT(redo()), ac); 0812 connect(d->document, &Okular::Document::canUndoChanged, kundo, &QAction::setEnabled); 0813 connect(d->document, &Okular::Document::canRedoChanged, kredo, &QAction::setEnabled); 0814 kundo->setEnabled(false); 0815 kredo->setEnabled(false); 0816 0817 d->annotator = new PageViewAnnotator(this, d->document); 0818 connect(d->annotator, &PageViewAnnotator::toolActive, this, [&](bool selected) { 0819 if (selected) { 0820 QAction *aMouseMode = d->mouseModeActionGroup->checkedAction(); 0821 if (aMouseMode) { 0822 aMouseMode->setChecked(false); 0823 } 0824 } else { 0825 switch (d->mouseMode) { 0826 case Okular::Settings::EnumMouseMode::Browse: 0827 d->aMouseNormal->setChecked(true); 0828 break; 0829 case Okular::Settings::EnumMouseMode::Zoom: 0830 d->aMouseZoom->setChecked(true); 0831 break; 0832 case Okular::Settings::EnumMouseMode::RectSelect: 0833 d->aMouseSelect->setChecked(true); 0834 break; 0835 case Okular::Settings::EnumMouseMode::TableSelect: 0836 d->aMouseTableSelect->setChecked(true); 0837 break; 0838 case Okular::Settings::EnumMouseMode::Magnifier: 0839 d->aMouseMagnifier->setChecked(true); 0840 break; 0841 case Okular::Settings::EnumMouseMode::TextSelect: 0842 d->aMouseTextSelect->setChecked(true); 0843 break; 0844 } 0845 } 0846 }); 0847 connect(d->annotator, &PageViewAnnotator::toolActive, d->mouseAnnotation, &MouseAnnotation::reset); 0848 connect(d->annotator, &PageViewAnnotator::requestOpenFile, this, &PageView::requestOpenFile); 0849 d->annotator->setupActions(ac); 0850 } 0851 0852 bool PageView::canFitPageWidth() const 0853 { 0854 return Okular::Settings::viewMode() != Okular::Settings::EnumViewMode::Single || d->zoomMode != ZoomFitWidth; 0855 } 0856 0857 void PageView::fitPageWidth(int page) 0858 { 0859 // zoom: Fit Width, columns: 1. setActions + relayout + setPage + update 0860 d->zoomMode = ZoomFitWidth; 0861 Okular::Settings::setViewMode(Okular::Settings::EnumViewMode::Single); 0862 d->aZoomFitWidth->setChecked(true); 0863 d->aZoomFitPage->setChecked(false); 0864 d->aZoomAutoFit->setChecked(false); 0865 updateViewMode(Okular::Settings::EnumViewMode::Single); 0866 viewport()->setUpdatesEnabled(false); 0867 slotRelayoutPages(); 0868 viewport()->setUpdatesEnabled(true); 0869 d->document->setViewportPage(page); 0870 updateZoomText(); 0871 setFocus(); 0872 } 0873 0874 void PageView::openAnnotationWindow(Okular::Annotation *annotation, int pageNumber) 0875 { 0876 if (!annotation) { 0877 return; 0878 } 0879 0880 // find the annot window 0881 AnnotWindow *existWindow = nullptr; 0882 for (AnnotWindow *aw : std::as_const(d->m_annowindows)) { 0883 if (aw->annotation() == annotation) { 0884 existWindow = aw; 0885 break; 0886 } 0887 } 0888 0889 if (existWindow == nullptr) { 0890 existWindow = new AnnotWindow(this, annotation, d->document, pageNumber); 0891 connect(existWindow, &QObject::destroyed, this, &PageView::slotAnnotationWindowDestroyed); 0892 0893 d->m_annowindows << existWindow; 0894 } else { 0895 existWindow->raise(); 0896 existWindow->findChild<KTextEdit *>()->setFocus(); 0897 } 0898 0899 existWindow->show(); 0900 } 0901 0902 void PageView::slotAnnotationWindowDestroyed(QObject *window) 0903 { 0904 d->m_annowindows.remove(static_cast<AnnotWindow *>(window)); 0905 } 0906 0907 void PageView::displayMessage(const QString &message, const QString &details, PageViewMessage::Icon icon, int duration) 0908 { 0909 if (!Okular::Settings::showOSD()) { 0910 if (icon == PageViewMessage::Error) { 0911 if (!details.isEmpty()) { 0912 KMessageBox::detailedError(this, message, details); 0913 } else { 0914 KMessageBox::error(this, message); 0915 } 0916 } 0917 return; 0918 } 0919 0920 // hide messageWindow if string is empty 0921 if (message.isEmpty()) { 0922 d->messageWindow->hide(); 0923 return; 0924 } 0925 0926 // display message (duration is length dependent) 0927 if (duration == -1) { 0928 duration = 500 + 100 * message.length(); 0929 if (!details.isEmpty()) { 0930 duration += 500 + 100 * details.length(); 0931 } 0932 } 0933 d->messageWindow->display(message, details, icon, duration); 0934 } 0935 0936 void PageView::reparseConfig() 0937 { 0938 // set smooth scrolling policies 0939 PageView::updateSmoothScrollAnimationSpeed(); 0940 0941 // set the scroll bars policies 0942 Qt::ScrollBarPolicy scrollBarMode = Okular::Settings::showScrollBars() ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff; 0943 if (horizontalScrollBarPolicy() != scrollBarMode) { 0944 setHorizontalScrollBarPolicy(scrollBarMode); 0945 setVerticalScrollBarPolicy(scrollBarMode); 0946 } 0947 0948 if (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Summary && ((int)Okular::Settings::viewColumns() != d->setting_viewCols)) { 0949 d->setting_viewCols = Okular::Settings::viewColumns(); 0950 0951 slotRelayoutPages(); 0952 } 0953 0954 if (Okular::Settings::rtlReadingDirection() != d->rtl_Mode) { 0955 d->rtl_Mode = Okular::Settings::rtlReadingDirection(); 0956 slotRelayoutPages(); 0957 } 0958 0959 updatePageStep(); 0960 0961 if (d->annotator) { 0962 d->annotator->reparseConfig(); 0963 } 0964 0965 // Something like invert colors may have changed 0966 // As we don't have a way to find out the old value 0967 // We just update the viewport, this shouldn't be that bad 0968 // since it's just a repaint of pixmaps we already have 0969 viewport()->update(); 0970 } 0971 0972 KActionCollection *PageView::actionCollection() const 0973 { 0974 return d->actionCollection; 0975 } 0976 0977 QAction *PageView::toggleFormsAction() const 0978 { 0979 return d->aToggleForms; 0980 } 0981 0982 int PageView::contentAreaWidth() const 0983 { 0984 return horizontalScrollBar()->maximum() + viewport()->width(); 0985 } 0986 0987 int PageView::contentAreaHeight() const 0988 { 0989 return verticalScrollBar()->maximum() + viewport()->height(); 0990 } 0991 0992 QPoint PageView::contentAreaPosition() const 0993 { 0994 return QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()); 0995 } 0996 0997 QPoint PageView::contentAreaPoint(const QPoint pos) const 0998 { 0999 return pos + contentAreaPosition(); 1000 } 1001 1002 QPointF PageView::contentAreaPoint(const QPointF pos) const 1003 { 1004 return pos + contentAreaPosition(); 1005 } 1006 1007 QString PageViewPrivate::selectedText() const 1008 { 1009 if (pagesWithTextSelection.isEmpty()) { 1010 return QString(); 1011 } 1012 1013 QString text; 1014 QList<int> selpages = pagesWithTextSelection.values(); 1015 std::sort(selpages.begin(), selpages.end()); 1016 const Okular::Page *pg = nullptr; 1017 if (selpages.count() == 1) { 1018 pg = document->page(selpages.first()); 1019 text.append(pg->text(pg->textSelection(), Okular::TextPage::CentralPixelTextAreaInclusionBehaviour)); 1020 } else { 1021 pg = document->page(selpages.first()); 1022 text.append(pg->text(pg->textSelection(), Okular::TextPage::CentralPixelTextAreaInclusionBehaviour)); 1023 int end = selpages.count() - 1; 1024 for (int i = 1; i < end; ++i) { 1025 pg = document->page(selpages.at(i)); 1026 text.append(pg->text(nullptr, Okular::TextPage::CentralPixelTextAreaInclusionBehaviour)); 1027 } 1028 pg = document->page(selpages.last()); 1029 text.append(pg->text(pg->textSelection(), Okular::TextPage::CentralPixelTextAreaInclusionBehaviour)); 1030 } 1031 1032 if (text.endsWith(QLatin1Char('\n'))) { 1033 text.chop(1); 1034 } 1035 return text; 1036 } 1037 1038 QMimeData *PageView::getTableContents() const 1039 { 1040 QString selText; 1041 QString selHtml; 1042 QList<double> xs = d->tableSelectionCols; 1043 QList<double> ys = d->tableSelectionRows; 1044 xs.prepend(0.0); 1045 xs.append(1.0); 1046 ys.prepend(0.0); 1047 ys.append(1.0); 1048 selHtml = QString::fromLatin1( 1049 "<html><head>" 1050 "<meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">" 1051 "</head><body><table>"); 1052 for (int r = 0; r + 1 < ys.length(); r++) { 1053 selHtml += QLatin1String("<tr>"); 1054 for (int c = 0; c + 1 < xs.length(); c++) { 1055 Okular::NormalizedRect cell(xs[c], ys[r], xs[c + 1], ys[r + 1]); 1056 if (c) { 1057 selText += QLatin1Char('\t'); 1058 } 1059 QString txt; 1060 for (const TableSelectionPart &tsp : std::as_const(d->tableSelectionParts)) { 1061 // first, crop the cell to this part 1062 if (!tsp.rectInSelection.intersects(cell)) { 1063 continue; 1064 } 1065 Okular::NormalizedRect cellPart = tsp.rectInSelection & cell; // intersection 1066 1067 // second, convert it from table coordinates to part coordinates 1068 cellPart.left -= tsp.rectInSelection.left; 1069 cellPart.left /= (tsp.rectInSelection.right - tsp.rectInSelection.left); 1070 cellPart.right -= tsp.rectInSelection.left; 1071 cellPart.right /= (tsp.rectInSelection.right - tsp.rectInSelection.left); 1072 cellPart.top -= tsp.rectInSelection.top; 1073 cellPart.top /= (tsp.rectInSelection.bottom - tsp.rectInSelection.top); 1074 cellPart.bottom -= tsp.rectInSelection.top; 1075 cellPart.bottom /= (tsp.rectInSelection.bottom - tsp.rectInSelection.top); 1076 1077 // third, convert from part coordinates to item coordinates 1078 cellPart.left *= (tsp.rectInItem.right - tsp.rectInItem.left); 1079 cellPart.left += tsp.rectInItem.left; 1080 cellPart.right *= (tsp.rectInItem.right - tsp.rectInItem.left); 1081 cellPart.right += tsp.rectInItem.left; 1082 cellPart.top *= (tsp.rectInItem.bottom - tsp.rectInItem.top); 1083 cellPart.top += tsp.rectInItem.top; 1084 cellPart.bottom *= (tsp.rectInItem.bottom - tsp.rectInItem.top); 1085 cellPart.bottom += tsp.rectInItem.top; 1086 1087 // now get the text 1088 Okular::RegularAreaRect rects; 1089 rects.append(cellPart); 1090 txt += tsp.item->page()->text(&rects, Okular::TextPage::CentralPixelTextAreaInclusionBehaviour); 1091 } 1092 QString html = txt; 1093 selText += txt.replace(QLatin1Char('\n'), QLatin1Char(' ')); 1094 html.replace(QLatin1Char('&'), QLatin1String("&")).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 continue; 3143 } 3144 3145 Okular::NormalizedRect wordArea = te.area(); 3146 3147 // convert it from item coordinates to part coordinates 3148 wordArea.left -= tsp.rectInItem.left; 3149 wordArea.left /= (tsp.rectInItem.right - tsp.rectInItem.left); 3150 wordArea.right -= tsp.rectInItem.left; 3151 wordArea.right /= (tsp.rectInItem.right - tsp.rectInItem.left); 3152 wordArea.top -= tsp.rectInItem.top; 3153 wordArea.top /= (tsp.rectInItem.bottom - tsp.rectInItem.top); 3154 wordArea.bottom -= tsp.rectInItem.top; 3155 wordArea.bottom /= (tsp.rectInItem.bottom - tsp.rectInItem.top); 3156 3157 // convert from part coordinates to table coordinates 3158 wordArea.left *= (tsp.rectInSelection.right - tsp.rectInSelection.left); 3159 wordArea.left += tsp.rectInSelection.left; 3160 wordArea.right *= (tsp.rectInSelection.right - tsp.rectInSelection.left); 3161 wordArea.right += tsp.rectInSelection.left; 3162 wordArea.top *= (tsp.rectInSelection.bottom - tsp.rectInSelection.top); 3163 wordArea.top += tsp.rectInSelection.top; 3164 wordArea.bottom *= (tsp.rectInSelection.bottom - tsp.rectInSelection.top); 3165 wordArea.bottom += tsp.rectInSelection.top; 3166 3167 // add to the ticks arrays... 3168 colTicks.append(qMakePair(wordArea.left, +1)); 3169 colTicks.append(qMakePair(wordArea.right, -1)); 3170 rowTicks.append(qMakePair(wordArea.top, +1)); 3171 rowTicks.append(qMakePair(wordArea.bottom, -1)); 3172 } 3173 } 3174 3175 int tally = 0; 3176 3177 std::sort(colSelectionTicks.begin(), colSelectionTicks.end()); 3178 std::sort(rowSelectionTicks.begin(), rowSelectionTicks.end()); 3179 3180 for (int i = 0; i < colSelectionTicks.length(); ++i) { 3181 tally += colSelectionTicks[i].second; 3182 if (tally == 0 && i + 1 < colSelectionTicks.length() && colSelectionTicks[i + 1].first != colSelectionTicks[i].first) { 3183 colTicks.append(qMakePair(colSelectionTicks[i].first, +1)); 3184 colTicks.append(qMakePair(colSelectionTicks[i + 1].first, -1)); 3185 } 3186 } 3187 Q_ASSERT(tally == 0); 3188 3189 for (int i = 0; i < rowSelectionTicks.length(); ++i) { 3190 tally += rowSelectionTicks[i].second; 3191 if (tally == 0 && i + 1 < rowSelectionTicks.length() && rowSelectionTicks[i + 1].first != rowSelectionTicks[i].first) { 3192 rowTicks.append(qMakePair(rowSelectionTicks[i].first, +1)); 3193 rowTicks.append(qMakePair(rowSelectionTicks[i + 1].first, -1)); 3194 } 3195 } 3196 Q_ASSERT(tally == 0); 3197 3198 std::sort(colTicks.begin(), colTicks.end()); 3199 std::sort(rowTicks.begin(), rowTicks.end()); 3200 3201 for (int i = 0; i < colTicks.length(); ++i) { 3202 tally += colTicks[i].second; 3203 if (tally == 0 && i + 1 < colTicks.length() && colTicks[i + 1].first != colTicks[i].first) { 3204 d->tableSelectionCols.append((colTicks[i].first + colTicks[i + 1].first) / 2); 3205 d->tableDividersGuessed = true; 3206 } 3207 } 3208 Q_ASSERT(tally == 0); 3209 3210 for (int i = 0; i < rowTicks.length(); ++i) { 3211 tally += rowTicks[i].second; 3212 if (tally == 0 && i + 1 < rowTicks.length() && rowTicks[i + 1].first != rowTicks[i].first) { 3213 d->tableSelectionRows.append((rowTicks[i].first + rowTicks[i + 1].first) / 2); 3214 d->tableDividersGuessed = true; 3215 } 3216 } 3217 Q_ASSERT(tally == 0); 3218 } 3219 3220 void PageView::mouseDoubleClickEvent(QMouseEvent *e) 3221 { 3222 if (e->button() == Qt::LeftButton) { 3223 const QPoint eventPos = contentAreaPoint(e->pos()); 3224 PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y()); 3225 if (pageItem) { 3226 // find out normalized mouse coords inside current item 3227 double nX = pageItem->absToPageX(eventPos.x()); 3228 double nY = pageItem->absToPageY(eventPos.y()); 3229 3230 if (d->mouseMode == Okular::Settings::EnumMouseMode::TextSelect) { 3231 textSelectionClear(); 3232 3233 Okular::RegularAreaRect *wordRect = pageItem->page()->wordAt(Okular::NormalizedPoint(nX, nY)); 3234 if (wordRect) { 3235 // TODO words with hyphens across pages 3236 d->document->setPageTextSelection(pageItem->pageNumber(), wordRect, palette().color(QPalette::Active, QPalette::Highlight)); 3237 d->pagesWithTextSelection << pageItem->pageNumber(); 3238 if (d->document->isAllowed(Okular::AllowCopy)) { 3239 const QString text = d->selectedText(); 3240 if (!text.isEmpty()) { 3241 QClipboard *cb = QApplication::clipboard(); 3242 if (cb->supportsSelection()) { 3243 cb->setText(text, QClipboard::Selection); 3244 } 3245 } 3246 } 3247 return; 3248 } 3249 } 3250 3251 const QRect &itemRect = pageItem->uncroppedGeometry(); 3252 Okular::Annotation *ann = nullptr; 3253 3254 const Okular::ObjectRect *orect = pageItem->page()->objectRect(Okular::ObjectRect::OAnnotation, nX, nY, itemRect.width(), itemRect.height()); 3255 if (orect) { 3256 ann = ((Okular::AnnotationObjectRect *)orect)->annotation(); 3257 } 3258 if (ann && ann->subType() != Okular::Annotation::AWidget) { 3259 openAnnotationWindow(ann, pageItem->pageNumber()); 3260 } 3261 } 3262 } 3263 } 3264 3265 void PageView::wheelEvent(QWheelEvent *e) 3266 { 3267 if (!d->document->isOpened()) { 3268 QAbstractScrollArea::wheelEvent(e); 3269 return; 3270 } 3271 3272 int delta = e->angleDelta().y(), vScroll = verticalScrollBar()->value(); 3273 e->accept(); 3274 if ((e->modifiers() & Qt::ControlModifier) == Qt::ControlModifier) { 3275 continuousZoom(delta); 3276 } else { 3277 if (delta <= -QWheelEvent::DefaultDeltasPerStep && !getContinuousMode() && vScroll == verticalScrollBar()->maximum()) { 3278 // go to next page 3279 if ((int)d->document->currentPage() < d->items.count() - 1) { 3280 // more optimized than document->setNextPage and then move view to top 3281 Okular::DocumentViewport newViewport = d->document->viewport(); 3282 newViewport.pageNumber += viewColumns(); 3283 if (newViewport.pageNumber >= (int)d->items.count()) { 3284 newViewport.pageNumber = d->items.count() - 1; 3285 } 3286 newViewport.rePos.enabled = true; 3287 newViewport.rePos.normalizedY = 0.0; 3288 d->document->setViewport(newViewport); 3289 d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); // sync scroller with scrollbar 3290 } 3291 } else if (delta >= QWheelEvent::DefaultDeltasPerStep && !getContinuousMode() && vScroll == verticalScrollBar()->minimum()) { 3292 // go to prev page 3293 if (d->document->currentPage() > 0) { 3294 // more optimized than document->setPrevPage and then move view to bottom 3295 Okular::DocumentViewport newViewport = d->document->viewport(); 3296 newViewport.pageNumber -= viewColumns(); 3297 if (newViewport.pageNumber < 0) { 3298 newViewport.pageNumber = 0; 3299 } 3300 newViewport.rePos.enabled = true; 3301 newViewport.rePos.normalizedY = 1.0; 3302 d->document->setViewport(newViewport); 3303 d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); // sync scroller with scrollbar 3304 } 3305 } else { 3306 // When the shift key is held down, scroll ten times faster 3307 int multiplier = e->modifiers() & Qt::ShiftModifier ? 10 : 1; 3308 3309 if (delta != 0 && delta % QWheelEvent::DefaultDeltasPerStep == 0) { 3310 // number of scroll wheel steps Qt gives to us at the same time 3311 int count = abs(delta / QWheelEvent::DefaultDeltasPerStep) * multiplier; 3312 if (delta < 0) { 3313 slotScrollDown(count); 3314 } else { 3315 slotScrollUp(count); 3316 } 3317 } else { 3318 d->scroller->scrollTo(d->scroller->finalPosition() - e->angleDelta() * multiplier, 0); 3319 } 3320 } 3321 } 3322 } 3323 3324 bool PageView::viewportEvent(QEvent *e) 3325 { 3326 if (e->type() == QEvent::ToolTip 3327 // Show tool tips only for those modes that change the cursor 3328 // to a hand when hovering over the link. 3329 && (d->mouseMode == Okular::Settings::EnumMouseMode::Browse || d->mouseMode == Okular::Settings::EnumMouseMode::RectSelect || d->mouseMode == Okular::Settings::EnumMouseMode::TextSelect || 3330 d->mouseMode == Okular::Settings::EnumMouseMode::TrimSelect)) { 3331 QHelpEvent *he = static_cast<QHelpEvent *>(e); 3332 if (d->mouseAnnotation->isMouseOver()) { 3333 d->mouseAnnotation->routeTooltipEvent(he); 3334 } else { 3335 const QPoint eventPos = contentAreaPoint(he->pos()); 3336 PageViewItem *pageItem = pickItemOnPoint(eventPos.x(), eventPos.y()); 3337 const Okular::ObjectRect *rect = nullptr; 3338 const Okular::Action *link = nullptr; 3339 if (pageItem) { 3340 double nX = pageItem->absToPageX(eventPos.x()); 3341 double nY = pageItem->absToPageY(eventPos.y()); 3342 rect = pageItem->page()->objectRect(Okular::ObjectRect::Action, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight()); 3343 if (rect) { 3344 link = static_cast<const Okular::Action *>(rect->object()); 3345 } 3346 } 3347 3348 if (link) { 3349 QRect r = rect->boundingRect(pageItem->uncroppedWidth(), pageItem->uncroppedHeight()); 3350 r.translate(pageItem->uncroppedGeometry().topLeft()); 3351 r.translate(-contentAreaPosition()); 3352 QString tip = link->actionTip(); 3353 if (!tip.isEmpty()) { 3354 QToolTip::showText(he->globalPos(), tip, viewport(), r); 3355 } 3356 } 3357 } 3358 e->accept(); 3359 return true; 3360 } else { 3361 // do not stop the event 3362 return QAbstractScrollArea::viewportEvent(e); 3363 } 3364 } 3365 3366 void PageView::scrollContentsBy(int dx, int dy) 3367 { 3368 const QRect r = viewport()->rect(); 3369 viewport()->scroll(dx, dy, r); 3370 // HACK manually repaint the damaged regions, as it seems some updates are missed 3371 // thus leaving artifacts around 3372 QRegion rgn(r); 3373 rgn -= rgn & r.translated(dx, dy); 3374 3375 for (const QRect &rect : rgn) { 3376 viewport()->update(rect); 3377 } 3378 3379 updateCursor(); 3380 } 3381 // END widget events 3382 3383 QList<Okular::RegularAreaRect *> PageView::textSelections(const QPoint start, const QPoint end, int &firstpage) 3384 { 3385 firstpage = -1; 3386 QList<Okular::RegularAreaRect *> ret; 3387 QSet<int> affectedItemsSet; 3388 QRect selectionRect = QRect(start, end).normalized(); 3389 for (const PageViewItem *item : std::as_const(d->items)) { 3390 if (item->isVisible() && selectionRect.intersects(item->croppedGeometry())) { 3391 affectedItemsSet.insert(item->pageNumber()); 3392 } 3393 } 3394 #ifdef PAGEVIEW_DEBUG 3395 qCDebug(OkularUiDebug) << ">>>> item selected by mouse:" << affectedItemsSet.count(); 3396 #endif 3397 3398 if (!affectedItemsSet.isEmpty()) { 3399 // is the mouse drag line the ne-sw diagonal of the selection rect? 3400 bool direction_ne_sw = start == selectionRect.topRight() || start == selectionRect.bottomLeft(); 3401 3402 int tmpmin = d->document->pages(); 3403 int tmpmax = 0; 3404 for (const int p : std::as_const(affectedItemsSet)) { 3405 if (p < tmpmin) { 3406 tmpmin = p; 3407 } 3408 if (p > tmpmax) { 3409 tmpmax = p; 3410 } 3411 } 3412 3413 PageViewItem *a = pickItemOnPoint((int)(direction_ne_sw ? selectionRect.right() : selectionRect.left()), (int)selectionRect.top()); 3414 int min = a && (a->pageNumber() != tmpmax) ? a->pageNumber() : tmpmin; 3415 PageViewItem *b = pickItemOnPoint((int)(direction_ne_sw ? selectionRect.left() : selectionRect.right()), (int)selectionRect.bottom()); 3416 int max = b && (b->pageNumber() != tmpmin) ? b->pageNumber() : tmpmax; 3417 3418 QList<int> affectedItemsIds; 3419 for (int i = min; i <= max; ++i) { 3420 affectedItemsIds.append(i); 3421 } 3422 #ifdef PAGEVIEW_DEBUG 3423 qCDebug(OkularUiDebug) << ">>>> pages:" << affectedItemsIds; 3424 #endif 3425 firstpage = affectedItemsIds.first(); 3426 3427 if (affectedItemsIds.count() == 1) { 3428 PageViewItem *item = d->items[affectedItemsIds.first()]; 3429 selectionRect.translate(-item->uncroppedGeometry().topLeft()); 3430 ret.append(textSelectionForItem(item, direction_ne_sw ? selectionRect.topRight() : selectionRect.topLeft(), direction_ne_sw ? selectionRect.bottomLeft() : selectionRect.bottomRight())); 3431 } else if (affectedItemsIds.count() > 1) { 3432 // first item 3433 PageViewItem *first = d->items[affectedItemsIds.first()]; 3434 QRect geom = first->croppedGeometry().intersected(selectionRect).translated(-first->uncroppedGeometry().topLeft()); 3435 ret.append(textSelectionForItem(first, selectionRect.bottom() > geom.height() ? (direction_ne_sw ? geom.topRight() : geom.topLeft()) : (direction_ne_sw ? geom.bottomRight() : geom.bottomLeft()), QPoint())); 3436 // last item 3437 PageViewItem *last = d->items[affectedItemsIds.last()]; 3438 geom = last->croppedGeometry().intersected(selectionRect).translated(-last->uncroppedGeometry().topLeft()); 3439 // the last item needs to appended at last... 3440 Okular::RegularAreaRect *lastArea = 3441 textSelectionForItem(last, QPoint(), selectionRect.bottom() > geom.height() ? (direction_ne_sw ? geom.bottomLeft() : geom.bottomRight()) : (direction_ne_sw ? geom.topLeft() : geom.topRight())); 3442 affectedItemsIds.removeFirst(); 3443 affectedItemsIds.removeLast(); 3444 // item between the two above 3445 for (const int page : std::as_const(affectedItemsIds)) { 3446 ret.append(textSelectionForItem(d->items[page])); 3447 } 3448 ret.append(lastArea); 3449 } 3450 } 3451 return ret; 3452 } 3453 3454 void PageView::drawDocumentOnPainter(const QRect contentsRect, QPainter *p) 3455 { 3456 QColor backColor; 3457 3458 if (Okular::Settings::useCustomBackgroundColor()) { 3459 backColor = Okular::Settings::backgroundColor(); 3460 } else { 3461 backColor = viewport()->palette().color(QPalette::Dark); 3462 } 3463 3464 // create a region from which we'll subtract painted rects 3465 QRegion remainingArea(contentsRect); 3466 3467 // This loop draws the actual pages 3468 // iterate over all items painting the ones intersecting contentsRect 3469 for (const PageViewItem *item : std::as_const(d->items)) { 3470 // check if a piece of the page intersects the contents rect 3471 if (!item->isVisible() || !item->croppedGeometry().intersects(contentsRect)) { 3472 continue; 3473 } 3474 3475 // get item and item's outline geometries 3476 QRect itemGeometry = item->croppedGeometry(); 3477 3478 // move the painter to the top-left corner of the real page 3479 p->save(); 3480 p->translate(itemGeometry.left(), itemGeometry.top()); 3481 3482 // draw the page using the PagePainter with all flags active 3483 if (contentsRect.intersects(itemGeometry)) { 3484 Okular::NormalizedPoint *viewPortPoint = nullptr; 3485 Okular::NormalizedPoint point(d->lastSourceLocationViewportNormalizedX, d->lastSourceLocationViewportNormalizedY); 3486 if (Okular::Settings::showSourceLocationsGraphically() && item->pageNumber() == d->lastSourceLocationViewportPageNumber) { 3487 viewPortPoint = &point; 3488 } 3489 QRect pixmapRect = contentsRect.intersected(itemGeometry); 3490 pixmapRect.translate(-item->croppedGeometry().topLeft()); 3491 PagePainter::paintCroppedPageOnPainter(p, item->page(), this, pageflags, item->uncroppedWidth(), item->uncroppedHeight(), pixmapRect, item->crop(), viewPortPoint); 3492 } 3493 3494 // remove painted area from 'remainingArea' and restore painter 3495 remainingArea -= itemGeometry; 3496 p->restore(); 3497 } 3498 3499 // fill the visible area around the page with the background color 3500 for (const QRect &backRect : remainingArea) { 3501 p->fillRect(backRect, backColor); 3502 } 3503 3504 // take outline and shadow into account when testing whether a repaint is necessary 3505 auto dpr = devicePixelRatioF(); 3506 QRect checkRect = contentsRect; 3507 checkRect.adjust(-3, -3, 1, 1); 3508 3509 // Method to linearly interpolate between black (=(0,0,0), omitted) and the background color 3510 auto interpolateColor = [&backColor](double t) { return QColor(t * backColor.red(), t * backColor.green(), t * backColor.blue()); }; 3511 3512 // width of the shadow in device pixels 3513 static const int shadowWidth = 2 * dpr; 3514 3515 // iterate over all items painting a black outline and a simple bottom/right gradient 3516 for (const PageViewItem *item : std::as_const(d->items)) { 3517 // check if a piece of the page intersects the contents rect 3518 if (!item->isVisible() || !item->croppedGeometry().intersects(checkRect)) { 3519 continue; 3520 } 3521 3522 // get item and item's outline geometries 3523 QRect itemGeometry = item->croppedGeometry(); 3524 3525 // move the painter to the top-left corner of the real page 3526 p->save(); 3527 p->translate(itemGeometry.left(), itemGeometry.top()); 3528 3529 // draw the page outline (black border and bottom-right shadow) 3530 if (!itemGeometry.contains(contentsRect)) { 3531 int itemWidth = itemGeometry.width(); 3532 int itemHeight = itemGeometry.height(); 3533 // draw simple outline 3534 QPen pen(Qt::black); 3535 pen.setWidth(0); 3536 p->setPen(pen); 3537 3538 QRectF outline(-1.0 / dpr, -1.0 / dpr, itemWidth + 1.0 / dpr, itemHeight + 1.0 / dpr); 3539 p->drawRect(outline); 3540 3541 // draw bottom/right gradient 3542 for (int i = 1; i <= shadowWidth; i++) { 3543 pen.setColor(interpolateColor(double(i) / (shadowWidth + 1))); 3544 p->setPen(pen); 3545 QPointF left((i - 1) / dpr, itemHeight + i / dpr); 3546 QPointF up(itemWidth + i / dpr, (i - 1) / dpr); 3547 QPointF corner(itemWidth + i / dpr, itemHeight + i / dpr); 3548 p->drawLine(left, corner); 3549 p->drawLine(up, corner); 3550 } 3551 } 3552 3553 p->restore(); 3554 } 3555 } 3556 3557 void PageView::updateItemSize(PageViewItem *item, int colWidth, int rowHeight) 3558 { 3559 const Okular::Page *okularPage = item->page(); 3560 double width = okularPage->width(), height = okularPage->height(), zoom = d->zoomFactor; 3561 Okular::NormalizedRect crop(0., 0., 1., 1.); 3562 3563 // Handle cropping, due to either "Trim Margin" or "Trim to Selection" cases 3564 if ((Okular::Settings::trimMargins() && okularPage->isBoundingBoxKnown() && !okularPage->boundingBox().isNull()) || (d->aTrimToSelection && d->aTrimToSelection->isChecked() && !d->trimBoundingBox.isNull())) { 3565 crop = Okular::Settings::trimMargins() ? okularPage->boundingBox() : d->trimBoundingBox; 3566 3567 // Rotate the bounding box 3568 for (int i = okularPage->rotation(); i > 0; --i) { 3569 Okular::NormalizedRect rot = crop; 3570 crop.left = 1 - rot.bottom; 3571 crop.top = rot.left; 3572 crop.right = 1 - rot.top; 3573 crop.bottom = rot.right; 3574 } 3575 3576 // Expand the crop slightly beyond the bounding box (for Trim Margins only) 3577 if (Okular::Settings::trimMargins()) { 3578 static const double cropExpandRatio = 0.04; 3579 const double cropExpand = cropExpandRatio * ((crop.right - crop.left) + (crop.bottom - crop.top)) / 2; 3580 crop = Okular::NormalizedRect(crop.left - cropExpand, crop.top - cropExpand, crop.right + cropExpand, crop.bottom + cropExpand) & Okular::NormalizedRect(0, 0, 1, 1); 3581 } 3582 3583 // We currently generate a larger image and then crop it, so if the 3584 // crop rect is very small the generated image is huge. Hence, we shouldn't 3585 // let the crop rect become too small. 3586 static double minCropRatio; 3587 if (Okular::Settings::trimMargins()) { 3588 // Make sure we crop by at most 50% in either dimension: 3589 minCropRatio = 0.5; 3590 } else { 3591 // Looser Constraint for "Trim Selection" 3592 minCropRatio = 0.20; 3593 } 3594 if ((crop.right - crop.left) < minCropRatio) { 3595 const double newLeft = (crop.left + crop.right) / 2 - minCropRatio / 2; 3596 crop.left = qMax(0.0, qMin(1.0 - minCropRatio, newLeft)); 3597 crop.right = crop.left + minCropRatio; 3598 } 3599 if ((crop.bottom - crop.top) < minCropRatio) { 3600 const double newTop = (crop.top + crop.bottom) / 2 - minCropRatio / 2; 3601 crop.top = qMax(0.0, qMin(1.0 - minCropRatio, newTop)); 3602 crop.bottom = crop.top + minCropRatio; 3603 } 3604 3605 width *= (crop.right - crop.left); 3606 height *= (crop.bottom - crop.top); 3607 #ifdef PAGEVIEW_DEBUG 3608 qCDebug(OkularUiDebug) << "Cropped page" << okularPage->number() << "to" << crop << "width" << width << "height" << height << "by bbox" << okularPage->boundingBox(); 3609 #endif 3610 } 3611 3612 if (d->zoomMode == ZoomFixed) { 3613 width *= zoom; 3614 height *= zoom; 3615 item->setWHZC((int)width, (int)height, d->zoomFactor, crop); 3616 } else if (d->zoomMode == ZoomFitWidth) { 3617 height = (height / width) * colWidth; 3618 zoom = (double)colWidth / width; 3619 item->setWHZC(colWidth, (int)height, zoom, crop); 3620 if ((uint)item->pageNumber() == d->document->currentPage()) { 3621 d->zoomFactor = zoom; 3622 } 3623 } else if (d->zoomMode == ZoomFitPage) { 3624 const double scaleW = (double)colWidth / (double)width; 3625 const double scaleH = (double)rowHeight / (double)height; 3626 zoom = qMin(scaleW, scaleH); 3627 item->setWHZC((int)(zoom * width), (int)(zoom * height), zoom, crop); 3628 if ((uint)item->pageNumber() == d->document->currentPage()) { 3629 d->zoomFactor = zoom; 3630 } 3631 } else if (d->zoomMode == ZoomFitAuto) { 3632 const double aspectRatioRelation = 1.25; // relation between aspect ratios for "auto fit" 3633 const double uiAspect = (double)rowHeight / (double)colWidth; 3634 const double pageAspect = (double)height / (double)width; 3635 const double rel = uiAspect / pageAspect; 3636 3637 if (!getContinuousMode() && rel > aspectRatioRelation) { 3638 // UI space is relatively much higher than the page 3639 zoom = (double)rowHeight / (double)height; 3640 } else if (rel < 1.0 / aspectRatioRelation) { 3641 // UI space is relatively much wider than the page in relation 3642 zoom = (double)colWidth / (double)width; 3643 } else { 3644 // aspect ratios of page and UI space are very similar 3645 const double scaleW = (double)colWidth / (double)width; 3646 const double scaleH = (double)rowHeight / (double)height; 3647 zoom = qMin(scaleW, scaleH); 3648 } 3649 item->setWHZC((int)(zoom * width), (int)(zoom * height), zoom, crop); 3650 if ((uint)item->pageNumber() == d->document->currentPage()) { 3651 d->zoomFactor = zoom; 3652 } 3653 } 3654 #ifndef NDEBUG 3655 else { 3656 qCDebug(OkularUiDebug) << "calling updateItemSize with unrecognized d->zoomMode!"; 3657 } 3658 #endif 3659 } 3660 3661 PageViewItem *PageView::pickItemOnPoint(int x, int y) 3662 { 3663 PageViewItem *item = nullptr; 3664 for (PageViewItem *i : std::as_const(d->visibleItems)) { 3665 const QRect &r = i->croppedGeometry(); 3666 if (x < r.right() && x > r.left() && y < r.bottom()) { 3667 if (y > r.top()) { 3668 item = i; 3669 } 3670 break; 3671 } 3672 } 3673 return item; 3674 } 3675 3676 void PageView::textSelectionClear() 3677 { 3678 // something to clear 3679 if (!d->pagesWithTextSelection.isEmpty()) { 3680 for (const int page : std::as_const(d->pagesWithTextSelection)) { 3681 d->document->setPageTextSelection(page, nullptr, QColor()); 3682 } 3683 d->pagesWithTextSelection.clear(); 3684 } 3685 } 3686 3687 void PageView::selectionStart(const QPoint pos, const QColor &color, bool /*aboveAll*/) 3688 { 3689 selectionClear(); 3690 d->mouseSelecting = true; 3691 d->mouseSelectionRect.setRect(pos.x(), pos.y(), 1, 1); 3692 d->mouseSelectionColor = color; 3693 // ensures page doesn't scroll 3694 if (d->autoScrollTimer) { 3695 d->scrollIncrement = 0; 3696 d->autoScrollTimer->stop(); 3697 } 3698 } 3699 3700 void PageView::scrollPosIntoView(const QPoint pos) 3701 { 3702 // this number slows the speed of the page by its value, chosen not to be too fast or too slow, the actual speed is determined from the mouse position, not critical 3703 const int damping = 6; 3704 3705 if (pos.x() < horizontalScrollBar()->value()) { 3706 d->dragScrollVector.setX((pos.x() - horizontalScrollBar()->value()) / damping); 3707 } else if (horizontalScrollBar()->value() + viewport()->width() < pos.x()) { 3708 d->dragScrollVector.setX((pos.x() - horizontalScrollBar()->value() - viewport()->width()) / damping); 3709 } else { 3710 d->dragScrollVector.setX(0); 3711 } 3712 3713 if (pos.y() < verticalScrollBar()->value()) { 3714 d->dragScrollVector.setY((pos.y() - verticalScrollBar()->value()) / damping); 3715 } else if (verticalScrollBar()->value() + viewport()->height() < pos.y()) { 3716 d->dragScrollVector.setY((pos.y() - verticalScrollBar()->value() - viewport()->height()) / damping); 3717 } else { 3718 d->dragScrollVector.setY(0); 3719 } 3720 3721 if (d->dragScrollVector != QPoint(0, 0)) { 3722 if (!d->dragScrollTimer.isActive()) { 3723 d->dragScrollTimer.start(1000 / 60); // 60 fps 3724 } 3725 } else { 3726 d->dragScrollTimer.stop(); 3727 } 3728 } 3729 3730 QPoint PageView::viewportToContentArea(const Okular::DocumentViewport &vp) const 3731 { 3732 Q_ASSERT(vp.pageNumber >= 0); 3733 3734 const QRect &r = d->items[vp.pageNumber]->croppedGeometry(); 3735 QPoint c {r.left(), r.top()}; 3736 3737 if (vp.rePos.enabled) { 3738 // Convert the coordinates of vp to normalized coordinates on the cropped page. 3739 // This is a no-op if the page isn't cropped. 3740 const Okular::NormalizedRect &crop = d->items[vp.pageNumber]->crop(); 3741 const double normalized_on_crop_x = (vp.rePos.normalizedX - crop.left) / (crop.right - crop.left); 3742 const double normalized_on_crop_y = (vp.rePos.normalizedY - crop.top) / (crop.bottom - crop.top); 3743 3744 if (vp.rePos.pos == Okular::DocumentViewport::Center) { 3745 c.rx() += qRound(normClamp(normalized_on_crop_x, 0.5) * (double)r.width()); 3746 c.ry() += qRound(normClamp(normalized_on_crop_y, 0.0) * (double)r.height()); 3747 } else { 3748 // TopLeft 3749 c.rx() += qRound(normClamp(normalized_on_crop_x, 0.0) * (double)r.width() + viewport()->width() / 2.0); 3750 c.ry() += qRound(normClamp(normalized_on_crop_y, 0.0) * (double)r.height() + viewport()->height() / 2.0); 3751 } 3752 } else { 3753 // exact repositioning disabled, align page top margin with viewport top border by default 3754 c.rx() += r.width() / 2; 3755 c.ry() += viewport()->height() / 2 - 10; 3756 } 3757 return c; 3758 } 3759 3760 void PageView::updateSelection(const QPoint pos) 3761 { 3762 if (d->mouseSelecting) { 3763 scrollPosIntoView(pos); 3764 // update the selection rect 3765 QRect updateRect = d->mouseSelectionRect; 3766 d->mouseSelectionRect.setBottomLeft(pos); 3767 updateRect |= d->mouseSelectionRect; 3768 updateRect.translate(-contentAreaPosition()); 3769 viewport()->update(updateRect.adjusted(-1, -2, 2, 1)); 3770 } else if (d->mouseTextSelecting) { 3771 scrollPosIntoView(pos); 3772 int first = -1; 3773 const QList<Okular::RegularAreaRect *> selections = textSelections(pos, d->mouseSelectPos.toPoint(), first); 3774 QSet<int> pagesWithSelectionSet; 3775 for (int i = 0; i < selections.count(); ++i) { 3776 pagesWithSelectionSet.insert(i + first); 3777 } 3778 3779 const QSet<int> noMoreSelectedPages = d->pagesWithTextSelection - pagesWithSelectionSet; 3780 // clear the selection from pages not selected anymore 3781 for (int p : noMoreSelectedPages) { 3782 d->document->setPageTextSelection(p, nullptr, QColor()); 3783 } 3784 // set the new selection for the selected pages 3785 for (int p : std::as_const(pagesWithSelectionSet)) { 3786 d->document->setPageTextSelection(p, selections[p - first], palette().color(QPalette::Active, QPalette::Highlight)); 3787 } 3788 d->pagesWithTextSelection = pagesWithSelectionSet; 3789 } 3790 } 3791 3792 static Okular::NormalizedPoint rotateInNormRect(const QPoint rotated, const QRect rect, Okular::Rotation rotation) 3793 { 3794 Okular::NormalizedPoint ret; 3795 3796 switch (rotation) { 3797 case Okular::Rotation0: 3798 ret = Okular::NormalizedPoint(rotated.x(), rotated.y(), rect.width(), rect.height()); 3799 break; 3800 case Okular::Rotation90: 3801 ret = Okular::NormalizedPoint(rotated.y(), rect.width() - rotated.x(), rect.height(), rect.width()); 3802 break; 3803 case Okular::Rotation180: 3804 ret = Okular::NormalizedPoint(rect.width() - rotated.x(), rect.height() - rotated.y(), rect.width(), rect.height()); 3805 break; 3806 case Okular::Rotation270: 3807 ret = Okular::NormalizedPoint(rect.height() - rotated.y(), rotated.x(), rect.height(), rect.width()); 3808 break; 3809 } 3810 3811 return ret; 3812 } 3813 3814 Okular::RegularAreaRect *PageView::textSelectionForItem(const PageViewItem *item, const QPoint startPoint, const QPoint endPoint) 3815 { 3816 const QRect &geometry = item->uncroppedGeometry(); 3817 Okular::NormalizedPoint startCursor(0.0, 0.0); 3818 if (!startPoint.isNull()) { 3819 startCursor = rotateInNormRect(startPoint, geometry, item->page()->rotation()); 3820 } 3821 Okular::NormalizedPoint endCursor(1.0, 1.0); 3822 if (!endPoint.isNull()) { 3823 endCursor = rotateInNormRect(endPoint, geometry, item->page()->rotation()); 3824 } 3825 Okular::TextSelection mouseTextSelectionInfo(startCursor, endCursor); 3826 3827 const Okular::Page *okularPage = item->page(); 3828 3829 if (!okularPage->hasTextPage()) { 3830 d->document->requestTextPage(okularPage->number()); 3831 } 3832 3833 Okular::RegularAreaRect *selectionArea = okularPage->textArea(&mouseTextSelectionInfo); 3834 #ifdef PAGEVIEW_DEBUG 3835 qCDebug(OkularUiDebug).nospace() << "text areas (" << okularPage->number() << "): " << (selectionArea ? QString::number(selectionArea->count()) : "(none)"); 3836 #endif 3837 return selectionArea; 3838 } 3839 3840 void PageView::selectionClear(const ClearMode mode) 3841 { 3842 QRect updatedRect = d->mouseSelectionRect.normalized().adjusted(-2, -2, 2, 2); 3843 d->mouseSelecting = false; 3844 d->mouseSelectionRect.setCoords(0, 0, 0, 0); 3845 d->tableSelectionCols.clear(); 3846 d->tableSelectionRows.clear(); 3847 d->tableDividersGuessed = false; 3848 for (const TableSelectionPart &tsp : std::as_const(d->tableSelectionParts)) { 3849 QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight()); 3850 selectionPartRect.translate(tsp.item->uncroppedGeometry().topLeft()); 3851 // should check whether this is on-screen here? 3852 updatedRect = updatedRect.united(selectionPartRect); 3853 } 3854 if (mode != ClearOnlyDividers) { 3855 d->tableSelectionParts.clear(); 3856 } 3857 d->tableSelectionParts.clear(); 3858 updatedRect.translate(-contentAreaPosition()); 3859 viewport()->update(updatedRect); 3860 } 3861 3862 // const to be used for both zoomFactorFitMode function and slotRelayoutPages. 3863 static const int kcolWidthMargin = 6; 3864 static const int krowHeightMargin = 12; 3865 3866 double PageView::zoomFactorFitMode(ZoomMode mode) 3867 { 3868 const int pageCount = d->items.count(); 3869 if (pageCount == 0) { 3870 return 0; 3871 } 3872 const bool facingCentered = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered || (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount == 1); 3873 const bool overrideCentering = facingCentered && pageCount < 3; 3874 const int nCols = overrideCentering ? 1 : viewColumns(); 3875 const int colWidth = viewport()->width() / nCols - kcolWidthMargin; 3876 const double rowHeight = viewport()->height() - krowHeightMargin; 3877 const PageViewItem *currentItem = d->items[qMax(0, (int)d->document->currentPage())]; 3878 // prevent segmentation fault when opening a new document; 3879 if (!currentItem) { 3880 return 0; 3881 } 3882 3883 // We need the real width/height of the cropped page. 3884 const Okular::Page *okularPage = currentItem->page(); 3885 const double width = okularPage->width() * currentItem->crop().width(); 3886 const double height = okularPage->height() * currentItem->crop().height(); 3887 3888 if (mode == ZoomFitWidth) { 3889 return (double)colWidth / width; 3890 } 3891 if (mode == ZoomFitPage) { 3892 const double scaleW = (double)colWidth / (double)width; 3893 const double scaleH = (double)rowHeight / (double)height; 3894 return qMin(scaleW, scaleH); 3895 } 3896 return 0; 3897 } 3898 3899 static double parseZoomString(QString z) 3900 { 3901 // kdelibs4 sometimes adds accelerators to actions' text directly :( 3902 z.remove(QLatin1Char('&')); 3903 z.remove(QLatin1Char('%')); 3904 return QLocale().toDouble(z) / 100.0; 3905 } 3906 3907 static QString makePrettyZoomString(double value) 3908 { 3909 // we do not need to display 2-digit precision 3910 QString localValue(QLocale().toString(value * 100.0, 'f', 1)); 3911 localValue.remove(QLocale().decimalPoint() + QLatin1Char('0')); 3912 // remove a trailing zero in numbers like 66.70 3913 if (localValue.right(1) == QLatin1String("0") && localValue.indexOf(QLocale().decimalPoint()) > -1) { 3914 localValue.chop(1); 3915 } 3916 return localValue; 3917 } 3918 3919 void PageView::updateZoom(ZoomMode newZoomMode) 3920 { 3921 if (newZoomMode == ZoomFixed) { 3922 if (d->aZoom->currentItem() == 0) { 3923 newZoomMode = ZoomFitWidth; 3924 } else if (d->aZoom->currentItem() == 1) { 3925 newZoomMode = ZoomFitPage; 3926 } else if (d->aZoom->currentItem() == 2) { 3927 newZoomMode = ZoomFitAuto; 3928 } 3929 } 3930 3931 float newFactor = d->zoomFactor; 3932 QAction *checkedZoomAction = nullptr; 3933 switch (newZoomMode) { 3934 case ZoomFixed: { // ZoomFixed case 3935 newFactor = parseZoomString(d->aZoom->currentText()); 3936 } break; 3937 case ZoomIn: 3938 case ZoomOut: { 3939 const float zoomFactorFitWidth = zoomFactorFitMode(ZoomFitWidth); 3940 const float zoomFactorFitPage = zoomFactorFitMode(ZoomFitPage); 3941 3942 QVector<float> zoomValue(kZoomValues.size()); 3943 3944 std::copy(kZoomValues.begin(), kZoomValues.end(), zoomValue.begin()); 3945 zoomValue.append(zoomFactorFitWidth); 3946 zoomValue.append(zoomFactorFitPage); 3947 std::sort(zoomValue.begin(), zoomValue.end()); 3948 3949 QVector<float>::iterator i; 3950 if (newZoomMode == ZoomOut) { 3951 if (newFactor <= zoomValue.first()) { 3952 return; 3953 } 3954 i = std::lower_bound(zoomValue.begin(), zoomValue.end(), newFactor) - 1; 3955 } else { 3956 if (newFactor >= zoomValue.last()) { 3957 return; 3958 } 3959 i = std::upper_bound(zoomValue.begin(), zoomValue.end(), newFactor); 3960 } 3961 const float tmpFactor = *i; 3962 if (tmpFactor == zoomFactorFitWidth) { 3963 newZoomMode = ZoomFitWidth; 3964 checkedZoomAction = d->aZoomFitWidth; 3965 } else if (tmpFactor == zoomFactorFitPage) { 3966 newZoomMode = ZoomFitPage; 3967 checkedZoomAction = d->aZoomFitPage; 3968 } else { 3969 newFactor = tmpFactor; 3970 newZoomMode = ZoomFixed; 3971 } 3972 } break; 3973 case ZoomActual: 3974 newZoomMode = ZoomFixed; 3975 newFactor = 1.0; 3976 break; 3977 case ZoomFitWidth: 3978 checkedZoomAction = d->aZoomFitWidth; 3979 break; 3980 case ZoomFitPage: 3981 checkedZoomAction = d->aZoomFitPage; 3982 break; 3983 case ZoomFitAuto: 3984 checkedZoomAction = d->aZoomAutoFit; 3985 break; 3986 case ZoomRefreshCurrent: 3987 newZoomMode = ZoomFixed; 3988 d->zoomFactor = -1; 3989 break; 3990 } 3991 const float upperZoomLimit = d->document->supportsTiles() ? 100.0 : 4.0; 3992 if (newFactor > upperZoomLimit) { 3993 newFactor = upperZoomLimit; 3994 } 3995 if (newFactor < kZoomValues[0]) { 3996 newFactor = kZoomValues[0]; 3997 } 3998 3999 if (newZoomMode != d->zoomMode || (newZoomMode == ZoomFixed && newFactor != d->zoomFactor)) { 4000 // rebuild layout and update the whole viewport 4001 d->zoomMode = newZoomMode; 4002 d->zoomFactor = newFactor; 4003 // be sure to block updates to document's viewport 4004 bool prevState = d->blockViewport; 4005 d->blockViewport = true; 4006 slotRelayoutPages(); 4007 d->blockViewport = prevState; 4008 // request pixmaps 4009 slotRequestVisiblePixmaps(); 4010 // update zoom text 4011 updateZoomText(); 4012 // update actions checked state 4013 if (d->aZoomFitWidth) { 4014 d->aZoomFitWidth->setChecked(checkedZoomAction == d->aZoomFitWidth); 4015 d->aZoomFitPage->setChecked(checkedZoomAction == d->aZoomFitPage); 4016 d->aZoomAutoFit->setChecked(checkedZoomAction == d->aZoomAutoFit); 4017 } 4018 } else if (newZoomMode == ZoomFixed && newFactor == d->zoomFactor) { 4019 updateZoomText(); 4020 } 4021 4022 updateZoomActionsEnabledStatus(); 4023 } 4024 4025 void PageView::updateZoomActionsEnabledStatus() 4026 { 4027 const float upperZoomLimit = d->document->supportsTiles() ? kZoomValues.back() : 4.0; 4028 const bool hasPages = d->document && d->document->pages() > 0; 4029 4030 if (d->aZoomFitWidth) { 4031 d->aZoomFitWidth->setEnabled(hasPages); 4032 } 4033 if (d->aZoomFitPage) { 4034 d->aZoomFitPage->setEnabled(hasPages); 4035 } 4036 if (d->aZoomAutoFit) { 4037 d->aZoomAutoFit->setEnabled(hasPages); 4038 } 4039 if (d->aZoom) { 4040 d->aZoom->selectableActionGroup()->setEnabled(hasPages); 4041 d->aZoom->setEnabled(hasPages); 4042 } 4043 if (d->aZoomIn) { 4044 d->aZoomIn->setEnabled(hasPages && d->zoomFactor < upperZoomLimit - 0.001); 4045 } 4046 if (d->aZoomOut) { 4047 d->aZoomOut->setEnabled(hasPages && d->zoomFactor > (kZoomValues[0] + 0.001)); 4048 } 4049 if (d->aZoomActual) { 4050 d->aZoomActual->setEnabled(hasPages && d->zoomFactor != 1.0); 4051 } 4052 } 4053 4054 void PageView::updateZoomText() 4055 { 4056 // use current page zoom as zoomFactor if in ZoomFit/* mode 4057 if (d->zoomMode != ZoomFixed && d->items.count() > 0) { 4058 d->zoomFactor = d->items[qMax(0, (int)d->document->currentPage())]->zoomFactor(); 4059 } 4060 float newFactor = d->zoomFactor; 4061 d->aZoom->removeAllActions(); 4062 4063 // add items that describe fit actions 4064 QStringList translated; 4065 translated << i18n("Fit Width") << i18n("Fit Page") << i18n("Auto Fit"); 4066 4067 // add percent items 4068 int idx = 0, selIdx = 3; 4069 bool inserted = false; // use: "d->zoomMode != ZoomFixed" to hide Fit/* zoom ratio 4070 int zoomValueCount = 11; 4071 if (d->document->supportsTiles()) { 4072 zoomValueCount = kZoomValues.size(); 4073 } 4074 while (idx < zoomValueCount || !inserted) { 4075 float value = idx < zoomValueCount ? kZoomValues[idx] : newFactor; 4076 if (!inserted && newFactor < (value - 0.0001)) { 4077 value = newFactor; 4078 } else { 4079 idx++; 4080 } 4081 if (value > (newFactor - 0.0001) && value < (newFactor + 0.0001)) { 4082 inserted = true; 4083 } 4084 if (!inserted) { 4085 selIdx++; 4086 } 4087 const QString localizedValue = makePrettyZoomString(value); 4088 const QString i18nZoomName = i18nc("Zoom percentage value %1 will be replaced by the actual zoom factor value, so make sure you include it in your translation in order to not to break anything", "%1%", localizedValue); 4089 if (makePrettyZoomString(parseZoomString(i18nZoomName)) == localizedValue) { 4090 translated << i18nZoomName; 4091 } else { 4092 qWarning() << "Wrong translation of zoom percentage. Please file a bug"; 4093 translated << QStringLiteral("%1%").arg(localizedValue); 4094 } 4095 } 4096 d->aZoom->setItems(translated); 4097 4098 // select current item in list 4099 if (d->zoomMode == ZoomFitWidth) { 4100 selIdx = 0; 4101 } else if (d->zoomMode == ZoomFitPage) { 4102 selIdx = 1; 4103 } else if (d->zoomMode == ZoomFitAuto) { 4104 selIdx = 2; 4105 } 4106 // we have to temporarily enable the actions as otherwise we can't set a new current item 4107 d->aZoom->setEnabled(true); 4108 d->aZoom->selectableActionGroup()->setEnabled(true); 4109 d->aZoom->setCurrentItem(selIdx); 4110 d->aZoom->setEnabled(d->items.size() > 0); 4111 d->aZoom->selectableActionGroup()->setEnabled(d->items.size() > 0); 4112 } 4113 4114 void PageView::updateViewMode(const int nr) 4115 { 4116 const QList<QAction *> actions = d->viewModeActionGroup->actions(); 4117 for (QAction *action : actions) { 4118 QVariant mode_id = action->data(); 4119 if (mode_id.toInt() == nr) { 4120 action->trigger(); 4121 } 4122 } 4123 } 4124 4125 void PageView::updateCursor() 4126 { 4127 const QPoint p = contentAreaPosition() + viewport()->mapFromGlobal(QCursor::pos()); 4128 updateCursor(p); 4129 } 4130 4131 void PageView::updateCursor(const QPoint p) 4132 { 4133 // reset mouse over link it will be re-set if that still valid 4134 d->mouseOverLinkObject = nullptr; 4135 4136 // detect the underlaying page (if present) 4137 PageViewItem *pageItem = pickItemOnPoint(p.x(), p.y()); 4138 QScroller::State scrollerState = d->scroller->state(); 4139 4140 if (d->annotator && d->annotator->active()) { 4141 if (pageItem || d->annotator->annotating()) { 4142 setCursor(d->annotator->cursor()); 4143 } else { 4144 setCursor(Qt::ForbiddenCursor); 4145 } 4146 } else if (scrollerState == QScroller::Pressed || scrollerState == QScroller::Dragging) { 4147 setCursor(Qt::ClosedHandCursor); 4148 } else if (pageItem) { 4149 double nX = pageItem->absToPageX(p.x()); 4150 double nY = pageItem->absToPageY(p.y()); 4151 Qt::CursorShape cursorShapeFallback; 4152 4153 // if over a ObjectRect (of type Link) change cursor to hand 4154 switch (d->mouseMode) { 4155 case Okular::Settings::EnumMouseMode::TextSelect: 4156 if (d->mouseTextSelecting) { 4157 setCursor(Qt::IBeamCursor); 4158 return; 4159 } 4160 cursorShapeFallback = Qt::IBeamCursor; 4161 break; 4162 case Okular::Settings::EnumMouseMode::Magnifier: 4163 setCursor(Qt::CrossCursor); 4164 return; 4165 case Okular::Settings::EnumMouseMode::RectSelect: 4166 case Okular::Settings::EnumMouseMode::TrimSelect: 4167 if (d->mouseSelecting) { 4168 setCursor(Qt::CrossCursor); 4169 return; 4170 } 4171 cursorShapeFallback = Qt::CrossCursor; 4172 break; 4173 case Okular::Settings::EnumMouseMode::Browse: 4174 d->mouseOnRect = false; 4175 if (d->mouseAnnotation->isMouseOver()) { 4176 d->mouseOnRect = true; 4177 setCursor(d->mouseAnnotation->cursor()); 4178 return; 4179 } else { 4180 cursorShapeFallback = Qt::OpenHandCursor; 4181 } 4182 break; 4183 default: 4184 setCursor(Qt::ArrowCursor); 4185 return; 4186 } 4187 4188 const Okular::ObjectRect *linkobj = pageItem->page()->objectRect(Okular::ObjectRect::Action, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight()); 4189 if (linkobj) { 4190 d->mouseOverLinkObject = linkobj; 4191 d->mouseOnRect = true; 4192 setCursor(Qt::PointingHandCursor); 4193 } else { 4194 setCursor(cursorShapeFallback); 4195 } 4196 } else { 4197 // if there's no page over the cursor and we were showing the pointingHandCursor 4198 // go back to the normal one 4199 d->mouseOnRect = false; 4200 setCursor(Qt::ArrowCursor); 4201 } 4202 } 4203 4204 void PageView::reloadForms() 4205 { 4206 if (d->m_formsVisible) { 4207 for (PageViewItem *item : std::as_const(d->visibleItems)) { 4208 item->reloadFormWidgetsState(); 4209 } 4210 } 4211 } 4212 4213 void PageView::moveMagnifier(const QPoint p) // non scaled point 4214 { 4215 const int w = d->magnifierView->width() * 0.5; 4216 const int h = d->magnifierView->height() * 0.5; 4217 4218 int x = p.x() - w; 4219 int y = p.y() - h; 4220 4221 const int max_x = viewport()->width(); 4222 const int max_y = viewport()->height(); 4223 4224 QPoint scroll(0, 0); 4225 4226 if (x < 0) { 4227 if (horizontalScrollBar()->value() > 0) { 4228 scroll.setX(x - w); 4229 } 4230 x = 0; 4231 } 4232 4233 if (y < 0) { 4234 if (verticalScrollBar()->value() > 0) { 4235 scroll.setY(y - h); 4236 } 4237 y = 0; 4238 } 4239 4240 if (p.x() + w > max_x) { 4241 if (horizontalScrollBar()->value() < horizontalScrollBar()->maximum()) { 4242 scroll.setX(p.x() + 2 * w - max_x); 4243 } 4244 x = max_x - d->magnifierView->width() - 1; 4245 } 4246 4247 if (p.y() + h > max_y) { 4248 if (verticalScrollBar()->value() < verticalScrollBar()->maximum()) { 4249 scroll.setY(p.y() + 2 * h - max_y); 4250 } 4251 y = max_y - d->magnifierView->height() - 1; 4252 } 4253 4254 if (!scroll.isNull()) { 4255 scrollPosIntoView(contentAreaPoint(p + scroll)); 4256 } 4257 4258 d->magnifierView->move(x, y); 4259 } 4260 4261 void PageView::updateMagnifier(const QPoint p) // scaled point 4262 { 4263 /* translate mouse coordinates to page coordinates and inform the magnifier of the situation */ 4264 PageViewItem *item = pickItemOnPoint(p.x(), p.y()); 4265 if (item) { 4266 Okular::NormalizedPoint np(item->absToPageX(p.x()), item->absToPageY(p.y())); 4267 d->magnifierView->updateView(np, item->page()); 4268 } 4269 } 4270 4271 int PageView::viewColumns() const 4272 { 4273 int vm = Okular::Settings::viewMode(); 4274 if (vm == Okular::Settings::EnumViewMode::Single) { 4275 return 1; 4276 } else if (vm == Okular::Settings::EnumViewMode::Facing || vm == Okular::Settings::EnumViewMode::FacingFirstCentered) { 4277 return 2; 4278 } else if (vm == Okular::Settings::EnumViewMode::Summary && d->document->pages() < Okular::Settings::viewColumns()) { 4279 return d->document->pages(); 4280 } else { 4281 return Okular::Settings::viewColumns(); 4282 } 4283 } 4284 4285 void PageView::center(int cx, int cy, bool smoothMove) 4286 { 4287 scrollTo(cx - viewport()->width() / 2, cy - viewport()->height() / 2, smoothMove); 4288 } 4289 4290 void PageView::scrollTo(int x, int y, bool smoothMove) 4291 { 4292 bool prevState = d->blockPixmapsRequest; 4293 4294 int newValue = -1; 4295 if (x != horizontalScrollBar()->value() || y != verticalScrollBar()->value()) { 4296 newValue = 1; // Pretend this call is the result of a scrollbar event 4297 } 4298 4299 d->blockPixmapsRequest = true; 4300 4301 if (smoothMove) { 4302 d->scroller->scrollTo(QPoint(x, y), d->currentLongScrollDuration); 4303 } else { 4304 d->scroller->scrollTo(QPoint(x, y), 0); 4305 } 4306 4307 d->blockPixmapsRequest = prevState; 4308 4309 slotRequestVisiblePixmaps(newValue); 4310 } 4311 4312 void PageView::toggleFormWidgets(bool on) 4313 { 4314 bool somehadfocus = false; 4315 for (PageViewItem *item : std::as_const(d->items)) { 4316 const bool hadfocus = item->setFormWidgetsVisible(on); 4317 somehadfocus = somehadfocus || hadfocus; 4318 } 4319 if (somehadfocus) { 4320 setFocus(); 4321 } 4322 d->m_formsVisible = on; 4323 } 4324 4325 void PageView::resizeContentArea(const QSize newSize) 4326 { 4327 const QSize vs = viewport()->size(); 4328 int hRange = newSize.width() - vs.width(); 4329 int vRange = newSize.height() - vs.height(); 4330 if (horizontalScrollBar()->isVisible() && hRange == verticalScrollBar()->width() && verticalScrollBar()->isVisible() && vRange == horizontalScrollBar()->height() && Okular::Settings::showScrollBars()) { 4331 hRange = 0; 4332 vRange = 0; 4333 } 4334 horizontalScrollBar()->setRange(0, hRange); 4335 verticalScrollBar()->setRange(0, vRange); 4336 updatePageStep(); 4337 } 4338 4339 void PageView::updatePageStep() 4340 { 4341 const QSize vs = viewport()->size(); 4342 horizontalScrollBar()->setPageStep(vs.width()); 4343 verticalScrollBar()->setPageStep(vs.height() * (100 - Okular::Settings::scrollOverlap()) / 100); 4344 } 4345 4346 void PageView::addWebShortcutsMenu(QMenu *menu, const QString &text) 4347 { 4348 if (text.isEmpty()) { 4349 return; 4350 } 4351 4352 QString searchText = text; 4353 searchText = searchText.replace(QLatin1Char('\n'), QLatin1Char(' ')).replace(QLatin1Char('\r'), QLatin1Char(' ')).simplified(); 4354 4355 if (searchText.isEmpty()) { 4356 return; 4357 } 4358 4359 KUriFilterData filterData(searchText); 4360 4361 filterData.setSearchFilteringOptions(KUriFilterData::RetrievePreferredSearchProvidersOnly); 4362 4363 if (KUriFilter::self()->filterSearchUri(filterData, KUriFilter::NormalTextFilter)) { 4364 const QStringList searchProviders = filterData.preferredSearchProviders(); 4365 4366 if (!searchProviders.isEmpty()) { 4367 QMenu *webShortcutsMenu = new QMenu(menu); 4368 webShortcutsMenu->setIcon(QIcon::fromTheme(QStringLiteral("preferences-web-browser-shortcuts"))); 4369 4370 const QString squeezedText = KStringHandler::rsqueeze(searchText, searchTextPreviewLength); 4371 webShortcutsMenu->setTitle(i18n("Search for '%1' with", squeezedText)); 4372 4373 QAction *action = nullptr; 4374 4375 for (const QString &searchProvider : searchProviders) { 4376 action = new QAction(searchProvider, webShortcutsMenu); 4377 action->setIcon(QIcon::fromTheme(filterData.iconNameForPreferredSearchProvider(searchProvider))); 4378 action->setData(filterData.queryForPreferredSearchProvider(searchProvider)); 4379 connect(action, &QAction::triggered, this, &PageView::slotHandleWebShortcutAction); 4380 webShortcutsMenu->addAction(action); 4381 } 4382 4383 webShortcutsMenu->addSeparator(); 4384 4385 action = new QAction(i18n("Configure Web Shortcuts..."), webShortcutsMenu); 4386 action->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); 4387 connect(action, &QAction::triggered, this, &PageView::slotConfigureWebShortcuts); 4388 webShortcutsMenu->addAction(action); 4389 4390 menu->addMenu(webShortcutsMenu); 4391 } 4392 } 4393 } 4394 4395 QMenu *PageView::createProcessLinkMenu(PageViewItem *item, const QPoint eventPos) 4396 { 4397 // check if the right-click was over a link 4398 const double nX = item->absToPageX(eventPos.x()); 4399 const double nY = item->absToPageY(eventPos.y()); 4400 const Okular::ObjectRect *rect = item->page()->objectRect(Okular::ObjectRect::Action, nX, nY, item->uncroppedWidth(), item->uncroppedHeight()); 4401 if (rect) { 4402 const Okular::Action *link = static_cast<const Okular::Action *>(rect->object()); 4403 4404 if (!link) { 4405 return nullptr; 4406 } 4407 4408 QMenu *menu = new QMenu(this); 4409 4410 // creating the menu and its actions 4411 QAction *processLink = menu->addAction(i18n("Follow This Link")); 4412 processLink->setObjectName(QStringLiteral("ProcessLinkAction")); 4413 if (link->actionType() == Okular::Action::Sound) { 4414 processLink->setText(i18n("Play this Sound")); 4415 if (Okular::AudioPlayer::instance()->state() == Okular::AudioPlayer::PlayingState) { 4416 QAction *actStopSound = menu->addAction(i18n("Stop Sound")); 4417 connect(actStopSound, &QAction::triggered, []() { Okular::AudioPlayer::instance()->stopPlaybacks(); }); 4418 } 4419 } 4420 4421 if (dynamic_cast<const Okular::BrowseAction *>(link)) { 4422 QAction *actCopyLinkLocation = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy Link Address")); 4423 actCopyLinkLocation->setObjectName(QStringLiteral("CopyLinkLocationAction")); 4424 connect(actCopyLinkLocation, &QAction::triggered, menu, [link]() { 4425 const Okular::BrowseAction *browseLink = static_cast<const Okular::BrowseAction *>(link); 4426 QClipboard *cb = QApplication::clipboard(); 4427 cb->setText(browseLink->url().toDisplayString(), QClipboard::Clipboard); 4428 if (cb->supportsSelection()) { 4429 cb->setText(browseLink->url().toDisplayString(), QClipboard::Selection); 4430 } 4431 }); 4432 } 4433 4434 connect(processLink, &QAction::triggered, this, [this, link]() { d->document->processAction(link); }); 4435 return menu; 4436 } 4437 return nullptr; 4438 } 4439 4440 void PageView::addSearchWithinDocumentAction(QMenu *menu, const QString &searchText) 4441 { 4442 const QString squeezedText = KStringHandler::rsqueeze(searchText, searchTextPreviewLength); 4443 QAction *action = new QAction(i18n("Search for '%1' in this document", squeezedText.simplified()), menu); 4444 action->setIcon(QIcon::fromTheme(QStringLiteral("document-preview"))); 4445 connect(action, &QAction::triggered, this, [this, searchText] { Q_EMIT triggerSearch(searchText); }); 4446 menu->addAction(action); 4447 } 4448 4449 void PageView::updateSmoothScrollAnimationSpeed() 4450 { 4451 // If it's turned off in Okular's own settings, don't bother to look at the 4452 // global settings 4453 if (!Okular::Settings::smoothScrolling()) { 4454 d->currentShortScrollDuration = 0; 4455 d->currentLongScrollDuration = 0; 4456 return; 4457 } 4458 4459 // If we are using smooth scrolling, scale the speed of the animated 4460 // transitions according to the global animation speed setting 4461 KConfigGroup kdeglobalsConfig = KConfigGroup(KSharedConfig::openConfig(), QStringLiteral("KDE")); 4462 const qreal globalAnimationScale = qMax(0.0, kdeglobalsConfig.readEntry("AnimationDurationFactor", 1.0)); 4463 d->currentShortScrollDuration = d->baseShortScrollDuration * globalAnimationScale; 4464 d->currentLongScrollDuration = d->baseLongScrollDuration * globalAnimationScale; 4465 } 4466 4467 bool PageView::getContinuousMode() const 4468 { 4469 return d->aViewContinuous ? d->aViewContinuous->isChecked() : Okular::Settings::viewContinuous(); 4470 } 4471 4472 void PageView::zoomWithFixedCenter(PageView::ZoomMode newZoomMode, QPointF zoomCenter, float newZoom) 4473 { 4474 const Okular::DocumentViewport &vp = d->document->viewport(); 4475 Q_ASSERT(vp.pageNumber >= 0); 4476 4477 // determine the page below zoom center 4478 const QPoint contentPos = contentAreaPoint(zoomCenter.toPoint()); 4479 const PageViewItem *page = pickItemOnPoint(contentPos.x(), contentPos.y()); 4480 const int hScrollBarMaximum = horizontalScrollBar()->maximum(); 4481 const int vScrollBarMaximum = verticalScrollBar()->maximum(); 4482 4483 // if the zoom center is not over a page, use viewport page number 4484 if (!page) { 4485 page = d->items[vp.pageNumber]; 4486 } 4487 4488 const QRect beginGeometry = page->croppedGeometry(); 4489 4490 QPoint offset {beginGeometry.left(), beginGeometry.top()}; 4491 4492 const QPointF oldScroll = contentAreaPosition() - offset; 4493 4494 d->blockPixmapsRequest = true; 4495 if (newZoom) { 4496 d->zoomFactor = newZoom; 4497 } 4498 4499 updateZoom(newZoomMode); 4500 d->blockPixmapsRequest = false; 4501 4502 const QRect afterGeometry = page->croppedGeometry(); 4503 const double vpZoomY = (double)afterGeometry.height() / (double)beginGeometry.height(); 4504 const double vpZoomX = (double)afterGeometry.width() / (double)beginGeometry.width(); 4505 4506 QPointF newScroll; 4507 // The calculation for newScroll is taken from Gwenview class Abstractimageview::setZoom 4508 newScroll.setY(vpZoomY * (oldScroll.y() + zoomCenter.y()) - (zoomCenter.y())); 4509 newScroll.setX(vpZoomX * (oldScroll.x() + zoomCenter.x()) - (zoomCenter.x())); 4510 4511 // add the remaining scroll from the previous zoom event 4512 newScroll.setY(newScroll.y() + d->remainingScroll.y() * vpZoomY); 4513 newScroll.setX(newScroll.x() + d->remainingScroll.x() * vpZoomX); 4514 4515 // adjust newScroll to the new margins after zooming 4516 offset = QPoint {afterGeometry.left(), afterGeometry.top()}; 4517 newScroll += offset; 4518 4519 // adjust newScroll for appear and disappear of the scrollbars 4520 if (Okular::Settings::showScrollBars()) { 4521 if (hScrollBarMaximum == 0 && horizontalScrollBar()->maximum() > 0) { 4522 newScroll.setY(newScroll.y() - (horizontalScrollBar()->height() / 2.0)); 4523 } 4524 4525 if (hScrollBarMaximum > 0 && horizontalScrollBar()->maximum() == 0) { 4526 newScroll.setY(newScroll.y() + (horizontalScrollBar()->height() / 2.0)); 4527 } 4528 4529 if (vScrollBarMaximum == 0 && verticalScrollBar()->maximum() > 0) { 4530 newScroll.setX(newScroll.x() - (verticalScrollBar()->width() / 2.0)); 4531 } 4532 4533 if (vScrollBarMaximum > 0 && verticalScrollBar()->maximum() == 0) { 4534 newScroll.setX(newScroll.x() + (verticalScrollBar()->width() / 2.0)); 4535 } 4536 } 4537 4538 const int newScrollX = std::round(newScroll.x()); 4539 const int newScrollY = std::round(newScroll.y()); 4540 scrollTo(newScrollX, newScrollY, false); 4541 4542 viewport()->setUpdatesEnabled(true); 4543 viewport()->update(); 4544 4545 // test if target scroll position was reached, if not save 4546 // the difference in d->remainingScroll for later use 4547 const QPointF diffF = newScroll - contentAreaPosition(); 4548 if (abs(diffF.x()) < 0.5 && abs(diffF.y()) < 0.5) { 4549 // scroll target reached set d->remainingScroll to 0.0 4550 d->remainingScroll = QPointF(0.0, 0.0); 4551 } else { 4552 d->remainingScroll = diffF; 4553 } 4554 } 4555 4556 // BEGIN private SLOTS 4557 void PageView::slotRelayoutPages() 4558 // called by: notifySetup, viewportResizeEvent, slotViewMode, slotContinuousToggled, updateZoom 4559 { 4560 // set an empty container if we have no pages 4561 const int pageCount = d->items.count(); 4562 if (pageCount < 1) { 4563 return; 4564 } 4565 4566 int viewportWidth = viewport()->width(), viewportHeight = viewport()->height(), fullWidth = 0, fullHeight = 0; 4567 4568 // handle the 'center first page in row' stuff 4569 const bool facing = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount > 1; 4570 const bool facingCentered = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered || (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount == 1); 4571 const bool overrideCentering = facingCentered && pageCount < 3; 4572 const bool centerFirstPage = facingCentered && !overrideCentering; 4573 const bool facingPages = facing || centerFirstPage; 4574 const bool centerLastPage = centerFirstPage && pageCount % 2 == 0; 4575 const bool continuousView = getContinuousMode(); 4576 const int nCols = overrideCentering ? 1 : viewColumns(); 4577 const bool singlePageViewMode = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Single; 4578 4579 if (d->aFitWindowToPage) { 4580 d->aFitWindowToPage->setEnabled(!continuousView && singlePageViewMode); 4581 } 4582 4583 // set all items geometry and resize contents. handle 'continuous' and 'single' modes separately 4584 4585 PageViewItem *currentItem = d->items[qMax(0, (int)d->document->currentPage())]; 4586 4587 // Here we find out column's width and row's height to compute a table 4588 // so we can place widgets 'centered in virtual cells'. 4589 const int nRows = (int)ceil((float)(centerFirstPage ? (pageCount + nCols - 1) : pageCount) / (float)nCols); 4590 4591 int *colWidth = new int[nCols], *rowHeight = new int[nRows], cIdx = 0, rIdx = 0; 4592 for (int i = 0; i < nCols; i++) { 4593 colWidth[i] = viewportWidth / nCols; 4594 } 4595 for (int i = 0; i < nRows; i++) { 4596 rowHeight[i] = 0; 4597 } 4598 // handle the 'centering on first row' stuff 4599 if (centerFirstPage) { 4600 cIdx += nCols - 1; 4601 } 4602 4603 // 1) find the maximum columns width and rows height for a grid in 4604 // which each page must well-fit inside a cell 4605 for (PageViewItem *item : std::as_const(d->items)) { 4606 // update internal page size (leaving a little margin in case of Fit* modes) 4607 updateItemSize(item, colWidth[cIdx] - kcolWidthMargin, viewportHeight - krowHeightMargin); 4608 // find row's maximum height and column's max width 4609 if (item->croppedWidth() + kcolWidthMargin > colWidth[cIdx]) { 4610 colWidth[cIdx] = item->croppedWidth() + kcolWidthMargin; 4611 } 4612 if (item->croppedHeight() + krowHeightMargin > rowHeight[rIdx]) { 4613 rowHeight[rIdx] = item->croppedHeight() + krowHeightMargin; 4614 } 4615 // handle the 'centering on first row' stuff 4616 // update col/row indices 4617 if (++cIdx == nCols) { 4618 cIdx = 0; 4619 rIdx++; 4620 } 4621 } 4622 4623 const int pageRowIdx = ((centerFirstPage ? nCols - 1 : 0) + currentItem->pageNumber()) / nCols; 4624 4625 // 2) compute full size 4626 for (int i = 0; i < nCols; i++) { 4627 fullWidth += colWidth[i]; 4628 } 4629 if (continuousView) { 4630 for (int i = 0; i < nRows; i++) { 4631 fullHeight += rowHeight[i]; 4632 } 4633 } else { 4634 fullHeight = rowHeight[pageRowIdx]; 4635 } 4636 4637 // 3) arrange widgets inside cells (and refine fullHeight if needed) 4638 int insertX = 0, insertY = fullHeight < viewportHeight ? (viewportHeight - fullHeight) / 2 : 0; 4639 const int origInsertY = insertY; 4640 cIdx = 0; 4641 rIdx = 0; 4642 if (centerFirstPage) { 4643 cIdx += nCols - 1; 4644 for (int i = 0; i < cIdx; ++i) { 4645 insertX += colWidth[i]; 4646 } 4647 } 4648 for (PageViewItem *item : std::as_const(d->items)) { 4649 int cWidth = colWidth[cIdx], rHeight = rowHeight[rIdx]; 4650 if (continuousView || rIdx == pageRowIdx) { 4651 const bool reallyDoCenterFirst = item->pageNumber() == 0 && centerFirstPage; 4652 const bool reallyDoCenterLast = item->pageNumber() == pageCount - 1 && centerLastPage; 4653 int actualX = 0; 4654 if (reallyDoCenterFirst || reallyDoCenterLast) { 4655 // page is centered across entire viewport 4656 actualX = (fullWidth - item->croppedWidth()) / 2; 4657 } else if (facingPages) { 4658 if (Okular::Settings::rtlReadingDirection()) { 4659 // RTL reading mode 4660 actualX = ((centerFirstPage && item->pageNumber() % 2 == 0) || (!centerFirstPage && item->pageNumber() % 2 == 1)) ? (fullWidth / 2) - item->croppedWidth() - 1 : (fullWidth / 2) + 1; 4661 } else { 4662 // page edges 'touch' the center of the viewport 4663 actualX = ((centerFirstPage && item->pageNumber() % 2 == 1) || (!centerFirstPage && item->pageNumber() % 2 == 0)) ? (fullWidth / 2) - item->croppedWidth() - 1 : (fullWidth / 2) + 1; 4664 } 4665 } else { 4666 // page is centered within its virtual column 4667 // actualX = insertX + (cWidth - item->croppedWidth()) / 2; 4668 if (Okular::Settings::rtlReadingDirection()) { 4669 actualX = fullWidth - insertX - cWidth + ((cWidth - item->croppedWidth()) / 2); 4670 } else { 4671 actualX = insertX + (cWidth - item->croppedWidth()) / 2; 4672 } 4673 } 4674 item->moveTo(actualX, (continuousView ? insertY : origInsertY) + (rHeight - item->croppedHeight()) / 2); 4675 item->setVisible(true); 4676 } else { 4677 item->moveTo(0, 0); 4678 item->setVisible(false); 4679 } 4680 item->setFormWidgetsVisible(d->m_formsVisible); 4681 // advance col/row index 4682 insertX += cWidth; 4683 if (++cIdx == nCols) { 4684 cIdx = 0; 4685 rIdx++; 4686 insertX = 0; 4687 insertY += rHeight; 4688 } 4689 #ifdef PAGEVIEW_DEBUG 4690 qWarning() << "updating size for pageno" << item->pageNumber() << "cropped" << item->croppedGeometry() << "uncropped" << item->uncroppedGeometry(); 4691 #endif 4692 } 4693 4694 delete[] colWidth; 4695 delete[] rowHeight; 4696 4697 // 3) reset dirty state 4698 d->dirtyLayout = false; 4699 4700 // 4) update scrollview's contents size and recenter view 4701 bool wasUpdatesEnabled = viewport()->updatesEnabled(); 4702 if (fullWidth != contentAreaWidth() || fullHeight != contentAreaHeight()) { 4703 const Okular::DocumentViewport vp = d->document->viewport(); 4704 // disable updates and resize the viewportContents 4705 if (wasUpdatesEnabled) { 4706 viewport()->setUpdatesEnabled(false); 4707 } 4708 resizeContentArea(QSize(fullWidth, fullHeight)); 4709 // restore previous viewport if defined and updates enabled 4710 if (wasUpdatesEnabled && !d->pinchZoomActive) { 4711 if (vp.pageNumber >= 0) { 4712 int prevX = horizontalScrollBar()->value(), prevY = verticalScrollBar()->value(); 4713 4714 const QPoint centerPos = viewportToContentArea(vp); 4715 center(centerPos.x(), centerPos.y()); 4716 4717 // center() usually moves the viewport, that requests pixmaps too. 4718 // if that doesn't happen we have to request them by hand 4719 if (prevX == horizontalScrollBar()->value() && prevY == verticalScrollBar()->value()) { 4720 slotRequestVisiblePixmaps(); 4721 } 4722 } 4723 // or else go to center page 4724 else { 4725 center(fullWidth / 2, 0); 4726 } 4727 viewport()->setUpdatesEnabled(true); 4728 } 4729 } else { 4730 slotRequestVisiblePixmaps(); 4731 } 4732 4733 // 5) update the whole viewport if updated enabled 4734 if (wasUpdatesEnabled && !d->pinchZoomActive) { 4735 viewport()->update(); 4736 } 4737 } 4738 4739 void PageView::delayedResizeEvent() 4740 { 4741 // If we already got here we don't need to execute the timer slot again 4742 d->delayResizeEventTimer->stop(); 4743 slotRelayoutPages(); 4744 slotRequestVisiblePixmaps(); 4745 } 4746 4747 static void slotRequestPreloadPixmap(PageView *pageView, const PageViewItem *i, const QRect expandedViewportRect, QList<Okular::PixmapRequest *> *requestedPixmaps) 4748 { 4749 Okular::NormalizedRect preRenderRegion; 4750 const QRect intersectionRect = expandedViewportRect.intersected(i->croppedGeometry()); 4751 if (!intersectionRect.isEmpty()) { 4752 preRenderRegion = Okular::NormalizedRect(intersectionRect.translated(-i->uncroppedGeometry().topLeft()), i->uncroppedWidth(), i->uncroppedHeight()); 4753 } 4754 4755 // request the pixmap if not already present 4756 if (!i->page()->hasPixmap(pageView, i->uncroppedWidth(), i->uncroppedHeight(), preRenderRegion) && i->uncroppedWidth() > 0) { 4757 Okular::PixmapRequest::PixmapRequestFeatures requestFeatures = Okular::PixmapRequest::Preload; 4758 requestFeatures |= Okular::PixmapRequest::Asynchronous; 4759 const bool pageHasTilesManager = i->page()->hasTilesManager(pageView); 4760 if (pageHasTilesManager && !preRenderRegion.isNull()) { 4761 Okular::PixmapRequest *p = new Okular::PixmapRequest(pageView, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), pageView->devicePixelRatioF(), PAGEVIEW_PRELOAD_PRIO, requestFeatures); 4762 requestedPixmaps->push_back(p); 4763 4764 p->setNormalizedRect(preRenderRegion); 4765 p->setTile(true); 4766 } else if (!pageHasTilesManager) { 4767 Okular::PixmapRequest *p = new Okular::PixmapRequest(pageView, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), pageView->devicePixelRatioF(), PAGEVIEW_PRELOAD_PRIO, requestFeatures); 4768 requestedPixmaps->push_back(p); 4769 p->setNormalizedRect(preRenderRegion); 4770 } 4771 } 4772 } 4773 4774 void PageView::slotRequestVisiblePixmaps(int newValue) 4775 { 4776 // if requests are blocked (because raised by an unwanted event), exit 4777 if (d->blockPixmapsRequest) { 4778 return; 4779 } 4780 4781 // precalc view limits for intersecting with page coords inside the loop 4782 const bool isEvent = newValue != -1 && !d->blockViewport; 4783 const QRectF viewportRect(horizontalScrollBar()->value(), verticalScrollBar()->value(), viewport()->width(), viewport()->height()); 4784 const QRectF viewportRectAtZeroZero(0, 0, viewport()->width(), viewport()->height()); 4785 4786 // some variables used to determine the viewport 4787 int nearPageNumber = -1; 4788 const double viewportCenterX = (viewportRect.left() + viewportRect.right()) / 2.0; 4789 const double viewportCenterY = (viewportRect.top() + viewportRect.bottom()) / 2.0; 4790 double focusedX = 0.5, focusedY = 0.0, minDistance = -1.0; 4791 // Margin (in pixels) around the viewport to preload 4792 const int pixelsToExpand = 512; 4793 4794 // iterate over all items 4795 d->visibleItems.clear(); 4796 QList<Okular::PixmapRequest *> requestedPixmaps; 4797 QVector<Okular::VisiblePageRect *> visibleRects; 4798 for (PageViewItem *i : std::as_const(d->items)) { 4799 const QSet<FormWidgetIface *> formWidgetsList = i->formWidgets(); 4800 for (FormWidgetIface *fwi : formWidgetsList) { 4801 Okular::NormalizedRect r = fwi->rect(); 4802 fwi->moveTo(qRound(i->uncroppedGeometry().left() + i->uncroppedWidth() * r.left) + 1 - viewportRect.left(), qRound(i->uncroppedGeometry().top() + i->uncroppedHeight() * r.top) + 1 - viewportRect.top()); 4803 } 4804 const QHash<Okular::Movie *, VideoWidget *> videoWidgets = i->videoWidgets(); 4805 for (VideoWidget *vw : videoWidgets) { 4806 const Okular::NormalizedRect r = vw->normGeometry(); 4807 vw->move(qRound(i->uncroppedGeometry().left() + i->uncroppedWidth() * r.left) + 1 - viewportRect.left(), qRound(i->uncroppedGeometry().top() + i->uncroppedHeight() * r.top) + 1 - viewportRect.top()); 4808 4809 if (vw->isPlaying() && viewportRectAtZeroZero.intersected(vw->geometry()).isEmpty()) { 4810 vw->stop(); 4811 vw->pageLeft(); 4812 } 4813 } 4814 4815 if (!i->isVisible()) { 4816 continue; 4817 } 4818 #ifdef PAGEVIEW_DEBUG 4819 qWarning() << "checking page" << i->pageNumber(); 4820 qWarning().nospace() << "viewportRect is " << viewportRect << ", page item is " << i->croppedGeometry() << " intersect : " << viewportRect.intersects(i->croppedGeometry()); 4821 #endif 4822 // if the item doesn't intersect the viewport, skip it 4823 QRectF intersectionRect = viewportRect.intersected(i->croppedGeometry()); 4824 if (intersectionRect.isEmpty()) { 4825 continue; 4826 } 4827 4828 // add the item to the 'visible list' 4829 d->visibleItems.push_back(i); 4830 4831 intersectionRect.translate(-i->uncroppedGeometry().topLeft()); 4832 const Okular::NormalizedRect normRect(intersectionRect.left() / i->uncroppedWidth(), intersectionRect.top() / i->uncroppedHeight(), intersectionRect.right() / i->uncroppedWidth(), intersectionRect.bottom() / i->uncroppedHeight()); 4833 4834 Okular::VisiblePageRect *vItem = new Okular::VisiblePageRect(i->pageNumber(), normRect); 4835 visibleRects.push_back(vItem); 4836 #ifdef PAGEVIEW_DEBUG 4837 qWarning() << "checking for pixmap for page" << i->pageNumber() << "=" << i->page()->hasPixmap(this, i->uncroppedWidth(), i->uncroppedHeight()); 4838 qWarning() << "checking for text for page" << i->pageNumber() << "=" << i->page()->hasTextPage(); 4839 #endif 4840 4841 Okular::NormalizedRect expandedVisibleRect = vItem->rect; 4842 if (i->page()->hasTilesManager(this) && Okular::Settings::memoryLevel() != Okular::Settings::EnumMemoryLevel::Low) { 4843 double rectMargin = pixelsToExpand / (double)i->uncroppedHeight(); 4844 expandedVisibleRect.left = qMax(0.0, vItem->rect.left - rectMargin); 4845 expandedVisibleRect.top = qMax(0.0, vItem->rect.top - rectMargin); 4846 expandedVisibleRect.right = qMin(1.0, vItem->rect.right + rectMargin); 4847 expandedVisibleRect.bottom = qMin(1.0, vItem->rect.bottom + rectMargin); 4848 } 4849 4850 // if the item has not the right pixmap, add a request for it 4851 if (!i->page()->hasPixmap(this, i->uncroppedWidth(), i->uncroppedHeight(), expandedVisibleRect)) { 4852 #ifdef PAGEVIEW_DEBUG 4853 qWarning() << "rerequesting visible pixmaps for page" << i->pageNumber() << "!"; 4854 #endif 4855 Okular::PixmapRequest *p = new Okular::PixmapRequest(this, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), devicePixelRatioF(), PAGEVIEW_PRIO, Okular::PixmapRequest::Asynchronous); 4856 requestedPixmaps.push_back(p); 4857 4858 if (i->page()->hasTilesManager(this)) { 4859 p->setNormalizedRect(expandedVisibleRect); 4860 p->setTile(true); 4861 } else { 4862 p->setNormalizedRect(vItem->rect); 4863 } 4864 } 4865 4866 // look for the item closest to viewport center and the relative 4867 // position between the item and the viewport center 4868 if (isEvent) { 4869 const QRect &geometry = i->croppedGeometry(); 4870 // compute distance between item center and viewport center (slightly moved left) 4871 const double distance = hypot((geometry.left() + geometry.right()) / 2.0 - (viewportCenterX - 4), (geometry.top() + geometry.bottom()) / 2.0 - viewportCenterY); 4872 if (distance >= minDistance && nearPageNumber != -1) { 4873 continue; 4874 } 4875 nearPageNumber = i->pageNumber(); 4876 minDistance = distance; 4877 if (geometry.height() > 0 && geometry.width() > 0) { 4878 // Compute normalized coordinates w.r.t. cropped page 4879 focusedX = (viewportCenterX - (double)geometry.left()) / (double)geometry.width(); 4880 focusedY = (viewportCenterY - (double)geometry.top()) / (double)geometry.height(); 4881 // Convert to normalized coordinates w.r.t. full page (no-op if not cropped) 4882 focusedX = i->crop().left + focusedX * i->crop().width(); 4883 focusedY = i->crop().top + focusedY * i->crop().height(); 4884 } 4885 } 4886 } 4887 4888 // if preloading is enabled, add the pages before and after in preloading 4889 if (!d->visibleItems.isEmpty() && Okular::SettingsCore::memoryLevel() != Okular::SettingsCore::EnumMemoryLevel::Low) { 4890 // as the requests are done in the order as they appear in the list, 4891 // request first the next page and then the previous 4892 4893 int pagesToPreload = viewColumns(); 4894 4895 // if the greedy option is set, preload all pages 4896 if (Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Greedy) { 4897 pagesToPreload = d->items.count(); 4898 } 4899 4900 const QRectF adjustedViewportRect = viewportRect.adjusted(0, -pixelsToExpand, 0, pixelsToExpand); 4901 const QRect expandedViewportRect(adjustedViewportRect.x(), adjustedViewportRect.y(), adjustedViewportRect.width(), adjustedViewportRect.height()); 4902 4903 for (int j = 1; j <= pagesToPreload; j++) { 4904 // add the page after the 'visible series' in preload 4905 const int tailRequest = d->visibleItems.last()->pageNumber() + j; 4906 if (tailRequest < (int)d->items.count()) { 4907 slotRequestPreloadPixmap(this, d->items[tailRequest], expandedViewportRect, &requestedPixmaps); 4908 } 4909 4910 // add the page before the 'visible series' in preload 4911 const int headRequest = d->visibleItems.first()->pageNumber() - j; 4912 if (headRequest >= 0) { 4913 slotRequestPreloadPixmap(this, d->items[headRequest], expandedViewportRect, &requestedPixmaps); 4914 } 4915 4916 // stop if we've already reached both ends of the document 4917 if (headRequest < 0 && tailRequest >= (int)d->items.count()) { 4918 break; 4919 } 4920 } 4921 } 4922 4923 // send requests to the document 4924 if (!requestedPixmaps.isEmpty()) { 4925 d->document->requestPixmaps(requestedPixmaps); 4926 } 4927 // if this functions was invoked by viewport events, send update to document 4928 if (isEvent && nearPageNumber != -1) { 4929 // determine the document viewport 4930 Okular::DocumentViewport newViewport(nearPageNumber); 4931 newViewport.rePos.enabled = true; 4932 newViewport.rePos.normalizedX = focusedX; 4933 newViewport.rePos.normalizedY = focusedY; 4934 // set the viewport to other observers 4935 // do not update history if the viewport is autoscrolling 4936 d->document->setViewport(newViewport, this, false, d->scroller->state() != QScroller::Scrolling); 4937 } 4938 d->document->setVisiblePageRects(visibleRects, this); 4939 } 4940 4941 void PageView::slotAutoScroll() 4942 { 4943 // the first time create the timer 4944 if (!d->autoScrollTimer) { 4945 d->autoScrollTimer = new QTimer(this); 4946 d->autoScrollTimer->setSingleShot(true); 4947 connect(d->autoScrollTimer, &QTimer::timeout, this, &PageView::slotAutoScroll); 4948 } 4949 4950 // if scrollIncrement is zero, stop the timer 4951 if (!d->scrollIncrement) { 4952 d->autoScrollTimer->stop(); 4953 return; 4954 } 4955 4956 // compute delay between timer ticks and scroll amount per tick 4957 int index = abs(d->scrollIncrement) - 1; // 0..9 4958 const int scrollDelay[10] = {200, 100, 50, 30, 20, 30, 25, 20, 30, 20}; 4959 const int scrollOffset[10] = {1, 1, 1, 1, 1, 2, 2, 2, 4, 4}; 4960 d->autoScrollTimer->start(scrollDelay[index]); 4961 int delta = d->scrollIncrement > 0 ? scrollOffset[index] : -scrollOffset[index]; 4962 d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, delta), scrollDelay[index]); 4963 } 4964 4965 void PageView::slotDragScroll() 4966 { 4967 scrollTo(horizontalScrollBar()->value() + d->dragScrollVector.x(), verticalScrollBar()->value() + d->dragScrollVector.y()); 4968 QPoint p = contentAreaPosition() + viewport()->mapFromGlobal(QCursor::pos()); 4969 updateSelection(p); 4970 } 4971 4972 void PageView::slotShowWelcome() 4973 { 4974 // show initial welcome text 4975 d->messageWindow->display(i18n("Welcome"), QString(), PageViewMessage::Info, 2000); 4976 } 4977 4978 void PageView::slotShowSizeAllCursor() 4979 { 4980 setCursor(Qt::SizeAllCursor); 4981 } 4982 4983 void PageView::slotHandleWebShortcutAction() 4984 { 4985 QAction *action = qobject_cast<QAction *>(sender()); 4986 4987 if (action) { 4988 KUriFilterData filterData(action->data().toString()); 4989 4990 if (KUriFilter::self()->filterSearchUri(filterData, KUriFilter::WebShortcutFilter)) { 4991 QDesktopServices::openUrl(filterData.uri()); 4992 } 4993 } 4994 } 4995 4996 void PageView::slotConfigureWebShortcuts() 4997 { 4998 auto *job = new KIO::CommandLauncherJob(QStringLiteral("kcmshell5"), QStringList() << QStringLiteral("webshortcuts")); 4999 job->start(); 5000 } 5001 5002 void PageView::slotZoom() 5003 { 5004 if (!d->aZoom->selectableActionGroup()->isEnabled()) { 5005 return; 5006 } 5007 5008 setFocus(); 5009 updateZoom(ZoomFixed); 5010 } 5011 5012 void PageView::slotZoomIn() 5013 { 5014 updateZoom(ZoomIn); 5015 } 5016 5017 void PageView::slotZoomOut() 5018 { 5019 updateZoom(ZoomOut); 5020 } 5021 5022 void PageView::slotZoomActual() 5023 { 5024 updateZoom(ZoomActual); 5025 } 5026 5027 void PageView::slotFitToWidthToggled(bool on) 5028 { 5029 if (on) { 5030 updateZoom(ZoomFitWidth); 5031 } 5032 } 5033 5034 void PageView::slotFitToPageToggled(bool on) 5035 { 5036 if (on) { 5037 updateZoom(ZoomFitPage); 5038 } 5039 } 5040 5041 void PageView::slotAutoFitToggled(bool on) 5042 { 5043 if (on) { 5044 updateZoom(ZoomFitAuto); 5045 } 5046 } 5047 5048 void PageView::slotViewMode(QAction *action) 5049 { 5050 const int nr = action->data().toInt(); 5051 if ((int)Okular::Settings::viewMode() != nr) { 5052 Okular::Settings::setViewMode(nr); 5053 Okular::Settings::self()->save(); 5054 if (d->document->pages() > 0) { 5055 slotRelayoutPages(); 5056 } 5057 } 5058 } 5059 5060 void PageView::slotContinuousToggled() 5061 { 5062 if (d->document->pages() > 0) { 5063 slotRelayoutPages(); 5064 } 5065 } 5066 5067 void PageView::slotReadingDirectionToggled(bool leftToRight) 5068 { 5069 Okular::Settings::setRtlReadingDirection(leftToRight); 5070 Okular::Settings::self()->save(); 5071 } 5072 5073 void PageView::slotUpdateReadingDirectionAction() 5074 { 5075 d->aReadingDirection->setChecked(Okular::Settings::rtlReadingDirection()); 5076 } 5077 5078 void PageView::slotSetMouseNormal() 5079 { 5080 d->mouseMode = Okular::Settings::EnumMouseMode::Browse; 5081 Okular::Settings::setMouseMode(d->mouseMode); 5082 // hide the messageWindow 5083 d->messageWindow->hide(); 5084 // force an update of the cursor 5085 updateCursor(); 5086 Okular::Settings::self()->save(); 5087 d->annotator->detachAnnotation(); 5088 } 5089 5090 void PageView::slotSetMouseZoom() 5091 { 5092 d->mouseMode = Okular::Settings::EnumMouseMode::Zoom; 5093 Okular::Settings::setMouseMode(d->mouseMode); 5094 // change the text in messageWindow (and show it if hidden) 5095 d->messageWindow->display(i18n("Select zooming area. Right-click to zoom out."), QString(), PageViewMessage::Info, -1); 5096 // force an update of the cursor 5097 updateCursor(); 5098 Okular::Settings::self()->save(); 5099 d->annotator->detachAnnotation(); 5100 } 5101 5102 void PageView::slotSetMouseMagnifier() 5103 { 5104 d->mouseMode = Okular::Settings::EnumMouseMode::Magnifier; 5105 Okular::Settings::setMouseMode(d->mouseMode); 5106 d->messageWindow->display(i18n("Click to see the magnified view."), QString()); 5107 5108 // force an update of the cursor 5109 updateCursor(); 5110 Okular::Settings::self()->save(); 5111 d->annotator->detachAnnotation(); 5112 } 5113 5114 void PageView::slotSetMouseSelect() 5115 { 5116 d->mouseMode = Okular::Settings::EnumMouseMode::RectSelect; 5117 Okular::Settings::setMouseMode(d->mouseMode); 5118 // change the text in messageWindow (and show it if hidden) 5119 d->messageWindow->display(i18n("Draw a rectangle around the text/graphics to copy."), QString(), PageViewMessage::Info, -1); 5120 // force an update of the cursor 5121 updateCursor(); 5122 Okular::Settings::self()->save(); 5123 d->annotator->detachAnnotation(); 5124 } 5125 5126 void PageView::slotSetMouseTextSelect() 5127 { 5128 d->mouseMode = Okular::Settings::EnumMouseMode::TextSelect; 5129 Okular::Settings::setMouseMode(d->mouseMode); 5130 // change the text in messageWindow (and show it if hidden) 5131 d->messageWindow->display(i18n("Select text"), QString(), PageViewMessage::Info, -1); 5132 // force an update of the cursor 5133 updateCursor(); 5134 Okular::Settings::self()->save(); 5135 d->annotator->detachAnnotation(); 5136 } 5137 5138 void PageView::slotSetMouseTableSelect() 5139 { 5140 d->mouseMode = Okular::Settings::EnumMouseMode::TableSelect; 5141 Okular::Settings::setMouseMode(d->mouseMode); 5142 // change the text in messageWindow (and show it if hidden) 5143 d->messageWindow->display(i18n("Draw a rectangle around the table, then click near edges to divide up; press Esc to clear."), QString(), PageViewMessage::Info, -1); 5144 // force an update of the cursor 5145 updateCursor(); 5146 Okular::Settings::self()->save(); 5147 d->annotator->detachAnnotation(); 5148 } 5149 5150 void PageView::showNoSigningCertificatesDialog(bool nonDateValidCerts) 5151 { 5152 if (nonDateValidCerts) { 5153 KMessageBox::information(this, i18n("All your signing certificates are either not valid yet or are past their validity date.")); 5154 } else { 5155 KMessageBox::information(this, 5156 i18n("There are no available signing certificates.<br/>For more information, please see the section about <a href=\"%1\">Adding Digital Signatures</a> in the manual.", 5157 QStringLiteral("help:/okular/signatures.html#adding_digital_signatures")), 5158 QString(), 5159 QString(), 5160 KMessageBox::Notify | KMessageBox::AllowLink); 5161 } 5162 } 5163 5164 Okular::Document *PageView::document() const 5165 { 5166 return d->document; 5167 } 5168 5169 void PageView::slotSignature() 5170 { 5171 if (!d->document->isHistoryClean()) { 5172 KMessageBox::information(this, i18n("You have unsaved changes. Please save the document before signing it.")); 5173 return; 5174 } 5175 5176 const Okular::CertificateStore *certStore = d->document->certificateStore(); 5177 bool userCancelled, nonDateValidCerts; 5178 const QList<Okular::CertificateInfo> &certs = certStore->signingCertificatesForNow(&userCancelled, &nonDateValidCerts); 5179 if (userCancelled) { 5180 return; 5181 } 5182 5183 if (certs.isEmpty()) { 5184 showNoSigningCertificatesDialog(nonDateValidCerts); 5185 return; 5186 } 5187 5188 d->messageWindow->display(i18n("Draw a rectangle to insert the signature field"), QString(), PageViewMessage::Info, -1); 5189 5190 d->annotator->setSignatureMode(true); 5191 5192 // force an update of the cursor 5193 updateCursor(); 5194 Okular::Settings::self()->save(); 5195 } 5196 5197 void PageView::slotAutoScrollUp() 5198 { 5199 if (d->scrollIncrement < -9) { 5200 return; 5201 } 5202 d->scrollIncrement--; 5203 slotAutoScroll(); 5204 setFocus(); 5205 } 5206 5207 void PageView::slotAutoScrollDown() 5208 { 5209 if (d->scrollIncrement > 9) { 5210 return; 5211 } 5212 d->scrollIncrement++; 5213 slotAutoScroll(); 5214 setFocus(); 5215 } 5216 5217 void PageView::slotScrollUp(int nSteps) 5218 { 5219 if (verticalScrollBar()->value() > verticalScrollBar()->minimum()) { 5220 if (nSteps) { 5221 d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, -100 * nSteps), d->currentShortScrollDuration); 5222 } else { 5223 if (d->scroller->finalPosition().y() > verticalScrollBar()->minimum()) { 5224 d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, -(1 - Okular::Settings::scrollOverlap() / 100.0) * viewport()->height()), d->currentLongScrollDuration); 5225 } 5226 } 5227 } else if (!getContinuousMode() && d->document->currentPage() > 0) { 5228 // Since we are in single page mode and at the top of the page, go to previous page. 5229 // setViewport() is more optimized than document->setPrevPage and then move view to bottom. 5230 Okular::DocumentViewport newViewport = d->document->viewport(); 5231 newViewport.pageNumber -= viewColumns(); 5232 if (newViewport.pageNumber < 0) { 5233 newViewport.pageNumber = 0; 5234 } 5235 newViewport.rePos.enabled = true; 5236 newViewport.rePos.normalizedY = 1.0; 5237 d->document->setViewport(newViewport); 5238 } 5239 } 5240 5241 void PageView::slotScrollDown(int nSteps) 5242 { 5243 if (verticalScrollBar()->value() < verticalScrollBar()->maximum()) { 5244 if (nSteps) { 5245 d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, 100 * nSteps), d->currentShortScrollDuration); 5246 } else { 5247 if (d->scroller->finalPosition().y() < verticalScrollBar()->maximum()) { 5248 d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, (1 - Okular::Settings::scrollOverlap() / 100.0) * viewport()->height()), d->currentLongScrollDuration); 5249 } 5250 } 5251 } else if (!getContinuousMode() && (int)d->document->currentPage() < d->items.count() - 1) { 5252 // Since we are in single page mode and at the bottom of the page, go to next page. 5253 // setViewport() is more optimized than document->setNextPage and then move view to top 5254 Okular::DocumentViewport newViewport = d->document->viewport(); 5255 newViewport.pageNumber += viewColumns(); 5256 if (newViewport.pageNumber >= (int)d->items.count()) { 5257 newViewport.pageNumber = d->items.count() - 1; 5258 } 5259 newViewport.rePos.enabled = true; 5260 newViewport.rePos.normalizedY = 0.0; 5261 d->document->setViewport(newViewport); 5262 } 5263 } 5264 5265 void PageView::slotRotateClockwise() 5266 { 5267 int id = ((int)d->document->rotation() + 1) % 4; 5268 d->document->setRotation(id); 5269 } 5270 5271 void PageView::slotRotateCounterClockwise() 5272 { 5273 int id = ((int)d->document->rotation() + 3) % 4; 5274 d->document->setRotation(id); 5275 } 5276 5277 void PageView::slotRotateOriginal() 5278 { 5279 d->document->setRotation(0); 5280 } 5281 5282 // Enforce mutual-exclusion between trim modes 5283 // Each mode is uniquely identified by a single value 5284 // From Okular::Settings::EnumTrimMode 5285 void PageView::updateTrimMode(int except_id) 5286 { 5287 const QList<QAction *> trimModeActions = d->aTrimMode->menu()->actions(); 5288 for (QAction *trimModeAction : trimModeActions) { 5289 if (trimModeAction->data().toInt() != except_id) { 5290 trimModeAction->setChecked(false); 5291 } 5292 } 5293 } 5294 5295 bool PageView::mouseReleaseOverLink(const Okular::ObjectRect *rect) const 5296 { 5297 if (rect) { 5298 // handle click over a link 5299 const Okular::Action *action = static_cast<const Okular::Action *>(rect->object()); 5300 d->document->processAction(action); 5301 return true; 5302 } 5303 return false; 5304 } 5305 5306 void PageView::slotTrimMarginsToggled(bool on) 5307 { 5308 if (on) { // Turn off any other Trim modes 5309 updateTrimMode(d->aTrimMargins->data().toInt()); 5310 } 5311 5312 if (Okular::Settings::trimMargins() != on) { 5313 Okular::Settings::setTrimMargins(on); 5314 Okular::Settings::self()->save(); 5315 if (d->document->pages() > 0) { 5316 slotRelayoutPages(); 5317 slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already! 5318 } 5319 } 5320 } 5321 5322 void PageView::slotTrimToSelectionToggled(bool on) 5323 { 5324 if (on) { // Turn off any other Trim modes 5325 updateTrimMode(d->aTrimToSelection->data().toInt()); 5326 5327 // Change the mouse mode 5328 d->mouseMode = Okular::Settings::EnumMouseMode::TrimSelect; 5329 d->aMouseNormal->setChecked(false); 5330 5331 // change the text in messageWindow (and show it if hidden) 5332 d->messageWindow->display(i18n("Draw a rectangle around the page area you wish to keep visible"), QString(), PageViewMessage::Info, -1); 5333 // force an update of the cursor 5334 updateCursor(); 5335 } else { 5336 // toggled off while making selection 5337 if (Okular::Settings::EnumMouseMode::TrimSelect == d->mouseMode) { 5338 // clear widget selection and invalidate rect 5339 selectionClear(); 5340 5341 // When Trim selection bbox interaction is over, we should switch to another mousemode. 5342 if (d->aPrevAction) { 5343 d->aPrevAction->trigger(); 5344 d->aPrevAction = nullptr; 5345 } else { 5346 d->aMouseNormal->trigger(); 5347 } 5348 } 5349 5350 d->trimBoundingBox = Okular::NormalizedRect(); // invalidate box 5351 if (d->document->pages() > 0) { 5352 slotRelayoutPages(); 5353 slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already! 5354 } 5355 } 5356 } 5357 5358 void PageView::slotToggleForms() 5359 { 5360 toggleFormWidgets(!d->m_formsVisible); 5361 } 5362 5363 void PageView::slotFormChanged(int pageNumber) 5364 { 5365 if (!d->refreshTimer) { 5366 d->refreshTimer = new QTimer(this); 5367 d->refreshTimer->setSingleShot(true); 5368 connect(d->refreshTimer, &QTimer::timeout, this, &PageView::slotRefreshPage); 5369 } 5370 d->refreshPages << pageNumber; 5371 int delay = 0; 5372 if (d->m_formsVisible) { 5373 delay = 1000; 5374 } 5375 d->refreshTimer->start(delay); 5376 } 5377 5378 void PageView::slotRefreshPage() 5379 { 5380 for (int req : std::as_const(d->refreshPages)) { 5381 QTimer::singleShot(0, this, [this, req] { d->document->refreshPixmaps(req); }); 5382 } 5383 d->refreshPages.clear(); 5384 } 5385 5386 #if HAVE_SPEECH 5387 void PageView::slotSpeakDocument() 5388 { 5389 QString text; 5390 for (const PageViewItem *item : std::as_const(d->items)) { 5391 Okular::RegularAreaRect *area = textSelectionForItem(item); 5392 text.append(item->page()->text(area)); 5393 text.append(QLatin1Char('\n')); 5394 delete area; 5395 } 5396 5397 d->tts()->say(text); 5398 } 5399 5400 void PageView::slotSpeakCurrentPage() 5401 { 5402 const int currentPage = d->document->viewport().pageNumber; 5403 5404 PageViewItem *item = d->items.at(currentPage); 5405 Okular::RegularAreaRect *area = textSelectionForItem(item); 5406 const QString text = item->page()->text(area); 5407 delete area; 5408 5409 d->tts()->say(text); 5410 } 5411 5412 void PageView::slotStopSpeaks() 5413 { 5414 if (!d->m_tts) { 5415 return; 5416 } 5417 5418 d->m_tts->stopAllSpeechs(); 5419 } 5420 5421 void PageView::slotPauseResumeSpeech() 5422 { 5423 if (!d->m_tts) { 5424 return; 5425 } 5426 5427 d->m_tts->pauseResumeSpeech(); 5428 } 5429 5430 #endif 5431 5432 void PageView::slotAction(Okular::Action *action) 5433 { 5434 d->document->processAction(action); 5435 } 5436 5437 void PageView::slotMouseUpAction(Okular::Action *action, Okular::FormField *form) 5438 { 5439 if (form && action->actionType() == Okular::Action::Script) { 5440 d->document->processFormMouseUpScripAction(action, form); 5441 } else { 5442 d->document->processAction(action); 5443 } 5444 } 5445 5446 void PageView::externalKeyPressEvent(QKeyEvent *e) 5447 { 5448 keyPressEvent(e); 5449 } 5450 5451 void PageView::slotProcessMovieAction(const Okular::MovieAction *action) 5452 { 5453 const Okular::MovieAnnotation *movieAnnotation = action->annotation(); 5454 if (!movieAnnotation) { 5455 return; 5456 } 5457 5458 Okular::Movie *movie = movieAnnotation->movie(); 5459 if (!movie) { 5460 return; 5461 } 5462 5463 const int currentPage = d->document->viewport().pageNumber; 5464 5465 PageViewItem *item = d->items.at(currentPage); 5466 if (!item) { 5467 return; 5468 } 5469 5470 VideoWidget *vw = item->videoWidgets().value(movie); 5471 if (!vw) { 5472 return; 5473 } 5474 5475 vw->show(); 5476 5477 switch (action->operation()) { 5478 case Okular::MovieAction::Play: 5479 vw->stop(); 5480 vw->play(); 5481 break; 5482 case Okular::MovieAction::Stop: 5483 vw->stop(); 5484 break; 5485 case Okular::MovieAction::Pause: 5486 vw->pause(); 5487 break; 5488 case Okular::MovieAction::Resume: 5489 vw->play(); 5490 break; 5491 }; 5492 } 5493 5494 void PageView::slotProcessRenditionAction(const Okular::RenditionAction *action) 5495 { 5496 Okular::Movie *movie = action->movie(); 5497 if (!movie) { 5498 return; 5499 } 5500 5501 const int currentPage = d->document->viewport().pageNumber; 5502 5503 PageViewItem *item = d->items.at(currentPage); 5504 if (!item) { 5505 return; 5506 } 5507 5508 VideoWidget *vw = item->videoWidgets().value(movie); 5509 if (!vw) { 5510 return; 5511 } 5512 5513 if (action->operation() == Okular::RenditionAction::None) { 5514 return; 5515 } 5516 5517 vw->show(); 5518 5519 switch (action->operation()) { 5520 case Okular::RenditionAction::Play: 5521 vw->stop(); 5522 vw->play(); 5523 break; 5524 case Okular::RenditionAction::Stop: 5525 vw->stop(); 5526 break; 5527 case Okular::RenditionAction::Pause: 5528 vw->pause(); 5529 break; 5530 case Okular::RenditionAction::Resume: 5531 vw->play(); 5532 break; 5533 default: 5534 return; 5535 }; 5536 } 5537 5538 void PageView::slotFitWindowToPage() 5539 { 5540 const PageViewItem *currentPageItem = nullptr; 5541 QSize viewportSize = viewport()->size(); 5542 for (const PageViewItem *pageItem : std::as_const(d->items)) { 5543 if (pageItem->isVisible()) { 5544 currentPageItem = pageItem; 5545 break; 5546 } 5547 } 5548 5549 if (!currentPageItem) { 5550 return; 5551 } 5552 5553 const QSize pageSize = QSize(currentPageItem->uncroppedWidth() + kcolWidthMargin, currentPageItem->uncroppedHeight() + krowHeightMargin); 5554 if (verticalScrollBar()->isVisible()) { 5555 viewportSize.setWidth(viewportSize.width() + verticalScrollBar()->width()); 5556 } 5557 if (horizontalScrollBar()->isVisible()) { 5558 viewportSize.setHeight(viewportSize.height() + horizontalScrollBar()->height()); 5559 } 5560 Q_EMIT fitWindowToPage(viewportSize, pageSize); 5561 } 5562 5563 void PageView::slotSelectPage() 5564 { 5565 textSelectionClear(); 5566 const int currentPage = d->document->viewport().pageNumber; 5567 PageViewItem *item = d->items.at(currentPage); 5568 5569 if (item) { 5570 Okular::RegularAreaRect *area = textSelectionForItem(item); 5571 d->pagesWithTextSelection.insert(currentPage); 5572 d->document->setPageTextSelection(currentPage, area, palette().color(QPalette::Active, QPalette::Highlight)); 5573 } 5574 } 5575 5576 void PageView::highlightSignatureFormWidget(const Okular::FormFieldSignature *form) 5577 { 5578 QVector<PageViewItem *>::const_iterator dIt = d->items.constBegin(), dEnd = d->items.constEnd(); 5579 for (; dIt != dEnd; ++dIt) { 5580 const QSet<FormWidgetIface *> fwi = (*dIt)->formWidgets(); 5581 for (FormWidgetIface *fw : fwi) { 5582 if (fw->formField() == form) { 5583 SignatureEdit *widget = static_cast<SignatureEdit *>(fw); 5584 widget->setDummyMode(true); 5585 QTimer::singleShot(250, this, [=] { widget->setDummyMode(false); }); 5586 return; 5587 } 5588 } 5589 } 5590 } 5591 5592 // END private SLOTS 5593 5594 /* kate: replace-tabs on; indent-width 4; */