1 /*
2 SPDX-FileCopyrightText: 2004 Enrico Ros <eros.kde@email.it>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include "presentationwidget.h"
8
9 // qt/kde includes
10 #include <QDBusConnection>
11 #include <QDBusMessage>
12 #include <QDBusReply>
13 #include <QLoggingCategory>
14
15 #include <KActionCollection>
16 #include <KCursor>
17 #include <KLineEdit>
18 #include <KLocalizedString>
19 #include <KMessageBox>
20 #include <KRandom>
21 #include <KSelectAction>
22 #include <QAction>
23 #include <QApplication>
24 #include <QDialog>
25 #include <QEvent>
26 #include <QFontMetrics>
27 #include <QGestureEvent>
28 #include <QIcon>
29 #include <QImage>
30 #include <QLabel>
31 #include <QLayout>
32 #include <QPainter>
33 #include <QScreen>
34 #include <QStyle>
35 #include <QStyleOption>
36 #include <QTimer>
37 #include <QToolBar>
38 #include <QToolTip>
39 #include <QValidator>
40
41 #ifdef Q_OS_LINUX
42 #include <QDBusUnixFileDescriptor>
43 #include <unistd.h> // For ::close() for sleep inhibition
44 #endif
45
46 // system includes
47 #include <math.h>
48 #include <stdlib.h>
49
50 // local includes
51 #include "annotationtools.h"
52 #include "core/action.h"
53 #include "core/annotations.h"
54 #include "core/audioplayer.h"
55 #include "core/document.h"
56 #include "core/generator.h"
57 #include "core/movie.h"
58 #include "core/page.h"
59 #include "debug_ui.h"
60 #include "drawingtoolactions.h"
61 #include "guiutils.h"
62 #include "pagepainter.h"
63 #include "presentationsearchbar.h"
64 #include "priorities.h"
65 #include "settings.h"
66 #include "settings_core.h"
67 #include "videowidget.h"
68
69 // comment this to disable the top-right progress indicator
70 #define ENABLE_PROGRESS_OVERLAY
71
72 // a frame contains a pointer to the page object, its geometry and the
73 // transition effect to the next frame
74 struct PresentationFrame {
75 PresentationFrame() = default;
76
~PresentationFramePresentationFrame77 ~PresentationFrame()
78 {
79 qDeleteAll(videoWidgets);
80 }
81
82 PresentationFrame(const PresentationFrame &) = delete;
83 PresentationFrame &operator=(const PresentationFrame &) = delete;
84
recalcGeometryPresentationFrame85 void recalcGeometry(int width, int height, float screenRatio)
86 {
87 // calculate frame geometry keeping constant aspect ratio
88 float pageRatio = page->ratio();
89 int pageWidth = width, pageHeight = height;
90 if (pageRatio > screenRatio)
91 pageWidth = (int)((float)pageHeight / pageRatio);
92 else
93 pageHeight = (int)((float)pageWidth * pageRatio);
94 geometry.setRect((width - pageWidth) / 2, (height - pageHeight) / 2, pageWidth, pageHeight);
95
96 for (VideoWidget *vw : qAsConst(videoWidgets)) {
97 const Okular::NormalizedRect r = vw->normGeometry();
98 QRect vwgeom = r.geometry(geometry.width(), geometry.height());
99 vw->resize(vwgeom.size());
100 vw->move(geometry.topLeft() + vwgeom.topLeft());
101 }
102 }
103
104 const Okular::Page *page;
105 QRect geometry;
106 QHash<Okular::Movie *, VideoWidget *> videoWidgets;
107 QLinkedList<SmoothPath> drawings;
108 };
109
110 // a custom QToolBar that basically does not propagate the event if the widget
111 // background is not automatically filled
112 class PresentationToolBar : public QToolBar
113 {
114 Q_OBJECT
115
116 public:
PresentationToolBar(QWidget * parent=Q_NULLPTR)117 explicit PresentationToolBar(QWidget *parent = Q_NULLPTR)
118 : QToolBar(parent)
119 {
120 }
121
122 protected:
mousePressEvent(QMouseEvent * e)123 void mousePressEvent(QMouseEvent *e) override
124 {
125 QToolBar::mousePressEvent(e);
126 e->accept();
127 }
128
mouseReleaseEvent(QMouseEvent * e)129 void mouseReleaseEvent(QMouseEvent *e) override
130 {
131 QToolBar::mouseReleaseEvent(e);
132 e->accept();
133 }
134 };
135
PresentationWidget(QWidget * parent,Okular::Document * doc,DrawingToolActions * drawingToolActions,KActionCollection * collection)136 PresentationWidget::PresentationWidget(QWidget *parent, Okular::Document *doc, DrawingToolActions *drawingToolActions, KActionCollection *collection)
137 : QWidget(nullptr /* must be null, to have an independent widget */, Qt::FramelessWindowHint)
138 , m_pressedLink(nullptr)
139 , m_handCursor(false)
140 , m_drawingEngine(nullptr)
141 , m_screenInhibitCookie(0)
142 , m_sleepInhibitFd(-1)
143 , m_parentWidget(parent)
144 , m_document(doc)
145 , m_frameIndex(-1)
146 , m_topBar(nullptr)
147 , m_pagesEdit(nullptr)
148 , m_searchBar(nullptr)
149 , m_ac(collection)
150 , m_screenSelect(nullptr)
151 , m_isSetup(false)
152 , m_blockNotifications(false)
153 , m_inBlackScreenMode(false)
154 , m_showSummaryView(Okular::Settings::slidesShowSummary())
155 , m_advanceSlides(Okular::SettingsCore::slidesAdvance())
156 , m_goToPreviousPageOnRelease(false)
157 , m_goToNextPageOnRelease(false)
158 {
159 setAttribute(Qt::WA_DeleteOnClose);
160 setAttribute(Qt::WA_OpaquePaintEvent);
161 setObjectName(QStringLiteral("presentationWidget"));
162 QString caption = doc->metaData(QStringLiteral("DocumentTitle")).toString();
163 if (caption.trimmed().isEmpty())
164 caption = doc->currentDocument().fileName();
165 caption = i18nc("[document title/filename] – Presentation", "%1 – Presentation", caption);
166 setWindowTitle(caption);
167
168 m_width = -1;
169
170 // create top toolbar
171 m_topBar = new PresentationToolBar(this);
172 m_topBar->setObjectName(QStringLiteral("presentationBar"));
173 m_topBar->setMovable(false);
174 m_topBar->layout()->setContentsMargins(0, 0, 0, 0);
175 m_topBar->addAction(QIcon::fromTheme(layoutDirection() == Qt::RightToLeft ? QStringLiteral("go-next") : QStringLiteral("go-previous")), i18n("Previous Page"), this, SLOT(slotPrevPage()));
176 m_pagesEdit = new KLineEdit(m_topBar);
177 QSizePolicy sp = m_pagesEdit->sizePolicy();
178 sp.setHorizontalPolicy(QSizePolicy::Minimum);
179 m_pagesEdit->setSizePolicy(sp);
180 QFontMetrics fm(m_pagesEdit->font());
181 QStyleOptionFrame option;
182 option.initFrom(m_pagesEdit);
183 m_pagesEdit->setMaximumWidth(fm.horizontalAdvance(QString::number(m_document->pages())) + 2 * style()->pixelMetric(QStyle::PM_DefaultFrameWidth, &option, m_pagesEdit) +
184 4); // the 4 comes from 2*horizontalMargin, horizontalMargin being a define in qlineedit.cpp
185 QIntValidator *validator = new QIntValidator(1, m_document->pages(), m_pagesEdit);
186 m_pagesEdit->setValidator(validator);
187 m_topBar->addWidget(m_pagesEdit);
188 QLabel *pagesLabel = new QLabel(m_topBar);
189 pagesLabel->setText(QLatin1String(" / ") + QString::number(m_document->pages()) + QLatin1String(" "));
190 m_topBar->addWidget(pagesLabel);
191 connect(m_pagesEdit, &QLineEdit::returnPressed, this, &PresentationWidget::slotPageChanged);
192 m_topBar->addAction(QIcon::fromTheme(layoutDirection() == Qt::RightToLeft ? QStringLiteral("go-previous") : QStringLiteral("go-next")), i18n("Next Page"), this, SLOT(slotNextPage()));
193 m_topBar->addSeparator();
194 QAction *playPauseAct = collection->action(QStringLiteral("presentation_play_pause"));
195 playPauseAct->setEnabled(true);
196 connect(playPauseAct, &QAction::triggered, this, &PresentationWidget::slotTogglePlayPause);
197 m_topBar->addAction(playPauseAct);
198 addAction(playPauseAct);
199 m_topBar->addSeparator();
200
201 const QList<QAction *> actionsList = drawingToolActions->actions();
202 for (QAction *action : actionsList) {
203 action->setEnabled(true);
204 m_topBar->addAction(action);
205 addAction(action);
206 }
207 connect(drawingToolActions, &DrawingToolActions::changeEngine, this, &PresentationWidget::slotChangeDrawingToolEngine);
208 connect(drawingToolActions, &DrawingToolActions::actionsRecreated, this, &PresentationWidget::slotAddDrawingToolActions);
209
210 QAction *eraseDrawingAct = collection->action(QStringLiteral("presentation_erase_drawings"));
211 eraseDrawingAct->setEnabled(true);
212 connect(eraseDrawingAct, &QAction::triggered, this, &PresentationWidget::clearDrawings);
213 m_topBar->addAction(eraseDrawingAct);
214 addAction(eraseDrawingAct);
215
216 const int screenCount = QApplication::screens().count();
217 if (screenCount > 1) {
218 m_topBar->addSeparator();
219 m_screenSelect = new KSelectAction(QIcon::fromTheme(QStringLiteral("video-display")), i18n("Switch Screen"), m_topBar);
220 m_screenSelect->setToolBarMode(KSelectAction::MenuMode);
221 m_screenSelect->setToolButtonPopupMode(QToolButton::InstantPopup);
222 m_topBar->addAction(m_screenSelect);
223 for (int i = 0; i < screenCount; ++i) {
224 QAction *act = m_screenSelect->addAction(i18nc("%1 is the screen number (0, 1, ...)", "Screen %1", i));
225 act->setData(QVariant::fromValue(i));
226 }
227 }
228 QWidget *spacer = new QWidget(m_topBar);
229 spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::MinimumExpanding);
230 m_topBar->addWidget(spacer);
231 m_topBar->addAction(QIcon::fromTheme(QStringLiteral("application-exit")), i18n("Exit Presentation Mode"), this, SLOT(close()));
232 m_topBar->setAutoFillBackground(true);
233 showTopBar(false);
234 // change topbar background color
235 QPalette p = m_topBar->palette();
236 p.setColor(QPalette::Active, QPalette::Button, Qt::gray);
237 p.setColor(QPalette::Active, QPalette::Window, Qt::darkGray);
238 m_topBar->setPalette(p);
239
240 // Grab swipe gestures to change pages
241 grabGesture(Qt::SwipeGesture);
242
243 // misc stuff
244 setMouseTracking(true);
245 setContextMenuPolicy(Qt::PreventContextMenu);
246 m_transitionTimer = new QTimer(this);
247 m_transitionTimer->setSingleShot(true);
248 connect(m_transitionTimer, &QTimer::timeout, this, &PresentationWidget::slotTransitionStep);
249 m_overlayHideTimer = new QTimer(this);
250 m_overlayHideTimer->setSingleShot(true);
251 connect(m_overlayHideTimer, &QTimer::timeout, this, &PresentationWidget::slotHideOverlay);
252 m_nextPageTimer = new QTimer(this);
253 m_nextPageTimer->setSingleShot(true);
254 connect(m_nextPageTimer, &QTimer::timeout, this, &PresentationWidget::slotNextPage);
255 setPlayPauseIcon();
256
257 connect(m_document, &Okular::Document::processMovieAction, this, &PresentationWidget::slotProcessMovieAction);
258 connect(m_document, &Okular::Document::processRenditionAction, this, &PresentationWidget::slotProcessRenditionAction);
259
260 // handle cursor appearance as specified in configuration
261 if (Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::HiddenDelay) {
262 KCursor::setAutoHideCursor(this, true);
263 KCursor::setHideCursorDelay(3000);
264 } else if (Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::Hidden) {
265 setCursor(QCursor(Qt::BlankCursor));
266 }
267
268 setupActions();
269
270 // inhibit power management
271 inhibitPowerManagement();
272
273 QTimer::singleShot(0, this, &PresentationWidget::slotDelayedEvents);
274
275 // setFocus() so KCursor::setAutoHideCursor() goes into effect if it's enabled
276 setFocus(Qt::OtherFocusReason);
277
278 // Catch TabletEnterProximity and TabletLeaveProximity events from the QApplication
279 qApp->installEventFilter(this);
280 }
281
~PresentationWidget()282 PresentationWidget::~PresentationWidget()
283 {
284 // allow power management saver again
285 allowPowerManagement();
286
287 // stop the audio playbacks
288 Okular::AudioPlayer::instance()->stopPlaybacks();
289
290 // remove our highlights
291 if (m_searchBar) {
292 m_document->resetSearch(PRESENTATION_SEARCH_ID);
293 }
294
295 // remove this widget from document observer
296 m_document->removeObserver(this);
297
298 const QList<QAction *> actionsList = actions();
299 for (QAction *action : actionsList) {
300 action->setChecked(false);
301 action->setEnabled(false);
302 }
303
304 delete m_drawingEngine;
305
306 // delete frames
307 qDeleteAll(m_frames);
308
309 qApp->removeEventFilter(this);
310 }
311
notifySetup(const QVector<Okular::Page * > & pageSet,int setupFlags)312 void PresentationWidget::notifySetup(const QVector<Okular::Page *> &pageSet, int setupFlags)
313 {
314 // same document, nothing to change - here we assume the document sets up
315 // us with the whole document set as first notifySetup()
316 if (!(setupFlags & Okular::DocumentObserver::DocumentChanged))
317 return;
318
319 // delete previous frames (if any (shouldn't be))
320 qDeleteAll(m_frames);
321 if (!m_frames.isEmpty())
322 qCWarning(OkularUiDebug) << "Frames setup changed while a Presentation is in progress.";
323 m_frames.clear();
324
325 // create the new frames
326 float screenRatio = (float)m_height / (float)m_width;
327 for (const Okular::Page *page : pageSet) {
328 PresentationFrame *frame = new PresentationFrame();
329 frame->page = page;
330 const QLinkedList<Okular::Annotation *> annotations = page->annotations();
331 for (Okular::Annotation *a : annotations) {
332 if (a->subType() == Okular::Annotation::AMovie) {
333 Okular::MovieAnnotation *movieAnn = static_cast<Okular::MovieAnnotation *>(a);
334 VideoWidget *vw = new VideoWidget(movieAnn, movieAnn->movie(), m_document, this);
335 frame->videoWidgets.insert(movieAnn->movie(), vw);
336 vw->pageInitialized();
337 } else if (a->subType() == Okular::Annotation::ARichMedia) {
338 Okular::RichMediaAnnotation *richMediaAnn = static_cast<Okular::RichMediaAnnotation *>(a);
339 if (richMediaAnn->movie()) {
340 VideoWidget *vw = new VideoWidget(richMediaAnn, richMediaAnn->movie(), m_document, this);
341 frame->videoWidgets.insert(richMediaAnn->movie(), vw);
342 vw->pageInitialized();
343 }
344 } else if (a->subType() == Okular::Annotation::AScreen) {
345 const Okular::ScreenAnnotation *screenAnn = static_cast<Okular::ScreenAnnotation *>(a);
346 Okular::Movie *movie = GuiUtils::renditionMovieFromScreenAnnotation(screenAnn);
347 if (movie) {
348 VideoWidget *vw = new VideoWidget(screenAnn, movie, m_document, this);
349 frame->videoWidgets.insert(movie, vw);
350 vw->pageInitialized();
351 }
352 }
353 }
354 frame->recalcGeometry(m_width, m_height, screenRatio);
355 // add the frame to the vector
356 m_frames.push_back(frame);
357 }
358
359 // get metadata from the document
360 m_metaStrings.clear();
361 const Okular::DocumentInfo info = m_document->documentInfo(QSet<Okular::DocumentInfo::Key>() << Okular::DocumentInfo::Title << Okular::DocumentInfo::Author);
362 if (!info.get(Okular::DocumentInfo::Title).isNull())
363 m_metaStrings += i18n("Title: %1", info.get(Okular::DocumentInfo::Title));
364 if (!info.get(Okular::DocumentInfo::Author).isNull())
365 m_metaStrings += i18n("Author: %1", info.get(Okular::DocumentInfo::Author));
366 m_metaStrings += i18n("Pages: %1", m_document->pages());
367 m_metaStrings += i18n("Click to begin");
368
369 m_isSetup = true;
370 }
371
notifyViewportChanged(bool)372 void PresentationWidget::notifyViewportChanged(bool /*smoothMove*/)
373 {
374 // display the current page
375 changePage(m_document->viewport().pageNumber);
376
377 // auto advance to the next page if set
378 startAutoChangeTimer();
379 }
380
notifyPageChanged(int pageNumber,int changedFlags)381 void PresentationWidget::notifyPageChanged(int pageNumber, int changedFlags)
382 {
383 // if we are blocking the notifications, do nothing
384 if (m_blockNotifications)
385 return;
386
387 // check if it's the last requested pixmap. if so update the widget.
388 if ((changedFlags & (DocumentObserver::Pixmap | DocumentObserver::Annotations | DocumentObserver::Highlights)) && pageNumber == m_frameIndex)
389 generatePage(changedFlags & (DocumentObserver::Annotations | DocumentObserver::Highlights));
390 }
391
notifyCurrentPageChanged(int previousPage,int currentPage)392 void PresentationWidget::notifyCurrentPageChanged(int previousPage, int currentPage)
393 {
394 if (previousPage != -1) {
395 // stop video playback
396 for (VideoWidget *vw : qAsConst(m_frames[previousPage]->videoWidgets)) {
397 vw->stop();
398 vw->pageLeft();
399 }
400
401 // stop audio playback, if any
402 Okular::AudioPlayer::instance()->stopPlaybacks();
403
404 // perform the page closing action, if any
405 if (m_document->page(previousPage)->pageAction(Okular::Page::Closing))
406 m_document->processAction(m_document->page(previousPage)->pageAction(Okular::Page::Closing));
407
408 // perform the additional actions of the page's annotations, if any
409 const QLinkedList<Okular::Annotation *> annotationsList = m_document->page(previousPage)->annotations();
410 for (const Okular::Annotation *annotation : annotationsList) {
411 Okular::Action *action = nullptr;
412
413 if (annotation->subType() == Okular::Annotation::AScreen)
414 action = static_cast<const Okular::ScreenAnnotation *>(annotation)->additionalAction(Okular::Annotation::PageClosing);
415 else if (annotation->subType() == Okular::Annotation::AWidget)
416 action = static_cast<const Okular::WidgetAnnotation *>(annotation)->additionalAction(Okular::Annotation::PageClosing);
417
418 if (action)
419 m_document->processAction(action);
420 }
421 }
422
423 if (currentPage != -1) {
424 m_frameIndex = currentPage;
425
426 // check if pixmap exists or else request it
427 PresentationFrame *frame = m_frames[m_frameIndex];
428 int pixW = frame->geometry.width();
429 int pixH = frame->geometry.height();
430
431 bool signalsBlocked = m_pagesEdit->signalsBlocked();
432 m_pagesEdit->blockSignals(true);
433 m_pagesEdit->setText(QString::number(m_frameIndex + 1));
434 m_pagesEdit->blockSignals(signalsBlocked);
435
436 // if pixmap not inside the Okular::Page we request it and wait for
437 // notifyPixmapChanged call or else we can proceed to pixmap generation
438 if (!frame->page->hasPixmap(this, ceil(pixW * devicePixelRatioF()), ceil(pixH * devicePixelRatioF()))) {
439 requestPixmaps();
440 } else {
441 // make the background pixmap
442 generatePage();
443 }
444
445 // perform the page opening action, if any
446 if (m_document->page(m_frameIndex)->pageAction(Okular::Page::Opening))
447 m_document->processAction(m_document->page(m_frameIndex)->pageAction(Okular::Page::Opening));
448
449 // perform the additional actions of the page's annotations, if any
450 const QLinkedList<Okular::Annotation *> annotationsList = m_document->page(m_frameIndex)->annotations();
451 for (const Okular::Annotation *annotation : annotationsList) {
452 Okular::Action *action = nullptr;
453
454 if (annotation->subType() == Okular::Annotation::AScreen)
455 action = static_cast<const Okular::ScreenAnnotation *>(annotation)->additionalAction(Okular::Annotation::PageOpening);
456 else if (annotation->subType() == Okular::Annotation::AWidget)
457 action = static_cast<const Okular::WidgetAnnotation *>(annotation)->additionalAction(Okular::Annotation::PageOpening);
458
459 if (action)
460 m_document->processAction(action);
461 }
462
463 // start autoplay video playback
464 for (VideoWidget *vw : qAsConst(m_frames[m_frameIndex]->videoWidgets)) {
465 vw->pageEntered();
466 }
467 }
468 }
469
canUnloadPixmap(int pageNumber) const470 bool PresentationWidget::canUnloadPixmap(int pageNumber) const
471 {
472 if (Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Low || Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Normal) {
473 // can unload all pixmaps except for the currently visible one
474 return pageNumber != m_frameIndex;
475 } else {
476 // can unload all pixmaps except for the currently visible one, previous and next
477 return qAbs(pageNumber - m_frameIndex) <= 1;
478 }
479 }
480
setupActions()481 void PresentationWidget::setupActions()
482 {
483 addAction(m_ac->action(QStringLiteral("first_page")));
484 addAction(m_ac->action(QStringLiteral("last_page")));
485 addAction(m_ac->action(QString::fromLocal8Bit(KStandardAction::name(KStandardAction::Prior))));
486 addAction(m_ac->action(QString::fromLocal8Bit(KStandardAction::name(KStandardAction::Next))));
487 addAction(m_ac->action(QString::fromLocal8Bit(KStandardAction::name(KStandardAction::DocumentBack))));
488 addAction(m_ac->action(QString::fromLocal8Bit(KStandardAction::name(KStandardAction::DocumentForward))));
489
490 QAction *action = m_ac->action(QStringLiteral("switch_blackscreen_mode"));
491 connect(action, &QAction::toggled, this, &PresentationWidget::toggleBlackScreenMode);
492 action->setEnabled(true);
493 addAction(action);
494 }
495
setPlayPauseIcon()496 void PresentationWidget::setPlayPauseIcon()
497 {
498 QAction *playPauseAction = m_ac->action(QStringLiteral("presentation_play_pause"));
499 if (m_nextPageTimer->isActive()) {
500 playPauseAction->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-pause")));
501 playPauseAction->setToolTip(i18nc("For Presentation", "Pause"));
502 } else {
503 playPauseAction->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-start")));
504 playPauseAction->setToolTip(i18nc("For Presentation", "Play"));
505 }
506 }
507
eventFilter(QObject * o,QEvent * e)508 bool PresentationWidget::eventFilter(QObject *o, QEvent *e)
509 {
510 if (o == qApp) {
511 if (e->type() == QTabletEvent::TabletEnterProximity) {
512 setCursor(QCursor(Qt::CrossCursor));
513 } else if (e->type() == QTabletEvent::TabletLeaveProximity) {
514 setCursor(QCursor(Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::Hidden ? Qt::BlankCursor : Qt::ArrowCursor));
515 if (Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::HiddenDelay) {
516 // Trick KCursor to hide the cursor if needed by sending an "unknown" key press event
517 // Send also the key release to make the world happy even it's probably not needed
518 QKeyEvent kp(QEvent::KeyPress, 0, Qt::NoModifier);
519 qApp->sendEvent(this, &kp);
520 QKeyEvent kr(QEvent::KeyRelease, 0, Qt::NoModifier);
521 qApp->sendEvent(this, &kr);
522 }
523 }
524 }
525 return false;
526 }
527
528 // <widget events>
event(QEvent * e)529 bool PresentationWidget::event(QEvent *e)
530 {
531 if (e->type() == QEvent::Gesture)
532 return gestureEvent(static_cast<QGestureEvent *>(e));
533
534 if (e->type() == QEvent::ToolTip) {
535 QHelpEvent *he = (QHelpEvent *)e;
536
537 QRect r;
538 const Okular::Action *link = getLink(he->x(), he->y(), &r);
539
540 if (link) {
541 QString tip = link->actionTip();
542 if (!tip.isEmpty())
543 QToolTip::showText(he->globalPos(), tip, this, r);
544 }
545 e->accept();
546 return true;
547 } else
548 // do not stop the event
549 return QWidget::event(e);
550 }
551
gestureEvent(QGestureEvent * event)552 bool PresentationWidget::gestureEvent(QGestureEvent *event)
553 {
554 // Swiping left or right on a touch screen will go to the previous or next slide, respectively.
555 // The precise gesture is the standard Qt swipe: with three(!) fingers.
556 if (QGesture *swipe = event->gesture(Qt::SwipeGesture)) {
557 QSwipeGesture *swipeEvent = static_cast<QSwipeGesture *>(swipe);
558
559 if (swipeEvent->state() == Qt::GestureFinished) {
560 if (swipeEvent->horizontalDirection() == QSwipeGesture::Left) {
561 slotPrevPage();
562 event->accept();
563 return true;
564 }
565 if (swipeEvent->horizontalDirection() == QSwipeGesture::Right) {
566 slotNextPage();
567 event->accept();
568 return true;
569 }
570 }
571 }
572
573 return false;
574 }
keyPressEvent(QKeyEvent * e)575 void PresentationWidget::keyPressEvent(QKeyEvent *e)
576 {
577 if (!m_isSetup)
578 return;
579
580 switch (e->key()) {
581 case Qt::Key_Left:
582 case Qt::Key_Backspace:
583 case Qt::Key_PageUp:
584 case Qt::Key_Up:
585 slotPrevPage();
586 break;
587 case Qt::Key_Right:
588 case Qt::Key_Space:
589 case Qt::Key_PageDown:
590 case Qt::Key_Down:
591 slotNextPage();
592 break;
593 case Qt::Key_Home:
594 slotFirstPage();
595 break;
596 case Qt::Key_End:
597 slotLastPage();
598 break;
599 case Qt::Key_Escape:
600 if (!m_topBar->isHidden())
601 showTopBar(false);
602 else
603 close();
604 break;
605 }
606 }
607
wheelEvent(QWheelEvent * e)608 void PresentationWidget::wheelEvent(QWheelEvent *e)
609 {
610 if (!m_isSetup)
611 return;
612
613 // performance note: don't remove the clipping
614 int div = e->angleDelta().y() / 120;
615 if (div > 0) {
616 if (div > 3)
617 div = 3;
618 while (div--)
619 slotPrevPage();
620 } else if (div < 0) {
621 if (div < -3)
622 div = -3;
623 while (div++)
624 slotNextPage();
625 }
626 }
627
mousePressEvent(QMouseEvent * e)628 void PresentationWidget::mousePressEvent(QMouseEvent *e)
629 {
630 if (!m_isSetup)
631 return;
632
633 if (m_drawingEngine) {
634 QRect r = routeMouseDrawingEvent(e);
635 if (r.isValid()) {
636 m_drawingRect |= r.translated(m_frames[m_frameIndex]->geometry.topLeft());
637 update(m_drawingRect);
638 }
639 return;
640 }
641
642 // pressing left button
643 if (e->button() == Qt::LeftButton) {
644 // if pressing on a link, skip other checks
645 if ((m_pressedLink = getLink(e->x(), e->y())))
646 return;
647
648 const Okular::Annotation *annotation = getAnnotation(e->x(), e->y());
649 if (annotation) {
650 if (annotation->subType() == Okular::Annotation::AMovie) {
651 const Okular::MovieAnnotation *movieAnnotation = static_cast<const Okular::MovieAnnotation *>(annotation);
652
653 VideoWidget *vw = m_frames[m_frameIndex]->videoWidgets.value(movieAnnotation->movie());
654 vw->show();
655 vw->play();
656 return;
657 } else if (annotation->subType() == Okular::Annotation::ARichMedia) {
658 const Okular::RichMediaAnnotation *richMediaAnnotation = static_cast<const Okular::RichMediaAnnotation *>(annotation);
659
660 VideoWidget *vw = m_frames[m_frameIndex]->videoWidgets.value(richMediaAnnotation->movie());
661 vw->show();
662 vw->play();
663 return;
664 } else if (annotation->subType() == Okular::Annotation::AScreen) {
665 m_document->processAction(static_cast<const Okular::ScreenAnnotation *>(annotation)->action());
666 return;
667 }
668 }
669
670 // handle clicking on top-right overlay
671 if (!(Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::Hidden) && m_overlayGeometry.contains(e->pos())) {
672 overlayClick(e->pos());
673 return;
674 }
675
676 // Actual mouse press events always lead to the next page page
677 if (e->source() == Qt::MouseEventNotSynthesized) {
678 m_goToNextPageOnRelease = true;
679 }
680 // Touch events may lead to the previous or next page
681 else if (Okular::Settings::slidesTapNavigation() != Okular::Settings::EnumSlidesTapNavigation::Disabled) {
682 switch (Okular::Settings::slidesTapNavigation()) {
683 case Okular::Settings::EnumSlidesTapNavigation::ForwardBackward: {
684 if (e->x() < (geometry().width() / 2)) {
685 m_goToPreviousPageOnRelease = true;
686 } else {
687 m_goToNextPageOnRelease = true;
688 }
689 break;
690 }
691 case Okular::Settings::EnumSlidesTapNavigation::Forward: {
692 m_goToNextPageOnRelease = true;
693 break;
694 }
695 case Okular::Settings::EnumSlidesTapNavigation::Disabled: {
696 // Do Nothing
697 }
698 }
699 }
700 }
701 // pressing forward button
702 else if (e->button() == Qt::ForwardButton) {
703 m_goToNextPageOnRelease = true;
704 }
705 // pressing right or backward button
706 else if (e->button() == Qt::RightButton || e->button() == Qt::BackButton)
707 m_goToPreviousPageOnRelease = true;
708 }
709
mouseReleaseEvent(QMouseEvent * e)710 void PresentationWidget::mouseReleaseEvent(QMouseEvent *e)
711 {
712 if (m_drawingEngine) {
713 routeMouseDrawingEvent(e);
714 return;
715 }
716
717 // if releasing on the same link we pressed over, execute it
718 if (m_pressedLink && e->button() == Qt::LeftButton) {
719 const Okular::Action *link = getLink(e->x(), e->y());
720 if (link == m_pressedLink)
721 m_document->processAction(link);
722 m_pressedLink = nullptr;
723 }
724
725 if (m_goToPreviousPageOnRelease) {
726 slotPrevPage();
727 m_goToPreviousPageOnRelease = false;
728 }
729
730 if (m_goToNextPageOnRelease) {
731 slotNextPage();
732 m_goToNextPageOnRelease = false;
733 }
734 }
735
mouseMoveEvent(QMouseEvent * e)736 void PresentationWidget::mouseMoveEvent(QMouseEvent *e)
737 {
738 // safety check
739 if (!m_isSetup)
740 return;
741
742 // update cursor and tooltip if hovering a link
743 if (!m_drawingEngine && Okular::Settings::slidesCursor() != Okular::Settings::EnumSlidesCursor::Hidden)
744 testCursorOnLink(e->x(), e->y());
745
746 if (!m_topBar->isHidden()) {
747 // hide a shown bar when exiting the area
748 if (e->y() > (m_topBar->height() + 1)) {
749 showTopBar(false);
750 setFocus(Qt::OtherFocusReason);
751 }
752 } else {
753 if (m_drawingEngine && e->buttons() != Qt::NoButton) {
754 QRect r = routeMouseDrawingEvent(e);
755 if (r.isValid()) {
756 m_drawingRect |= r.translated(m_frames[m_frameIndex]->geometry.topLeft());
757 update(m_drawingRect);
758 }
759 } else {
760 // show the bar if reaching top 2 pixels
761 if (e->y() <= 1)
762 showTopBar(true);
763 // handle "dragging the wheel" if clicking on its geometry
764 else if ((QApplication::mouseButtons() & Qt::LeftButton) && m_overlayGeometry.contains(e->pos()))
765 overlayClick(e->pos());
766 }
767 }
768 }
769
paintEvent(QPaintEvent * pe)770 void PresentationWidget::paintEvent(QPaintEvent *pe)
771 {
772 qreal dpr = devicePixelRatioF();
773
774 if (m_inBlackScreenMode) {
775 QPainter painter(this);
776 painter.fillRect(pe->rect(), Qt::black);
777 return;
778 }
779
780 if (!m_isSetup) {
781 m_width = width();
782 m_height = height();
783
784 connect(m_document, &Okular::Document::linkFind, this, &PresentationWidget::slotFind);
785
786 // register this observer in document. events will come immediately
787 m_document->addObserver(this);
788
789 // show summary if requested
790 if (Okular::Settings::slidesShowSummary())
791 generatePage();
792 }
793
794 // check painting rect consistency
795 QRect r = pe->rect().intersected(QRect(QPoint(0, 0), geometry().size()));
796 if (r.isNull())
797 return;
798
799 if (m_lastRenderedPixmap.isNull()) {
800 QPainter painter(this);
801 painter.fillRect(pe->rect(), Okular::Settings::slidesBackgroundColor());
802 return;
803 }
804
805 // blit the pixmap to the screen
806 QPainter painter(this);
807 for (const QRect &r : pe->region()) {
808 if (!r.isValid())
809 continue;
810 #ifdef ENABLE_PROGRESS_OVERLAY
811 const QRect dR(QRectF(r.x() * dpr, r.y() * dpr, r.width() * dpr, r.height() * dpr).toAlignedRect());
812 if (Okular::Settings::slidesShowProgress() && r.intersects(m_overlayGeometry)) {
813 // backbuffer the overlay operation
814 QPixmap backPixmap(dR.size());
815 backPixmap.setDevicePixelRatio(dpr);
816 QPainter pixPainter(&backPixmap);
817
818 // first draw the background on the backbuffer
819 pixPainter.drawPixmap(QPoint(0, 0), m_lastRenderedPixmap, dR);
820
821 // then blend the overlay (a piece of) over the background
822 QRect ovr = m_overlayGeometry.intersected(r);
823 pixPainter.drawPixmap((ovr.left() - r.left()), (ovr.top() - r.top()), m_lastRenderedOverlay, (ovr.left() - m_overlayGeometry.left()) * dpr, (ovr.top() - m_overlayGeometry.top()) * dpr, ovr.width() * dpr, ovr.height() * dpr);
824
825 // finally blit the pixmap to the screen
826 pixPainter.end();
827 const QRect backPixmapRect = backPixmap.rect();
828 const QRect dBackPixmapRect(QRectF(backPixmapRect.x() * dpr, backPixmapRect.y() * dpr, backPixmapRect.width() * dpr, backPixmapRect.height() * dpr).toAlignedRect());
829 painter.drawPixmap(r.topLeft(), backPixmap, dBackPixmapRect);
830 } else
831 #endif
832 // copy the rendered pixmap to the screen
833 painter.drawPixmap(r.topLeft(), m_lastRenderedPixmap, dR);
834 }
835
836 // paint drawings
837 if (m_frameIndex != -1) {
838 painter.save();
839
840 const QRect &geom = m_frames[m_frameIndex]->geometry;
841
842 const QSize pmSize(geom.width() * dpr, geom.height() * dpr);
843 QPixmap pm(pmSize);
844 pm.fill(Qt::transparent);
845 QPainter pmPainter(&pm);
846
847 pm.setDevicePixelRatio(dpr);
848 pmPainter.setRenderHints(QPainter::Antialiasing);
849
850 // Paint old paths
851 for (const SmoothPath &drawing : qAsConst(m_frames[m_frameIndex]->drawings)) {
852 drawing.paint(&pmPainter, pmSize.width(), pmSize.height());
853 }
854
855 // Paint the path that is currently being drawn by the user
856 if (m_drawingEngine && m_drawingRect.intersects(pe->rect()))
857 m_drawingEngine->paint(&pmPainter, pmSize.width(), pmSize.height(), m_drawingRect.intersected(pe->rect()));
858
859 painter.setRenderHints(QPainter::Antialiasing);
860 painter.drawPixmap(geom.topLeft(), pm);
861
862 painter.restore();
863 }
864 painter.end();
865 }
866
resizeEvent(QResizeEvent * re)867 void PresentationWidget::resizeEvent(QResizeEvent *re)
868 {
869 m_width = width();
870 m_height = height();
871
872 // if by chance the new size equals the old, do not invalidate pixmaps and such..
873 if (size() == re->oldSize())
874 return;
875
876 // BEGIN Top toolbar
877 // tool bar height in pixels, make it large enough to hold the text fields with the page numbers
878 const int toolBarHeight = m_pagesEdit->height() * 1.5;
879
880 m_topBar->setGeometry(0, 0, width(), toolBarHeight);
881 m_topBar->setIconSize(QSize(toolBarHeight * 0.75, toolBarHeight * 0.75));
882 // END Top toolbar
883
884 // BEGIN Content area
885 // update the frames
886 const float screenRatio = (float)m_height / (float)m_width;
887 for (PresentationFrame *frame : qAsConst(m_frames)) {
888 frame->recalcGeometry(m_width, m_height, screenRatio);
889 }
890
891 if (m_frameIndex != -1) {
892 // ugliness alarm!
893 const_cast<Okular::Page *>(m_frames[m_frameIndex]->page)->deletePixmap(this);
894 // force the regeneration of the pixmap
895 m_lastRenderedPixmap = QPixmap();
896 m_blockNotifications = true;
897 requestPixmaps();
898 m_blockNotifications = false;
899 }
900
901 if (m_transitionTimer->isActive()) {
902 m_transitionTimer->stop();
903 }
904
905 generatePage(true /* no transitions */);
906 // END Content area
907 }
908
enterEvent(QEvent * e)909 void PresentationWidget::enterEvent(QEvent *e)
910 {
911 if (!m_topBar->isHidden()) {
912 QEnterEvent *ee = static_cast<QEnterEvent *>(e);
913 // This can happen when we exited the widget via a "tooltip" and the tooltip disappears
914 if (ee->y() > (m_topBar->height() + 1)) {
915 showTopBar(false);
916 }
917 }
918 QWidget::enterEvent(e);
919 }
920
leaveEvent(QEvent * e)921 void PresentationWidget::leaveEvent(QEvent *e)
922 {
923 Q_UNUSED(e)
924
925 if (!m_topBar->isHidden()) {
926 if (QToolTip::isVisible()) {
927 // make sure we're not hovering over the tooltip
928 // because the world is sad, this works differently on Wayland and X11
929 // on X11 the widget under the cursor is the tooltip window
930 // on wayland it's the button generating the tooltip (why? no idea)
931 const QWidget *widgetUnderCursor = qApp->widgetAt(QCursor::pos());
932 if (widgetUnderCursor) {
933 const QWidget *widgetUnderCursorWindow = widgetUnderCursor->window();
934 if (widgetUnderCursorWindow == this) {
935 qDebug() << "Wayland";
936 return;
937 } else {
938 const QWidget *widgetUnderCursorParentWindow = widgetUnderCursorWindow->parentWidget() ? widgetUnderCursorWindow->parentWidget()->window() : nullptr;
939 if (widgetUnderCursorParentWindow == this) {
940 qDebug() << "X11";
941 return;
942 }
943 }
944 }
945 }
946 showTopBar(false);
947 }
948 }
949 // </widget events>
950
getObjectRect(Okular::ObjectRect::ObjectType type,int x,int y,QRect * geometry) const951 const void *PresentationWidget::getObjectRect(Okular::ObjectRect::ObjectType type, int x, int y, QRect *geometry) const
952 {
953 // no links on invalid pages
954 if (geometry && !geometry->isNull())
955 geometry->setRect(0, 0, 0, 0);
956 if (m_frameIndex < 0 || m_frameIndex >= (int)m_frames.size())
957 return nullptr;
958
959 // get frame, page and geometry
960 const PresentationFrame *frame = m_frames[m_frameIndex];
961 const Okular::Page *page = frame->page;
962 const QRect &frameGeometry = frame->geometry;
963
964 // compute normalized x and y
965 double nx = (double)(x - frameGeometry.left()) / (double)frameGeometry.width();
966 double ny = (double)(y - frameGeometry.top()) / (double)frameGeometry.height();
967
968 // no links outside the pages
969 if (nx < 0 || nx > 1 || ny < 0 || ny > 1)
970 return nullptr;
971
972 // check if 1) there is an object and 2) it's a link
973 const QRect screenRect = oldQt_screenOf(this)->geometry();
974 const Okular::ObjectRect *object = page->objectRect(type, nx, ny, screenRect.width(), screenRect.height());
975 if (!object)
976 return nullptr;
977
978 // compute link geometry if destination rect present
979 if (geometry) {
980 *geometry = object->boundingRect(frameGeometry.width(), frameGeometry.height());
981 geometry->translate(frameGeometry.left(), frameGeometry.top());
982 }
983
984 // return the link pointer
985 return object->object();
986 }
987
getLink(int x,int y,QRect * geometry) const988 const Okular::Action *PresentationWidget::getLink(int x, int y, QRect *geometry) const
989 {
990 return reinterpret_cast<const Okular::Action *>(getObjectRect(Okular::ObjectRect::Action, x, y, geometry));
991 }
992
getAnnotation(int x,int y,QRect * geometry) const993 const Okular::Annotation *PresentationWidget::getAnnotation(int x, int y, QRect *geometry) const
994 {
995 return reinterpret_cast<const Okular::Annotation *>(getObjectRect(Okular::ObjectRect::OAnnotation, x, y, geometry));
996 }
997
testCursorOnLink(int x,int y)998 void PresentationWidget::testCursorOnLink(int x, int y)
999 {
1000 const Okular::Action *link = getLink(x, y, nullptr);
1001 const Okular::Annotation *annotation = getAnnotation(x, y, nullptr);
1002
1003 const bool needsHandCursor = ((link != nullptr) || ((annotation != nullptr) && (annotation->subType() == Okular::Annotation::AMovie)) || ((annotation != nullptr) && (annotation->subType() == Okular::Annotation::ARichMedia)) ||
1004 ((annotation != nullptr) && (annotation->subType() == Okular::Annotation::AScreen) && (GuiUtils::renditionMovieFromScreenAnnotation(static_cast<const Okular::ScreenAnnotation *>(annotation)) != nullptr)));
1005
1006 // only react on changes (in/out from a link)
1007 if ((needsHandCursor && !m_handCursor) || (!needsHandCursor && m_handCursor)) {
1008 // change cursor shape
1009 m_handCursor = needsHandCursor;
1010 setCursor(QCursor(m_handCursor ? Qt::PointingHandCursor : Qt::ArrowCursor));
1011 }
1012 }
1013
overlayClick(const QPoint position)1014 void PresentationWidget::overlayClick(const QPoint position)
1015 {
1016 // clicking the progress indicator
1017 int xPos = position.x() - m_overlayGeometry.x() - m_overlayGeometry.width() / 2, yPos = m_overlayGeometry.height() / 2 - position.y();
1018 if (!xPos && !yPos)
1019 return;
1020
1021 // compute angle relative to indicator (note coord transformation)
1022 float angle = 0.5 + 0.5 * atan2((double)-xPos, (double)-yPos) / M_PI;
1023 int pageIndex = (int)(angle * (m_frames.count() - 1) + 0.5);
1024
1025 // go to selected page
1026 changePage(pageIndex);
1027 }
1028
changePage(int newPage)1029 void PresentationWidget::changePage(int newPage)
1030 {
1031 if (m_showSummaryView) {
1032 m_showSummaryView = false;
1033 m_frameIndex = -1;
1034 return;
1035 }
1036
1037 if (m_frameIndex == newPage)
1038 return;
1039
1040 // switch to newPage
1041 m_document->setViewportPage(newPage, this);
1042
1043 if ((Okular::Settings::slidesShowSummary() && !m_showSummaryView) || m_frameIndex == -1)
1044 notifyCurrentPageChanged(-1, newPage);
1045 }
1046
generatePage(bool disableTransition)1047 void PresentationWidget::generatePage(bool disableTransition)
1048 {
1049 if (m_lastRenderedPixmap.isNull()) {
1050 qreal dpr = devicePixelRatioF();
1051 m_lastRenderedPixmap = QPixmap(m_width * dpr, m_height * dpr);
1052 m_lastRenderedPixmap.setDevicePixelRatio(dpr);
1053
1054 m_previousPagePixmap = QPixmap();
1055 } else {
1056 m_previousPagePixmap = m_lastRenderedPixmap;
1057 }
1058
1059 // opens the painter over the pixmap
1060 QPainter pixmapPainter;
1061 pixmapPainter.begin(&m_lastRenderedPixmap);
1062 // generate welcome page
1063 if (m_frameIndex == -1)
1064 generateIntroPage(pixmapPainter);
1065 // generate a normal pixmap with extended margin filling
1066 if (m_frameIndex >= 0 && m_frameIndex < (int)m_document->pages())
1067 generateContentsPage(m_frameIndex, pixmapPainter);
1068 pixmapPainter.end();
1069
1070 // generate the top-right corner overlay
1071 #ifdef ENABLE_PROGRESS_OVERLAY
1072 if (Okular::Settings::slidesShowProgress() && m_frameIndex != -1)
1073 generateOverlay();
1074 #endif
1075
1076 // start transition on pages that have one
1077 disableTransition |= (Okular::Settings::slidesTransition() == Okular::Settings::EnumSlidesTransition::NoTransitions);
1078 if (!disableTransition) {
1079 const Okular::PageTransition *transition = m_frameIndex != -1 ? m_frames[m_frameIndex]->page->transition() : nullptr;
1080 if (transition)
1081 initTransition(transition);
1082 else {
1083 Okular::PageTransition trans = defaultTransition();
1084 initTransition(&trans);
1085 }
1086 } else {
1087 Okular::PageTransition trans = defaultTransition(Okular::Settings::EnumSlidesTransition::Replace);
1088 initTransition(&trans);
1089 }
1090
1091 // update cursor + tooltip
1092 if (!m_drawingEngine && Okular::Settings::slidesCursor() != Okular::Settings::EnumSlidesCursor::Hidden) {
1093 QPoint p = mapFromGlobal(QCursor::pos());
1094 testCursorOnLink(p.x(), p.y());
1095 }
1096 }
1097
generateIntroPage(QPainter & p)1098 void PresentationWidget::generateIntroPage(QPainter &p)
1099 {
1100 qreal dpr = devicePixelRatioF();
1101
1102 // use a vertical gray gradient background
1103 int blend1 = m_height / 10, blend2 = 9 * m_height / 10;
1104 int baseTint = QColor(Qt::gray).red();
1105 for (int i = 0; i < m_height; i++) {
1106 int k = baseTint;
1107 if (i < blend1)
1108 k -= (int)(baseTint * (i - blend1) * (i - blend1) / (float)(blend1 * blend1));
1109 if (i > blend2)
1110 k += (int)((255 - baseTint) * (i - blend2) * (i - blend2) / (float)(blend1 * blend1));
1111 p.fillRect(0, i, m_width, 1, QColor(k, k, k));
1112 }
1113
1114 // draw okular logo in the four corners
1115 QPixmap logo = QIcon::fromTheme(QStringLiteral("okular")).pixmap(64 * dpr);
1116 logo.setDevicePixelRatio(dpr);
1117 if (!logo.isNull()) {
1118 p.drawPixmap(5, 5, logo);
1119 p.drawPixmap(m_width - 5 - logo.width(), 5, logo);
1120 p.drawPixmap(m_width - 5 - logo.width(), m_height - 5 - logo.height(), logo);
1121 p.drawPixmap(5, m_height - 5 - logo.height(), logo);
1122 }
1123
1124 // draw metadata text (the last line is 'click to begin')
1125 int strNum = m_metaStrings.count(), strHeight = m_height / (strNum + 4), fontHeight = 2 * strHeight / 3;
1126 QFont font(p.font());
1127 font.setPixelSize(fontHeight);
1128 QFontMetrics metrics(font);
1129 for (int i = 0; i < strNum; i++) {
1130 // set a font to fit text width
1131 float wScale = (float)metrics.boundingRect(m_metaStrings[i]).width() / (float)m_width;
1132 QFont f(font);
1133 if (wScale > 1.0)
1134 f.setPixelSize((int)((float)fontHeight / (float)wScale));
1135 p.setFont(f);
1136
1137 // text shadow
1138 p.setPen(Qt::darkGray);
1139 p.drawText(2, m_height / 4 + strHeight * i + 2, m_width, strHeight, Qt::AlignHCenter | Qt::AlignVCenter, m_metaStrings[i]);
1140 // text body
1141 p.setPen(128 + (127 * i) / strNum);
1142 p.drawText(0, m_height / 4 + strHeight * i, m_width, strHeight, Qt::AlignHCenter | Qt::AlignVCenter, m_metaStrings[i]);
1143 }
1144 }
1145
generateContentsPage(int pageNum,QPainter & p)1146 void PresentationWidget::generateContentsPage(int pageNum, QPainter &p)
1147 {
1148 PresentationFrame *frame = m_frames[pageNum];
1149
1150 // translate painter and contents rect
1151 QRect geom(frame->geometry);
1152 p.translate(geom.left(), geom.top());
1153 geom.translate(-geom.left(), -geom.top());
1154
1155 // draw the page using the shared PagePainter class
1156 int flags = PagePainter::Accessibility | PagePainter::Highlights | PagePainter::Annotations;
1157
1158 PagePainter::paintPageOnPainter(&p, frame->page, this, flags, geom.width(), geom.height(), geom);
1159
1160 // restore painter
1161 p.translate(-frame->geometry.left(), -frame->geometry.top());
1162
1163 // fill unpainted areas with background color
1164 const QRegion unpainted(QRect(0, 0, m_width, m_height));
1165 const QRegion rgn = unpainted.subtracted(frame->geometry);
1166 for (const QRect &r : rgn) {
1167 p.fillRect(r, Okular::Settings::slidesBackgroundColor());
1168 }
1169 }
1170
1171 // from Arthur - Qt4 - (is defined elsewhere as 'qt_div_255' to not break final compilation)
qt_div255(int x)1172 inline int qt_div255(int x)
1173 {
1174 return (x + (x >> 8) + 0x80) >> 8;
1175 }
generateOverlay()1176 void PresentationWidget::generateOverlay()
1177 {
1178 #ifdef ENABLE_PROGRESS_OVERLAY
1179 qreal dpr = devicePixelRatioF();
1180
1181 // calculate overlay geometry and resize pixmap if needed
1182 double side = m_width / 16.0;
1183 m_overlayGeometry.setRect(m_width - side - 4, 4, side, side);
1184
1185 // note: to get a sort of antialiasing, we render the pixmap double sized
1186 // and the resulting image is smoothly scaled down. So here we open a
1187 // painter on the double sized pixmap.
1188 side *= 2;
1189
1190 QPixmap doublePixmap(side * dpr, side * dpr);
1191 doublePixmap.setDevicePixelRatio(dpr);
1192 doublePixmap.fill(Qt::black);
1193 QPainter pixmapPainter(&doublePixmap);
1194 pixmapPainter.setRenderHints(QPainter::Antialiasing);
1195
1196 // draw PIE SLICES in blue levels (the levels will then be the alpha component)
1197 int pages = m_document->pages();
1198 if (pages > 28) { // draw continuous slices
1199 int degrees = (int)(360 * (float)(m_frameIndex + 1) / (float)pages);
1200 pixmapPainter.setPen(0x05);
1201 pixmapPainter.setBrush(QColor(0x40));
1202 pixmapPainter.drawPie(2, 2, side - 4, side - 4, 90 * 16, (360 - degrees) * 16);
1203 pixmapPainter.setPen(0x40);
1204 pixmapPainter.setBrush(QColor(0xF0));
1205 pixmapPainter.drawPie(2, 2, side - 4, side - 4, 90 * 16, -degrees * 16);
1206 } else { // draw discrete slices
1207 float oldCoord = -90;
1208 for (int i = 0; i < pages; i++) {
1209 float newCoord = -90 + 360 * (float)(i + 1) / (float)pages;
1210 pixmapPainter.setPen(i <= m_frameIndex ? 0x40 : 0x05);
1211 pixmapPainter.setBrush(QColor(i <= m_frameIndex ? 0xF0 : 0x40));
1212 pixmapPainter.drawPie(2, 2, side - 4, side - 4, (int)(-16 * (oldCoord + 1)), (int)(-16 * (newCoord - (oldCoord + 2))));
1213 oldCoord = newCoord;
1214 }
1215 }
1216 int circleOut = side / 4;
1217 pixmapPainter.setPen(Qt::black);
1218 pixmapPainter.setBrush(Qt::black);
1219 pixmapPainter.drawEllipse(circleOut, circleOut, side - 2 * circleOut, side - 2 * circleOut);
1220
1221 // draw TEXT using maximum opacity
1222 QFont f(pixmapPainter.font());
1223 f.setPixelSize(side / 4);
1224 pixmapPainter.setFont(f);
1225 pixmapPainter.setPen(0xFF);
1226 // use a little offset to prettify output
1227 pixmapPainter.drawText(2, 2, side, side, Qt::AlignCenter, QString::number(m_frameIndex + 1));
1228
1229 // end drawing pixmap and halve image
1230 pixmapPainter.end();
1231 QImage image(doublePixmap.toImage().scaled((side / 2) * dpr, (side / 2) * dpr, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
1232 image.setDevicePixelRatio(dpr);
1233 image = image.convertToFormat(QImage::Format_ARGB32);
1234 image.setDevicePixelRatio(dpr);
1235
1236 // draw circular shadow using the same technique
1237 doublePixmap.fill(Qt::black);
1238 pixmapPainter.begin(&doublePixmap);
1239 pixmapPainter.setPen(0x40);
1240 pixmapPainter.setBrush(QColor(0x80));
1241 pixmapPainter.drawEllipse(0, 0, side, side);
1242 pixmapPainter.end();
1243 QImage shadow(doublePixmap.toImage().scaled((side / 2) * dpr, (side / 2) * dpr, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
1244 shadow.setDevicePixelRatio(dpr);
1245
1246 // generate a 2 colors pixmap using mixing shadow (made with highlight color)
1247 // and image (made with highlightedText color)
1248 QPalette pal = palette();
1249 QColor color = pal.color(QPalette::Active, QPalette::HighlightedText);
1250 int red = color.red(), green = color.green(), blue = color.blue();
1251 color = pal.color(QPalette::Active, QPalette::Highlight);
1252 int sRed = color.red(), sGreen = color.green(), sBlue = color.blue();
1253 // pointers
1254 unsigned int *data = reinterpret_cast<unsigned int *>(image.bits()), *shadowData = reinterpret_cast<unsigned int *>(shadow.bits()), pixels = image.width() * image.height();
1255 // cache data (reduce computation time to 26%!)
1256 int c1 = -1, c2 = -1, cR = 0, cG = 0, cB = 0, cA = 0;
1257 // foreach pixel
1258 for (unsigned int i = 0; i < pixels; ++i) {
1259 // alpha for shadow and image
1260 int shadowAlpha = shadowData[i] & 0xFF, srcAlpha = data[i] & 0xFF;
1261 // cache values
1262 if (srcAlpha != c1 || shadowAlpha != c2) {
1263 c1 = srcAlpha;
1264 c2 = shadowAlpha;
1265 // fuse color components and alpha value of image over shadow
1266 data[i] = qRgba(cR = qt_div255(srcAlpha * red + (255 - srcAlpha) * sRed),
1267 cG = qt_div255(srcAlpha * green + (255 - srcAlpha) * sGreen),
1268 cB = qt_div255(srcAlpha * blue + (255 - srcAlpha) * sBlue),
1269 cA = qt_div255(srcAlpha * srcAlpha + (255 - srcAlpha) * shadowAlpha));
1270 } else
1271 data[i] = qRgba(cR, cG, cB, cA);
1272 }
1273 m_lastRenderedOverlay = QPixmap::fromImage(image);
1274 m_lastRenderedOverlay.setDevicePixelRatio(dpr);
1275
1276 // start the autohide timer
1277 // repaint( m_overlayGeometry ); // toggle with next line
1278 update(m_overlayGeometry);
1279 m_overlayHideTimer->start(2500);
1280 #endif
1281 }
1282
routeMouseDrawingEvent(QMouseEvent * e)1283 QRect PresentationWidget::routeMouseDrawingEvent(QMouseEvent *e)
1284 {
1285 if (m_frameIndex == -1) // Can't draw on the summary page
1286 return QRect();
1287
1288 const QRect &geom = m_frames[m_frameIndex]->geometry;
1289 const Okular::Page *page = m_frames[m_frameIndex]->page;
1290
1291 AnnotatorEngine::EventType eventType;
1292 AnnotatorEngine::Button button;
1293 AnnotatorEngine::Modifiers modifiers;
1294
1295 // figure out the event type and button
1296 AnnotatorEngine::decodeEvent(e, &eventType, &button);
1297
1298 static bool hasclicked = false;
1299 if (eventType == AnnotatorEngine::Press)
1300 hasclicked = true;
1301
1302 QPointF mousePos = e->localPos();
1303 double nX = (mousePos.x() - (double)geom.left()) / (double)geom.width();
1304 double nY = (mousePos.y() - (double)geom.top()) / (double)geom.height();
1305 QRect ret;
1306 bool isInside = nX >= 0 && nX < 1 && nY >= 0 && nY < 1;
1307
1308 if (hasclicked && !isInside) {
1309 // Fake a move to the last border pos
1310 nX = qBound(0., nX, 1.);
1311 nY = qBound(0., nY, 1.);
1312 m_drawingEngine->event(AnnotatorEngine::Move, button, modifiers, nX, nY, geom.width(), geom.height(), page);
1313
1314 // Fake a release in the following lines
1315 eventType = AnnotatorEngine::Release;
1316 isInside = true;
1317 } else if (!hasclicked && isInside) {
1318 // we're coming from the outside, pretend we started clicking at the closest border
1319 if (nX < (1 - nX) && nX < nY && nX < (1 - nY))
1320 nX = 0;
1321 else if (nY < (1 - nY) && nY < nX && nY < (1 - nX))
1322 nY = 0;
1323 else if ((1 - nX) < nX && (1 - nX) < nY && (1 - nX) < (1 - nY))
1324 nX = 1;
1325 else
1326 nY = 1;
1327
1328 hasclicked = true;
1329 eventType = AnnotatorEngine::Press;
1330 }
1331
1332 if (hasclicked && isInside) {
1333 ret = m_drawingEngine->event(eventType, button, modifiers, nX, nY, geom.width(), geom.height(), page);
1334 }
1335
1336 if (eventType == AnnotatorEngine::Release) {
1337 hasclicked = false;
1338 }
1339
1340 if (m_drawingEngine->creationCompleted()) {
1341 // add drawing to current page
1342 m_frames[m_frameIndex]->drawings << m_drawingEngine->endSmoothPath();
1343
1344 // remove the actual drawer and create a new one just after
1345 // that - that gives continuous drawing
1346 delete m_drawingEngine;
1347 m_drawingRect = QRect();
1348 m_drawingEngine = new SmoothPathEngine(m_currentDrawingToolElement);
1349
1350 // schedule repaint
1351 update();
1352 }
1353
1354 return ret;
1355 }
1356
startAutoChangeTimer()1357 void PresentationWidget::startAutoChangeTimer()
1358 {
1359 double pageDuration = m_frameIndex >= 0 && m_frameIndex < (int)m_frames.count() ? m_frames[m_frameIndex]->page->duration() : -1;
1360 if (m_advanceSlides || pageDuration >= 0.0) {
1361 double secs;
1362 if (pageDuration < 0.0)
1363 secs = Okular::SettingsCore::slidesAdvanceTime();
1364 else if (m_advanceSlides)
1365 secs = qMin<double>(pageDuration, Okular::SettingsCore::slidesAdvanceTime());
1366 else
1367 secs = pageDuration;
1368
1369 m_nextPageTimer->start((int)(secs * 1000));
1370 }
1371 setPlayPauseIcon();
1372 }
1373
defaultScreen() const1374 QScreen *PresentationWidget::defaultScreen() const
1375 {
1376 const int preferenceScreen = Okular::Settings::slidesScreen();
1377
1378 if (preferenceScreen == -2) {
1379 return oldQt_screenOf(m_parentWidget);
1380 } else if (preferenceScreen == -1) {
1381 return QApplication::primaryScreen();
1382 } else if (preferenceScreen >= 0 && preferenceScreen < QApplication::screens().count()) {
1383 return QApplication::screens().at(preferenceScreen);
1384 } else {
1385 return oldQt_screenOf(m_parentWidget);
1386 }
1387 }
1388
requestPixmaps()1389 void PresentationWidget::requestPixmaps()
1390 {
1391 const qreal dpr = devicePixelRatioF();
1392 PresentationFrame *frame = m_frames[m_frameIndex];
1393 int pixW = frame->geometry.width();
1394 int pixH = frame->geometry.height();
1395
1396 // operation will take long: set busy cursor
1397 QApplication::setOverrideCursor(QCursor(Qt::BusyCursor));
1398 // request the pixmap
1399 QLinkedList<Okular::PixmapRequest *> requests;
1400 requests.push_back(new Okular::PixmapRequest(this, m_frameIndex, pixW, pixH, dpr, PRESENTATION_PRIO, Okular::PixmapRequest::NoFeature));
1401 // restore cursor
1402 QApplication::restoreOverrideCursor();
1403 // ask for next and previous page if not in low memory usage setting
1404 if (Okular::SettingsCore::memoryLevel() != Okular::SettingsCore::EnumMemoryLevel::Low) {
1405 int pagesToPreload = 1;
1406
1407 // If greedy, preload everything
1408 if (Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Greedy)
1409 pagesToPreload = (int)m_document->pages();
1410
1411 Okular::PixmapRequest::PixmapRequestFeatures requestFeatures = Okular::PixmapRequest::Preload;
1412 requestFeatures |= Okular::PixmapRequest::Asynchronous;
1413
1414 for (int j = 1; j <= pagesToPreload; j++) {
1415 int tailRequest = m_frameIndex + j;
1416 if (tailRequest < (int)m_document->pages()) {
1417 PresentationFrame *nextFrame = m_frames[tailRequest];
1418 pixW = nextFrame->geometry.width();
1419 pixH = nextFrame->geometry.height();
1420 if (!nextFrame->page->hasPixmap(this, pixW, pixH))
1421 requests.push_back(new Okular::PixmapRequest(this, tailRequest, pixW, pixH, dpr, PRESENTATION_PRELOAD_PRIO, requestFeatures));
1422 }
1423
1424 int headRequest = m_frameIndex - j;
1425 if (headRequest >= 0) {
1426 PresentationFrame *prevFrame = m_frames[headRequest];
1427 pixW = prevFrame->geometry.width();
1428 pixH = prevFrame->geometry.height();
1429 if (!prevFrame->page->hasPixmap(this, pixW, pixH))
1430 requests.push_back(new Okular::PixmapRequest(this, headRequest, pixW, pixH, dpr, PRESENTATION_PRELOAD_PRIO, requestFeatures));
1431 }
1432
1433 // stop if we've already reached both ends of the document
1434 if (headRequest < 0 && tailRequest >= (int)m_document->pages())
1435 break;
1436 }
1437 }
1438 m_document->requestPixmaps(requests);
1439 }
1440
slotNextPage()1441 void PresentationWidget::slotNextPage()
1442 {
1443 int nextIndex = m_frameIndex + 1;
1444
1445 // loop when configured
1446 if (nextIndex == m_frames.count() && Okular::Settings::slidesLoop())
1447 nextIndex = 0;
1448
1449 if (nextIndex < m_frames.count()) {
1450 // go to next page
1451 changePage(nextIndex);
1452 // auto advance to the next page if set
1453 startAutoChangeTimer();
1454 } else {
1455 #ifdef ENABLE_PROGRESS_OVERLAY
1456 if (Okular::Settings::slidesShowProgress())
1457 generateOverlay();
1458 #endif
1459 if (m_transitionTimer->isActive()) {
1460 m_transitionTimer->stop();
1461 m_lastRenderedPixmap = m_currentPagePixmap;
1462 update();
1463 }
1464 }
1465 // we need the setFocus() call here to let KCursor::autoHide() work correctly
1466 setFocus();
1467 }
1468
slotPrevPage()1469 void PresentationWidget::slotPrevPage()
1470 {
1471 if (m_frameIndex > 0) {
1472 // go to previous page
1473 changePage(m_frameIndex - 1);
1474
1475 // auto advance to the next page if set
1476 startAutoChangeTimer();
1477 } else {
1478 #ifdef ENABLE_PROGRESS_OVERLAY
1479 if (Okular::Settings::slidesShowProgress())
1480 generateOverlay();
1481 #endif
1482 if (m_transitionTimer->isActive()) {
1483 m_transitionTimer->stop();
1484 m_lastRenderedPixmap = m_currentPagePixmap;
1485 update();
1486 }
1487 }
1488 }
1489
slotFirstPage()1490 void PresentationWidget::slotFirstPage()
1491 {
1492 changePage(0);
1493 }
1494
slotLastPage()1495 void PresentationWidget::slotLastPage()
1496 {
1497 changePage((int)m_frames.count() - 1);
1498 }
1499
slotHideOverlay()1500 void PresentationWidget::slotHideOverlay()
1501 {
1502 QRect geom(m_overlayGeometry);
1503 m_overlayGeometry.setCoords(0, 0, -1, -1);
1504 update(geom);
1505 }
1506
slotTransitionStep()1507 void PresentationWidget::slotTransitionStep()
1508 {
1509 switch (m_currentTransition.type()) {
1510 case Okular::PageTransition::Fade: {
1511 QPainter pixmapPainter;
1512 m_currentPixmapOpacity += 1.0 / m_transitionSteps;
1513 m_lastRenderedPixmap = QPixmap(m_lastRenderedPixmap.size());
1514 m_lastRenderedPixmap.setDevicePixelRatio(devicePixelRatioF());
1515 m_lastRenderedPixmap.fill(Qt::transparent);
1516 pixmapPainter.begin(&m_lastRenderedPixmap);
1517 pixmapPainter.setCompositionMode(QPainter::CompositionMode_Source);
1518 pixmapPainter.setOpacity(1 - m_currentPixmapOpacity);
1519 pixmapPainter.drawPixmap(0, 0, m_previousPagePixmap);
1520 pixmapPainter.setOpacity(m_currentPixmapOpacity);
1521 pixmapPainter.drawPixmap(0, 0, m_currentPagePixmap);
1522 update();
1523 if (m_currentPixmapOpacity >= 1)
1524 return;
1525 } break;
1526 default: {
1527 if (m_transitionRects.empty()) {
1528 // it's better to fix the transition to cover the whole screen than
1529 // enabling the following line that wastes cpu for nothing
1530 // update();
1531 return;
1532 }
1533
1534 for (int i = 0; i < m_transitionMul && !m_transitionRects.empty(); i++) {
1535 update(m_transitionRects.first());
1536 m_transitionRects.pop_front();
1537 }
1538 } break;
1539 }
1540 m_transitionTimer->start(m_transitionDelay);
1541 }
1542
slotDelayedEvents()1543 void PresentationWidget::slotDelayedEvents()
1544 {
1545 setScreen(defaultScreen());
1546 show();
1547
1548 if (m_screenSelect) {
1549 m_screenSelect->setCurrentItem(QApplication::screens().indexOf(oldQt_screenOf(this)));
1550 connect(m_screenSelect->selectableActionGroup(), &QActionGroup::triggered, this, &PresentationWidget::chooseScreen);
1551 }
1552
1553 // inform user on how to exit from presentation mode
1554 KMessageBox::information(
1555 this,
1556 i18n("There are two ways of exiting presentation mode, you can press either ESC key or click with the quit button that appears when placing the mouse in the top-right corner. Of course you can cycle windows (Alt+TAB by default)"),
1557 QString(),
1558 QStringLiteral("presentationInfo"));
1559 }
1560
slotPageChanged()1561 void PresentationWidget::slotPageChanged()
1562 {
1563 bool ok = true;
1564 int p = m_pagesEdit->text().toInt(&ok);
1565 if (!ok)
1566 return;
1567
1568 changePage(p - 1);
1569 }
1570
slotChangeDrawingToolEngine(const QDomElement & element)1571 void PresentationWidget::slotChangeDrawingToolEngine(const QDomElement &element)
1572 {
1573 if (element.isNull()) {
1574 delete m_drawingEngine;
1575 m_drawingEngine = nullptr;
1576 m_drawingRect = QRect();
1577 setCursor(Qt::ArrowCursor);
1578 } else {
1579 m_drawingEngine = new SmoothPathEngine(element);
1580 setCursor(QCursor(QPixmap(QStringLiteral("pencil")), Qt::ArrowCursor));
1581 m_currentDrawingToolElement = element;
1582 }
1583 }
1584
slotAddDrawingToolActions()1585 void PresentationWidget::slotAddDrawingToolActions()
1586 {
1587 DrawingToolActions *drawingToolActions = qobject_cast<DrawingToolActions *>(sender());
1588
1589 const QList<QAction *> actionsList = drawingToolActions->actions();
1590 for (QAction *action : actionsList) {
1591 action->setEnabled(true);
1592 m_topBar->addAction(action);
1593 addAction(action);
1594 }
1595 }
1596
clearDrawings()1597 void PresentationWidget::clearDrawings()
1598 {
1599 if (m_frameIndex != -1)
1600 m_frames[m_frameIndex]->drawings.clear();
1601 update();
1602 }
1603
chooseScreen(QAction * act)1604 void PresentationWidget::chooseScreen(QAction *act)
1605 {
1606 if (!act || act->data().type() != QVariant::Int)
1607 return;
1608
1609 const int newScreen = act->data().toInt();
1610 if (newScreen < QApplication::screens().count()) {
1611 setScreen(QApplication::screens().at(newScreen));
1612 }
1613 }
1614
toggleBlackScreenMode(bool)1615 void PresentationWidget::toggleBlackScreenMode(bool)
1616 {
1617 m_inBlackScreenMode = !m_inBlackScreenMode;
1618
1619 update();
1620 }
1621
setScreen(const QScreen * newScreen)1622 void PresentationWidget::setScreen(const QScreen *newScreen)
1623 {
1624 // To move to a new screen, need to disable fullscreen first:
1625 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
1626 if (newScreen != screen()) {
1627 #else
1628 if (true) { // TODO Qt6 (oldQt_screenOf() doesn’t help here, because it has a default value.)
1629 #endif
1630 setWindowState(windowState() & ~Qt::WindowFullScreen);
1631 }
1632 setGeometry(newScreen->geometry());
1633 setWindowState(windowState() | Qt::WindowFullScreen);
1634 }
1635
1636 void PresentationWidget::inhibitPowerManagement()
1637 {
1638 #ifdef Q_OS_LINUX
1639 QString reason = i18nc("Reason for inhibiting the screensaver activation, when the presentation mode is active", "Giving a presentation");
1640
1641 if (!m_screenInhibitCookie) {
1642 QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("/ScreenSaver"), QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("Inhibit"));
1643 message << QCoreApplication::applicationName();
1644 message << reason;
1645
1646 QDBusPendingReply<uint> reply = QDBusConnection::sessionBus().asyncCall(message);
1647 reply.waitForFinished();
1648 if (reply.isValid()) {
1649 m_screenInhibitCookie = reply.value();
1650 qCDebug(OkularUiDebug) << "Screen inhibition cookie" << m_screenInhibitCookie;
1651 } else {
1652 qCWarning(OkularUiDebug) << "Unable to inhibit screensaver" << reply.error();
1653 }
1654 }
1655
1656 if (m_sleepInhibitFd != -1) {
1657 QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.login1"), QStringLiteral("/org/freedesktop/login1"), QStringLiteral("org.freedesktop.login1.Manager"), QStringLiteral("Inhibit"));
1658 message << QStringLiteral("sleep");
1659 message << QCoreApplication::applicationName();
1660 message << reason;
1661 message << QStringLiteral("block");
1662
1663 QDBusPendingReply<QDBusUnixFileDescriptor> reply = QDBusConnection::systemBus().asyncCall(message);
1664 reply.waitForFinished();
1665 if (reply.isValid()) {
1666 m_sleepInhibitFd = dup(reply.value().fileDescriptor());
1667 } else {
1668 qCWarning(OkularUiDebug) << "Unable to inhibit sleep" << reply.error();
1669 }
1670 }
1671 #endif
1672 }
1673
1674 void PresentationWidget::allowPowerManagement()
1675 {
1676 #ifdef Q_OS_LINUX
1677 if (m_sleepInhibitFd != -1) {
1678 ::close(m_sleepInhibitFd);
1679 m_sleepInhibitFd = -1;
1680 }
1681
1682 if (m_screenInhibitCookie) {
1683 QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("/ScreenSaver"), QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("UnInhibit"));
1684 message << m_screenInhibitCookie;
1685
1686 QDBusPendingReply<uint> reply = QDBusConnection::sessionBus().asyncCall(message);
1687 reply.waitForFinished();
1688
1689 m_screenInhibitCookie = 0;
1690 }
1691 #endif
1692 }
1693
1694 void PresentationWidget::showTopBar(bool show)
1695 {
1696 if (show) {
1697 m_topBar->show();
1698
1699 // Don't autohide the mouse cursor if it's over the toolbar
1700 if (Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::HiddenDelay) {
1701 KCursor::setAutoHideCursor(this, false);
1702 }
1703
1704 // Always show a cursor when topBar is visible
1705 if (!m_drawingEngine) {
1706 setCursor(QCursor(Qt::ArrowCursor));
1707 }
1708 } else {
1709 m_topBar->hide();
1710
1711 // Reenable autohide if need be when leaving the toolbar
1712 if (Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::HiddenDelay) {
1713 KCursor::setAutoHideCursor(this, true);
1714 }
1715
1716 // Or hide the cursor again if hidden cursor is enabled
1717 else if (Okular::Settings::slidesCursor() == Okular::Settings::EnumSlidesCursor::Hidden) {
1718 // Don't hide the cursor if drawing mode is on
1719 if (!m_drawingEngine) {
1720 setCursor(QCursor(Qt::BlankCursor));
1721 }
1722 }
1723 }
1724
1725 // Make sure mouse tracking isn't off after the KCursor::setAutoHideCursor() calls
1726 setMouseTracking(true);
1727 }
1728
1729 void PresentationWidget::slotFind()
1730 {
1731 if (!m_searchBar) {
1732 m_searchBar = new PresentationSearchBar(m_document, this, this);
1733 m_searchBar->forceSnap();
1734 }
1735 m_searchBar->focusOnSearchEdit();
1736 m_searchBar->show();
1737 }
1738
1739 const Okular::PageTransition PresentationWidget::defaultTransition() const
1740 {
1741 return defaultTransition(Okular::Settings::slidesTransition());
1742 }
1743
1744 const Okular::PageTransition PresentationWidget::defaultTransition(int type) const
1745 {
1746 switch (type) {
1747 case Okular::Settings::EnumSlidesTransition::BlindsHorizontal: {
1748 Okular::PageTransition transition(Okular::PageTransition::Blinds);
1749 transition.setAlignment(Okular::PageTransition::Horizontal);
1750 return transition;
1751 break;
1752 }
1753 case Okular::Settings::EnumSlidesTransition::BlindsVertical: {
1754 Okular::PageTransition transition(Okular::PageTransition::Blinds);
1755 transition.setAlignment(Okular::PageTransition::Vertical);
1756 return transition;
1757 break;
1758 }
1759 case Okular::Settings::EnumSlidesTransition::BoxIn: {
1760 Okular::PageTransition transition(Okular::PageTransition::Box);
1761 transition.setDirection(Okular::PageTransition::Inward);
1762 return transition;
1763 break;
1764 }
1765 case Okular::Settings::EnumSlidesTransition::BoxOut: {
1766 Okular::PageTransition transition(Okular::PageTransition::Box);
1767 transition.setDirection(Okular::PageTransition::Outward);
1768 return transition;
1769 break;
1770 }
1771 case Okular::Settings::EnumSlidesTransition::Dissolve: {
1772 return Okular::PageTransition(Okular::PageTransition::Dissolve);
1773 break;
1774 }
1775 case Okular::Settings::EnumSlidesTransition::GlitterDown: {
1776 Okular::PageTransition transition(Okular::PageTransition::Glitter);
1777 transition.setAngle(270);
1778 return transition;
1779 break;
1780 }
1781 case Okular::Settings::EnumSlidesTransition::GlitterRight: {
1782 Okular::PageTransition transition(Okular::PageTransition::Glitter);
1783 transition.setAngle(0);
1784 return transition;
1785 break;
1786 }
1787 case Okular::Settings::EnumSlidesTransition::GlitterRightDown: {
1788 Okular::PageTransition transition(Okular::PageTransition::Glitter);
1789 transition.setAngle(315);
1790 return transition;
1791 break;
1792 }
1793 case Okular::Settings::EnumSlidesTransition::Random: {
1794 return defaultTransition(KRandom::random() % 18);
1795 break;
1796 }
1797 case Okular::Settings::EnumSlidesTransition::SplitHorizontalIn: {
1798 Okular::PageTransition transition(Okular::PageTransition::Split);
1799 transition.setAlignment(Okular::PageTransition::Horizontal);
1800 transition.setDirection(Okular::PageTransition::Inward);
1801 return transition;
1802 break;
1803 }
1804 case Okular::Settings::EnumSlidesTransition::SplitHorizontalOut: {
1805 Okular::PageTransition transition(Okular::PageTransition::Split);
1806 transition.setAlignment(Okular::PageTransition::Horizontal);
1807 transition.setDirection(Okular::PageTransition::Outward);
1808 return transition;
1809 break;
1810 }
1811 case Okular::Settings::EnumSlidesTransition::SplitVerticalIn: {
1812 Okular::PageTransition transition(Okular::PageTransition::Split);
1813 transition.setAlignment(Okular::PageTransition::Vertical);
1814 transition.setDirection(Okular::PageTransition::Inward);
1815 return transition;
1816 break;
1817 }
1818 case Okular::Settings::EnumSlidesTransition::SplitVerticalOut: {
1819 Okular::PageTransition transition(Okular::PageTransition::Split);
1820 transition.setAlignment(Okular::PageTransition::Vertical);
1821 transition.setDirection(Okular::PageTransition::Outward);
1822 return transition;
1823 break;
1824 }
1825 case Okular::Settings::EnumSlidesTransition::WipeDown: {
1826 Okular::PageTransition transition(Okular::PageTransition::Wipe);
1827 transition.setAngle(270);
1828 return transition;
1829 break;
1830 }
1831 case Okular::Settings::EnumSlidesTransition::WipeRight: {
1832 Okular::PageTransition transition(Okular::PageTransition::Wipe);
1833 transition.setAngle(0);
1834 return transition;
1835 break;
1836 }
1837 case Okular::Settings::EnumSlidesTransition::WipeLeft: {
1838 Okular::PageTransition transition(Okular::PageTransition::Wipe);
1839 transition.setAngle(180);
1840 return transition;
1841 break;
1842 }
1843 case Okular::Settings::EnumSlidesTransition::WipeUp: {
1844 Okular::PageTransition transition(Okular::PageTransition::Wipe);
1845 transition.setAngle(90);
1846 return transition;
1847 break;
1848 }
1849 case Okular::Settings::EnumSlidesTransition::Fade: {
1850 return Okular::PageTransition(Okular::PageTransition::Fade);
1851 break;
1852 }
1853 case Okular::Settings::EnumSlidesTransition::NoTransitions:
1854 case Okular::Settings::EnumSlidesTransition::Replace:
1855 default:
1856 return Okular::PageTransition(Okular::PageTransition::Replace);
1857 break;
1858 }
1859 // should not happen, just make gcc happy
1860 return Okular::PageTransition();
1861 }
1862
1863 /** ONLY the TRANSITIONS GENERATION function from here on **/
1864 void PresentationWidget::initTransition(const Okular::PageTransition *transition)
1865 {
1866 // if it's just a 'replace' transition, repaint the screen
1867 if (transition->type() == Okular::PageTransition::Replace) {
1868 update();
1869 return;
1870 }
1871
1872 const bool isInward = transition->direction() == Okular::PageTransition::Inward;
1873 const bool isHorizontal = transition->alignment() == Okular::PageTransition::Horizontal;
1874 const float totalTime = transition->duration();
1875
1876 m_transitionRects.clear();
1877 m_currentTransition = *transition;
1878 m_currentPagePixmap = m_lastRenderedPixmap;
1879
1880 switch (transition->type()) {
1881 // split: horizontal / vertical and inward / outward
1882 case Okular::PageTransition::Split: {
1883 const int steps = isHorizontal ? 100 : 75;
1884 if (isHorizontal) {
1885 if (isInward) {
1886 int xPosition = 0;
1887 for (int i = 0; i < steps; i++) {
1888 int xNext = ((i + 1) * m_width) / (2 * steps);
1889 m_transitionRects.push_back(QRect(xPosition, 0, xNext - xPosition, m_height));
1890 m_transitionRects.push_back(QRect(m_width - xNext, 0, xNext - xPosition, m_height));
1891 xPosition = xNext;
1892 }
1893 } else {
1894 int xPosition = m_width / 2;
1895 for (int i = 0; i < steps; i++) {
1896 int xNext = ((steps - (i + 1)) * m_width) / (2 * steps);
1897 m_transitionRects.push_back(QRect(xNext, 0, xPosition - xNext, m_height));
1898 m_transitionRects.push_back(QRect(m_width - xPosition, 0, xPosition - xNext, m_height));
1899 xPosition = xNext;
1900 }
1901 }
1902 } else {
1903 if (isInward) {
1904 int yPosition = 0;
1905 for (int i = 0; i < steps; i++) {
1906 int yNext = ((i + 1) * m_height) / (2 * steps);
1907 m_transitionRects.push_back(QRect(0, yPosition, m_width, yNext - yPosition));
1908 m_transitionRects.push_back(QRect(0, m_height - yNext, m_width, yNext - yPosition));
1909 yPosition = yNext;
1910 }
1911 } else {
1912 int yPosition = m_height / 2;
1913 for (int i = 0; i < steps; i++) {
1914 int yNext = ((steps - (i + 1)) * m_height) / (2 * steps);
1915 m_transitionRects.push_back(QRect(0, yNext, m_width, yPosition - yNext));
1916 m_transitionRects.push_back(QRect(0, m_height - yPosition, m_width, yPosition - yNext));
1917 yPosition = yNext;
1918 }
1919 }
1920 }
1921 m_transitionMul = 2;
1922 m_transitionDelay = (int)((totalTime * 1000) / steps);
1923 } break;
1924
1925 // blinds: horizontal(l-to-r) / vertical(t-to-b)
1926 case Okular::PageTransition::Blinds: {
1927 const int blinds = isHorizontal ? 8 : 6;
1928 const int steps = m_width / (4 * blinds);
1929 if (isHorizontal) {
1930 int xPosition[8];
1931 for (int b = 0; b < blinds; b++)
1932 xPosition[b] = (b * m_width) / blinds;
1933
1934 for (int i = 0; i < steps; i++) {
1935 int stepOffset = (int)(((float)i * (float)m_width) / ((float)blinds * (float)steps));
1936 for (int b = 0; b < blinds; b++) {
1937 m_transitionRects.push_back(QRect(xPosition[b], 0, stepOffset, m_height));
1938 xPosition[b] = stepOffset + (b * m_width) / blinds;
1939 }
1940 }
1941 } else {
1942 int yPosition[6];
1943 for (int b = 0; b < blinds; b++)
1944 yPosition[b] = (b * m_height) / blinds;
1945
1946 for (int i = 0; i < steps; i++) {
1947 int stepOffset = (int)(((float)i * (float)m_height) / ((float)blinds * (float)steps));
1948 for (int b = 0; b < blinds; b++) {
1949 m_transitionRects.push_back(QRect(0, yPosition[b], m_width, stepOffset));
1950 yPosition[b] = stepOffset + (b * m_height) / blinds;
1951 }
1952 }
1953 }
1954 m_transitionMul = blinds;
1955 m_transitionDelay = (int)((totalTime * 1000) / steps);
1956 } break;
1957
1958 // box: inward / outward
1959 case Okular::PageTransition::Box: {
1960 const int steps = m_width / 10;
1961 if (isInward) {
1962 int L = 0, T = 0, R = m_width, B = m_height;
1963 for (int i = 0; i < steps; i++) {
1964 // compute shrunk box coords
1965 int newL = ((i + 1) * m_width) / (2 * steps);
1966 int newT = ((i + 1) * m_height) / (2 * steps);
1967 int newR = m_width - newL;
1968 int newB = m_height - newT;
1969 // add left, right, topcenter, bottomcenter rects
1970 m_transitionRects.push_back(QRect(L, T, newL - L, B - T));
1971 m_transitionRects.push_back(QRect(newR, T, R - newR, B - T));
1972 m_transitionRects.push_back(QRect(newL, T, newR - newL, newT - T));
1973 m_transitionRects.push_back(QRect(newL, newB, newR - newL, B - newB));
1974 L = newL;
1975 T = newT;
1976 R = newR, B = newB;
1977 }
1978 } else {
1979 int L = m_width / 2, T = m_height / 2, R = L, B = T;
1980 for (int i = 0; i < steps; i++) {
1981 // compute shrunk box coords
1982 int newL = ((steps - (i + 1)) * m_width) / (2 * steps);
1983 int newT = ((steps - (i + 1)) * m_height) / (2 * steps);
1984 int newR = m_width - newL;
1985 int newB = m_height - newT;
1986 // add left, right, topcenter, bottomcenter rects
1987 m_transitionRects.push_back(QRect(newL, newT, L - newL, newB - newT));
1988 m_transitionRects.push_back(QRect(R, newT, newR - R, newB - newT));
1989 m_transitionRects.push_back(QRect(L, newT, R - L, T - newT));
1990 m_transitionRects.push_back(QRect(L, B, R - L, newB - B));
1991 L = newL;
1992 T = newT;
1993 R = newR, B = newB;
1994 }
1995 }
1996 m_transitionMul = 4;
1997 m_transitionDelay = (int)((totalTime * 1000) / steps);
1998 } break;
1999
2000 // wipe: implemented for 4 canonical angles
2001 case Okular::PageTransition::Wipe: {
2002 const int angle = transition->angle();
2003 const int steps = (angle == 0) || (angle == 180) ? m_width / 8 : m_height / 8;
2004 if (angle == 0) {
2005 int xPosition = 0;
2006 for (int i = 0; i < steps; i++) {
2007 int xNext = ((i + 1) * m_width) / steps;
2008 m_transitionRects.push_back(QRect(xPosition, 0, xNext - xPosition, m_height));
2009 xPosition = xNext;
2010 }
2011 } else if (angle == 90) {
2012 int yPosition = m_height;
2013 for (int i = 0; i < steps; i++) {
2014 int yNext = ((steps - (i + 1)) * m_height) / steps;
2015 m_transitionRects.push_back(QRect(0, yNext, m_width, yPosition - yNext));
2016 yPosition = yNext;
2017 }
2018 } else if (angle == 180) {
2019 int xPosition = m_width;
2020 for (int i = 0; i < steps; i++) {
2021 int xNext = ((steps - (i + 1)) * m_width) / steps;
2022 m_transitionRects.push_back(QRect(xNext, 0, xPosition - xNext, m_height));
2023 xPosition = xNext;
2024 }
2025 } else if (angle == 270) {
2026 int yPosition = 0;
2027 for (int i = 0; i < steps; i++) {
2028 int yNext = ((i + 1) * m_height) / steps;
2029 m_transitionRects.push_back(QRect(0, yPosition, m_width, yNext - yPosition));
2030 yPosition = yNext;
2031 }
2032 } else {
2033 update();
2034 return;
2035 }
2036 m_transitionMul = 1;
2037 m_transitionDelay = (int)((totalTime * 1000) / steps);
2038 } break;
2039
2040 // dissolve: replace 'random' rects
2041 case Okular::PageTransition::Dissolve: {
2042 const int gridXsteps = 50;
2043 const int gridYsteps = 38;
2044 const int steps = gridXsteps * gridYsteps;
2045 int oldX = 0;
2046 int oldY = 0;
2047 // create a grid of gridXstep by gridYstep QRects
2048 for (int y = 0; y < gridYsteps; y++) {
2049 int newY = (int)(m_height * ((float)(y + 1) / (float)gridYsteps));
2050 for (int x = 0; x < gridXsteps; x++) {
2051 int newX = (int)(m_width * ((float)(x + 1) / (float)gridXsteps));
2052 m_transitionRects.push_back(QRect(oldX, oldY, newX - oldX, newY - oldY));
2053 oldX = newX;
2054 }
2055 oldX = 0;
2056 oldY = newY;
2057 }
2058 // randomize the grid
2059 for (int i = 0; i < steps; i++) {
2060 #ifndef Q_OS_WIN
2061 int n1 = (int)(steps * drand48());
2062 int n2 = (int)(steps * drand48());
2063 #else
2064 int n1 = (int)(steps * (std::rand() / RAND_MAX));
2065 int n2 = (int)(steps * (std::rand() / RAND_MAX));
2066 #endif
2067 // swap items if index differs
2068 if (n1 != n2) {
2069 QRect r = m_transitionRects[n2];
2070 m_transitionRects[n2] = m_transitionRects[n1];
2071 m_transitionRects[n1] = r;
2072 }
2073 }
2074 // set global transition parameters
2075 m_transitionMul = 40;
2076 m_transitionDelay = (int)((m_transitionMul * 1000 * totalTime) / steps);
2077 } break;
2078
2079 // glitter: similar to dissolve but has a direction
2080 case Okular::PageTransition::Glitter: {
2081 const int gridXsteps = 50;
2082 const int gridYsteps = 38;
2083 const int steps = gridXsteps * gridYsteps;
2084 const int angle = transition->angle();
2085 // generate boxes using a given direction
2086 if (angle == 90) {
2087 int yPosition = m_height;
2088 for (int i = 0; i < gridYsteps; i++) {
2089 int yNext = ((gridYsteps - (i + 1)) * m_height) / gridYsteps;
2090 int xPosition = 0;
2091 for (int j = 0; j < gridXsteps; j++) {
2092 int xNext = ((j + 1) * m_width) / gridXsteps;
2093 m_transitionRects.push_back(QRect(xPosition, yNext, xNext - xPosition, yPosition - yNext));
2094 xPosition = xNext;
2095 }
2096 yPosition = yNext;
2097 }
2098 } else if (angle == 180) {
2099 int xPosition = m_width;
2100 for (int i = 0; i < gridXsteps; i++) {
2101 int xNext = ((gridXsteps - (i + 1)) * m_width) / gridXsteps;
2102 int yPosition = 0;
2103 for (int j = 0; j < gridYsteps; j++) {
2104 int yNext = ((j + 1) * m_height) / gridYsteps;
2105 m_transitionRects.push_back(QRect(xNext, yPosition, xPosition - xNext, yNext - yPosition));
2106 yPosition = yNext;
2107 }
2108 xPosition = xNext;
2109 }
2110 } else if (angle == 270) {
2111 int yPosition = 0;
2112 for (int i = 0; i < gridYsteps; i++) {
2113 int yNext = ((i + 1) * m_height) / gridYsteps;
2114 int xPosition = 0;
2115 for (int j = 0; j < gridXsteps; j++) {
2116 int xNext = ((j + 1) * m_width) / gridXsteps;
2117 m_transitionRects.push_back(QRect(xPosition, yPosition, xNext - xPosition, yNext - yPosition));
2118 xPosition = xNext;
2119 }
2120 yPosition = yNext;
2121 }
2122 } else // if angle is 0 or 315
2123 {
2124 int xPosition = 0;
2125 for (int i = 0; i < gridXsteps; i++) {
2126 int xNext = ((i + 1) * m_width) / gridXsteps;
2127 int yPosition = 0;
2128 for (int j = 0; j < gridYsteps; j++) {
2129 int yNext = ((j + 1) * m_height) / gridYsteps;
2130 m_transitionRects.push_back(QRect(xPosition, yPosition, xNext - xPosition, yNext - yPosition));
2131 yPosition = yNext;
2132 }
2133 xPosition = xNext;
2134 }
2135 }
2136 // add a 'glitter' (1 over 10 pieces is randomized)
2137 int randomSteps = steps / 20;
2138 for (int i = 0; i < randomSteps; i++) {
2139 #ifndef Q_OS_WIN
2140 int n1 = (int)(steps * drand48());
2141 int n2 = (int)(steps * drand48());
2142 #else
2143 int n1 = (int)(steps * (std::rand() / RAND_MAX));
2144 int n2 = (int)(steps * (std::rand() / RAND_MAX));
2145 #endif
2146 // swap items if index differs
2147 if (n1 != n2) {
2148 QRect r = m_transitionRects[n2];
2149 m_transitionRects[n2] = m_transitionRects[n1];
2150 m_transitionRects[n1] = r;
2151 }
2152 }
2153 // set global transition parameters
2154 m_transitionMul = (angle == 90) || (angle == 270) ? gridYsteps : gridXsteps;
2155 m_transitionMul /= 2;
2156 m_transitionDelay = (int)((m_transitionMul * 1000 * totalTime) / steps);
2157 } break;
2158
2159 case Okular::PageTransition::Fade: {
2160 enum { FADE_TRANSITION_FPS = 20 };
2161 const int steps = totalTime * FADE_TRANSITION_FPS;
2162 m_transitionSteps = steps;
2163 QPainter pixmapPainter;
2164 m_currentPixmapOpacity = (double)1 / steps;
2165 m_transitionDelay = (int)(totalTime * 1000) / steps;
2166 m_lastRenderedPixmap = QPixmap(m_lastRenderedPixmap.size());
2167 m_lastRenderedPixmap.fill(Qt::transparent);
2168 pixmapPainter.begin(&m_lastRenderedPixmap);
2169 pixmapPainter.setCompositionMode(QPainter::CompositionMode_Source);
2170 pixmapPainter.setOpacity(1 - m_currentPixmapOpacity);
2171 pixmapPainter.drawPixmap(0, 0, m_previousPagePixmap);
2172 pixmapPainter.setOpacity(m_currentPixmapOpacity);
2173 pixmapPainter.drawPixmap(0, 0, m_currentPagePixmap);
2174 pixmapPainter.end();
2175 update();
2176 } break;
2177 // implement missing transitions (a binary raster engine needed here)
2178 case Okular::PageTransition::Fly:
2179
2180 case Okular::PageTransition::Push:
2181
2182 case Okular::PageTransition::Cover:
2183
2184 case Okular::PageTransition::Uncover:
2185
2186 default:
2187 update();
2188 return;
2189 }
2190
2191 // send the first start to the timer
2192 m_transitionTimer->start(0);
2193 }
2194
2195 void PresentationWidget::slotProcessMovieAction(const Okular::MovieAction *action)
2196 {
2197 const Okular::MovieAnnotation *movieAnnotation = action->annotation();
2198 if (!movieAnnotation)
2199 return;
2200
2201 Okular::Movie *movie = movieAnnotation->movie();
2202 if (!movie)
2203 return;
2204
2205 VideoWidget *vw = m_frames[m_frameIndex]->videoWidgets.value(movieAnnotation->movie());
2206 if (!vw)
2207 return;
2208
2209 vw->show();
2210
2211 switch (action->operation()) {
2212 case Okular::MovieAction::Play:
2213 vw->stop();
2214 vw->play();
2215 break;
2216 case Okular::MovieAction::Stop:
2217 vw->stop();
2218 break;
2219 case Okular::MovieAction::Pause:
2220 vw->pause();
2221 break;
2222 case Okular::MovieAction::Resume:
2223 vw->play();
2224 break;
2225 };
2226 }
2227
2228 void PresentationWidget::slotProcessRenditionAction(const Okular::RenditionAction *action)
2229 {
2230 Okular::Movie *movie = action->movie();
2231 if (!movie)
2232 return;
2233
2234 VideoWidget *vw = m_frames[m_frameIndex]->videoWidgets.value(movie);
2235 if (!vw)
2236 return;
2237
2238 if (action->operation() == Okular::RenditionAction::None)
2239 return;
2240
2241 vw->show();
2242
2243 switch (action->operation()) {
2244 case Okular::RenditionAction::Play:
2245 vw->stop();
2246 vw->play();
2247 break;
2248 case Okular::RenditionAction::Stop:
2249 vw->stop();
2250 break;
2251 case Okular::RenditionAction::Pause:
2252 vw->pause();
2253 break;
2254 case Okular::RenditionAction::Resume:
2255 vw->play();
2256 break;
2257 default:
2258 return;
2259 };
2260 }
2261
2262 void PresentationWidget::slotTogglePlayPause()
2263 {
2264 if (!m_nextPageTimer->isActive()) {
2265 m_advanceSlides = true;
2266 startAutoChangeTimer();
2267 } else {
2268 m_nextPageTimer->stop();
2269 m_advanceSlides = false;
2270 setPlayPauseIcon();
2271 }
2272 }
2273
2274 #include "presentationwidget.moc"
2275