File indexing completed on 2024-04-28 15:52:00

0001 /*
0002     SPDX-FileCopyrightText: 2008 Pino Toscano <pino@kde.org>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "videowidget.h"
0008 
0009 // qt/kde includes
0010 #include <qaction.h>
0011 #include <qcoreapplication.h>
0012 #include <qdir.h>
0013 #include <qevent.h>
0014 #include <qlabel.h>
0015 #include <qlayout.h>
0016 #include <qmenu.h>
0017 #include <qstackedlayout.h>
0018 #include <qtoolbar.h>
0019 #include <qtoolbutton.h>
0020 #include <qwidgetaction.h>
0021 
0022 #include <KLocalizedString>
0023 #include <QIcon>
0024 
0025 #include "config-okular.h"
0026 
0027 #if HAVE_PHONON
0028 #include <phonon/mediaobject.h>
0029 #include <phonon/seekslider.h>
0030 #include <phonon/videoplayer.h>
0031 #endif
0032 
0033 #include "core/annotations.h"
0034 #include "core/area.h"
0035 #include "core/document.h"
0036 #include "core/movie.h"
0037 #include "snapshottaker.h"
0038 
0039 #if HAVE_PHONON
0040 
0041 static QAction *createToolBarButtonWithWidgetPopup(QToolBar *toolBar, QWidget *widget, const QIcon &icon)
0042 {
0043     QToolButton *button = new QToolButton(toolBar);
0044     QAction *action = toolBar->addWidget(button);
0045     button->setAutoRaise(true);
0046     button->setIcon(icon);
0047     button->setPopupMode(QToolButton::InstantPopup);
0048     QMenu *menu = new QMenu(button);
0049     button->setMenu(menu);
0050     QWidgetAction *widgetAction = new QWidgetAction(menu);
0051     QWidget *dummy = new QWidget(menu);
0052     widgetAction->setDefaultWidget(dummy);
0053     QVBoxLayout *dummyLayout = new QVBoxLayout(dummy);
0054     dummyLayout->setContentsMargins(5, 5, 5, 5);
0055     dummyLayout->addWidget(widget);
0056     menu->addAction(widgetAction);
0057     return action;
0058 }
0059 
0060 /* Private storage. */
0061 class VideoWidget::Private
0062 {
0063 public:
0064     Private(Okular::Movie *m, Okular::Document *doc, VideoWidget *qq)
0065         : q(qq)
0066         , movie(m)
0067         , document(doc)
0068         , player(nullptr)
0069         , loaded(false)
0070     {
0071     }
0072 
0073     ~Private()
0074     {
0075         if (player) {
0076             player->stop();
0077         }
0078     }
0079 
0080     enum PlayPauseMode { PlayMode, PauseMode };
0081 
0082     void load();
0083     void setupPlayPauseAction(PlayPauseMode mode);
0084     void setPosterImage(const QImage &);
0085     void takeSnapshot();
0086     void videoStopped();
0087     void stateChanged(Phonon::State newState);
0088 
0089     // slots
0090     void finished();
0091     void playOrPause();
0092 
0093     VideoWidget *q;
0094     Okular::Movie *movie;
0095     Okular::Document *document;
0096     Okular::NormalizedRect geom;
0097     Phonon::VideoPlayer *player;
0098     Phonon::SeekSlider *seekSlider;
0099     QToolBar *controlBar;
0100     QAction *playPauseAction;
0101     QAction *stopAction;
0102     QAction *seekSliderAction;
0103     QAction *seekSliderMenuAction;
0104     QStackedLayout *pageLayout;
0105     QLabel *posterImagePage;
0106     bool loaded : 1;
0107     double repetitionsLeft;
0108 };
0109 
0110 static QUrl urlFromUrlString(const QString &url, Okular::Document *document)
0111 {
0112     QUrl newurl;
0113     if (url.startsWith(QLatin1Char('/'))) {
0114         newurl = QUrl::fromLocalFile(url);
0115     } else {
0116         newurl = QUrl(url);
0117         if (newurl.isRelative()) {
0118             newurl = document->currentDocument().adjusted(QUrl::RemoveFilename);
0119             newurl.setPath(newurl.path() + url);
0120         }
0121     }
0122     return newurl;
0123 }
0124 
0125 void VideoWidget::Private::load()
0126 {
0127     repetitionsLeft = movie->playRepetitions();
0128     if (loaded) {
0129         return;
0130     }
0131 
0132     loaded = true;
0133 
0134     player->load(urlFromUrlString(movie->url(), document));
0135 
0136     connect(player->mediaObject(), &Phonon::MediaObject::stateChanged, q, [this](Phonon::State s) { stateChanged(s); });
0137 
0138     seekSlider->setEnabled(true);
0139 }
0140 
0141 void VideoWidget::Private::setupPlayPauseAction(PlayPauseMode mode)
0142 {
0143     if (mode == PlayMode) {
0144         playPauseAction->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-start")));
0145         playPauseAction->setText(i18nc("start the movie playback", "Play"));
0146     } else if (mode == PauseMode) {
0147         playPauseAction->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-pause")));
0148         playPauseAction->setText(i18nc("pause the movie playback", "Pause"));
0149     }
0150 }
0151 
0152 void VideoWidget::Private::takeSnapshot()
0153 {
0154     const QUrl url = urlFromUrlString(movie->url(), document);
0155     SnapshotTaker *taker = new SnapshotTaker(url, q);
0156 
0157     q->connect(taker, &SnapshotTaker::finished, q, [this](const QImage &image) { setPosterImage(image); });
0158 }
0159 
0160 void VideoWidget::Private::videoStopped()
0161 {
0162     if (movie->showPosterImage()) {
0163         pageLayout->setCurrentIndex(1);
0164     } else {
0165         q->hide();
0166     }
0167 }
0168 
0169 void VideoWidget::Private::finished()
0170 {
0171     switch (movie->playMode()) {
0172     case Okular::Movie::PlayLimited:
0173     case Okular::Movie::PlayOpen:
0174         repetitionsLeft -= 1.0;
0175         if (repetitionsLeft < 1e-5) { // allow for some calculation error
0176             // playback has ended
0177             stopAction->setEnabled(false);
0178             setupPlayPauseAction(PlayMode);
0179             if (movie->playMode() == Okular::Movie::PlayLimited) {
0180                 controlBar->setVisible(false);
0181             }
0182             videoStopped();
0183         } else {
0184             // not done yet, repeat
0185             // if repetitionsLeft is less than 1, we are supposed to stop midway, but not even Adobe reader does this
0186             player->play();
0187         }
0188         break;
0189     case Okular::Movie::PlayRepeat:
0190         // repeat the playback
0191         player->play();
0192         break;
0193     case Okular::Movie::PlayPalindrome:
0194         // FIXME we should play backward, but we cannot
0195         player->play();
0196         break;
0197     }
0198 }
0199 
0200 void VideoWidget::Private::playOrPause()
0201 {
0202     if (player->isPlaying()) {
0203         player->pause();
0204         setupPlayPauseAction(PlayMode);
0205     } else {
0206         q->play();
0207     }
0208 }
0209 
0210 void VideoWidget::Private::setPosterImage(const QImage &image)
0211 {
0212     if (!image.isNull()) {
0213         // cache the snapshot image
0214         movie->setPosterImage(image);
0215     }
0216 
0217     posterImagePage->setPixmap(QPixmap::fromImage(image));
0218 }
0219 
0220 void VideoWidget::Private::stateChanged(Phonon::State newState)
0221 {
0222     if (newState == Phonon::PlayingState) {
0223         pageLayout->setCurrentIndex(0);
0224     }
0225 }
0226 
0227 VideoWidget::VideoWidget(const Okular::Annotation *annotation, Okular::Movie *movie, Okular::Document *document, QWidget *parent)
0228     : QWidget(parent)
0229     , d(new Private(movie, document, this))
0230 {
0231     // do not propagate the mouse events to the parent widget;
0232     // they should be tied to this widget, not spread around...
0233     setAttribute(Qt::WA_NoMousePropagation);
0234 
0235     // Setup player page
0236     QWidget *playerPage = new QWidget(this);
0237 
0238     QVBoxLayout *mainlay = new QVBoxLayout(playerPage);
0239     mainlay->setContentsMargins(0, 0, 0, 0);
0240     mainlay->setSpacing(0);
0241 
0242     d->player = new Phonon::VideoPlayer(Phonon::NoCategory, playerPage);
0243     d->player->installEventFilter(playerPage);
0244     mainlay->addWidget(d->player);
0245 
0246     d->controlBar = new QToolBar(playerPage);
0247     d->controlBar->setIconSize(QSize(16, 16));
0248     d->controlBar->setAutoFillBackground(true);
0249     mainlay->addWidget(d->controlBar);
0250 
0251     d->playPauseAction = new QAction(d->controlBar);
0252     d->controlBar->addAction(d->playPauseAction);
0253     d->setupPlayPauseAction(Private::PlayMode);
0254     d->stopAction = d->controlBar->addAction(QIcon::fromTheme(QStringLiteral("media-playback-stop")), i18nc("stop the movie playback", "Stop"), this, SLOT(stop()));
0255     d->stopAction->setEnabled(false);
0256     d->controlBar->addSeparator();
0257     d->seekSlider = new Phonon::SeekSlider(d->player->mediaObject(), d->controlBar);
0258     d->seekSliderAction = d->controlBar->addWidget(d->seekSlider);
0259     d->seekSlider->setEnabled(false);
0260 
0261     Phonon::SeekSlider *verticalSeekSlider = new Phonon::SeekSlider(d->player->mediaObject(), nullptr);
0262     verticalSeekSlider->setMaximumHeight(100);
0263     d->seekSliderMenuAction = createToolBarButtonWithWidgetPopup(d->controlBar, verticalSeekSlider, QIcon::fromTheme(QStringLiteral("player-time")));
0264     d->seekSliderMenuAction->setVisible(false);
0265 
0266     d->controlBar->setVisible(movie->showControls());
0267 
0268     connect(d->player, &Phonon::VideoPlayer::finished, this, [this] { d->finished(); });
0269     connect(d->playPauseAction, &QAction::triggered, this, [this] { d->playOrPause(); });
0270 
0271     d->geom = annotation->transformedBoundingRectangle();
0272 
0273     // Setup poster image page
0274     d->posterImagePage = new QLabel;
0275     d->posterImagePage->setScaledContents(true);
0276     d->posterImagePage->installEventFilter(this);
0277     d->posterImagePage->setCursor(Qt::PointingHandCursor);
0278 
0279     d->pageLayout = new QStackedLayout(this);
0280     d->pageLayout->setContentsMargins({});
0281     d->pageLayout->setSpacing(0);
0282     d->pageLayout->addWidget(playerPage);
0283     d->pageLayout->addWidget(d->posterImagePage);
0284 
0285     if (movie->showPosterImage()) {
0286         d->pageLayout->setCurrentIndex(1);
0287 
0288         const QImage posterImage = movie->posterImage();
0289         if (posterImage.isNull()) {
0290             d->takeSnapshot();
0291         } else {
0292             d->setPosterImage(posterImage);
0293         }
0294     } else {
0295         d->pageLayout->setCurrentIndex(0);
0296     }
0297 }
0298 
0299 VideoWidget::~VideoWidget()
0300 {
0301     delete d;
0302 }
0303 
0304 void VideoWidget::setNormGeometry(const Okular::NormalizedRect &rect)
0305 {
0306     d->geom = rect;
0307 }
0308 
0309 Okular::NormalizedRect VideoWidget::normGeometry() const
0310 {
0311     return d->geom;
0312 }
0313 
0314 bool VideoWidget::isPlaying() const
0315 {
0316     return d->player->isPlaying();
0317 }
0318 
0319 void VideoWidget::pageInitialized()
0320 {
0321     hide();
0322 }
0323 
0324 void VideoWidget::pageEntered()
0325 {
0326     if (d->movie->showPosterImage()) {
0327         d->pageLayout->setCurrentIndex(1);
0328         show();
0329     }
0330 
0331     if (d->movie->autoPlay()) {
0332         show();
0333         QMetaObject::invokeMethod(this, "play", Qt::QueuedConnection);
0334         if (d->movie->startPaused()) {
0335             QMetaObject::invokeMethod(this, "pause", Qt::QueuedConnection);
0336         }
0337     }
0338 }
0339 
0340 void VideoWidget::pageLeft()
0341 {
0342     d->player->stop();
0343     d->videoStopped();
0344 
0345     hide();
0346 }
0347 
0348 void VideoWidget::play()
0349 {
0350     d->controlBar->setVisible(d->movie->showControls());
0351     d->load();
0352     // if d->repetitionsLeft is less than 1, we are supposed to stop midway, but not even Adobe reader does this
0353     d->player->play();
0354     d->stopAction->setEnabled(true);
0355     d->setupPlayPauseAction(Private::PauseMode);
0356 }
0357 
0358 void VideoWidget::stop()
0359 {
0360     d->player->stop();
0361     d->stopAction->setEnabled(false);
0362     d->setupPlayPauseAction(Private::PlayMode);
0363 }
0364 
0365 void VideoWidget::pause()
0366 {
0367     d->player->pause();
0368     d->setupPlayPauseAction(Private::PlayMode);
0369 }
0370 
0371 bool VideoWidget::eventFilter(QObject *object, QEvent *event)
0372 {
0373     if (object == d->player || object == d->posterImagePage) {
0374         switch (event->type()) {
0375         case QEvent::MouseButtonPress: {
0376             QMouseEvent *me = static_cast<QMouseEvent *>(event);
0377             if (me->button() == Qt::LeftButton) {
0378                 if (!d->player->isPlaying()) {
0379                     play();
0380                 }
0381                 event->accept();
0382             }
0383             break;
0384         }
0385         case QEvent::Wheel: {
0386             if (object == d->posterImagePage) {
0387                 QWheelEvent *we = static_cast<QWheelEvent *>(event);
0388 
0389                 // forward wheel events to parent widget
0390                 QWheelEvent *copy = new QWheelEvent(we->position(), we->globalPosition(), we->pixelDelta(), we->angleDelta(), we->buttons(), we->modifiers(), we->phase(), we->inverted(), we->source());
0391                 QCoreApplication::postEvent(parentWidget(), copy);
0392             }
0393             break;
0394         }
0395         default:;
0396         }
0397     }
0398 
0399     return false;
0400 }
0401 
0402 bool VideoWidget::event(QEvent *event)
0403 {
0404     switch (event->type()) {
0405     case QEvent::ToolTip:
0406         // "eat" the help events (= tooltips), avoid parent widgets receiving them
0407         event->accept();
0408         return true;
0409         break;
0410     default:;
0411     }
0412 
0413     return QWidget::event(event);
0414 }
0415 
0416 void VideoWidget::resizeEvent(QResizeEvent *event)
0417 {
0418     const QSize &s = event->size();
0419     int usedSpace = d->seekSlider->geometry().left() + d->seekSlider->iconSize().width();
0420     // try to give the slider at least 30px of space
0421     if (s.width() < (usedSpace + 30)) {
0422         d->seekSliderAction->setVisible(false);
0423         d->seekSliderMenuAction->setVisible(true);
0424     } else {
0425         d->seekSliderAction->setVisible(true);
0426         d->seekSliderMenuAction->setVisible(false);
0427     }
0428 }
0429 #else
0430 
0431 class VideoWidget::Private
0432 {
0433 public:
0434     Okular::NormalizedRect geom;
0435 };
0436 
0437 bool VideoWidget::event(QEvent *event)
0438 {
0439     return QWidget::event(event);
0440 }
0441 
0442 bool VideoWidget::eventFilter(QObject *object, QEvent *event)
0443 {
0444     return QWidget::eventFilter(object, event);
0445 }
0446 
0447 bool VideoWidget::isPlaying() const
0448 {
0449     return false;
0450 }
0451 
0452 Okular::NormalizedRect VideoWidget::normGeometry() const
0453 {
0454     return d->geom;
0455 }
0456 
0457 void VideoWidget::pageEntered()
0458 {
0459     show();
0460 }
0461 
0462 void VideoWidget::pageInitialized()
0463 {
0464 }
0465 
0466 void VideoWidget::pageLeft()
0467 {
0468 }
0469 void VideoWidget::pause()
0470 {
0471 }
0472 void VideoWidget::play()
0473 {
0474 }
0475 
0476 void VideoWidget::resizeEvent(QResizeEvent *event)
0477 {
0478     QWidget::resizeEvent(event);
0479 }
0480 
0481 void VideoWidget::setNormGeometry(const Okular::NormalizedRect &rect)
0482 {
0483     d->geom = rect;
0484 }
0485 
0486 void VideoWidget::stop()
0487 {
0488 }
0489 
0490 VideoWidget::VideoWidget(const Okular::Annotation *annotation, Okular::Movie *movie, Okular::Document *document, QWidget *parent)
0491     : QWidget(parent)
0492     , d(new VideoWidget::Private)
0493 {
0494     auto layout = new QVBoxLayout();
0495     d->geom = annotation->transformedBoundingRectangle();
0496     auto poster = new QLabel;
0497     if (movie->showPosterImage()) {
0498         auto posterImage = movie->posterImage();
0499         if (!posterImage.isNull()) {
0500             poster->setPixmap(QPixmap::fromImage(posterImage));
0501         }
0502     }
0503     Q_EMIT document->warning(i18n("Videos not supported in this okular"), 5000);
0504     poster->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
0505     layout->addWidget(poster, 2);
0506 
0507     auto label = new QLabel(i18n("Videos not supported in this Okular"));
0508     label->setAutoFillBackground(true);
0509     label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
0510     layout->addWidget(label, 1, Qt::AlignCenter);
0511     setLayout(layout);
0512 }
0513 
0514 VideoWidget::~VideoWidget() noexcept
0515 {
0516 }
0517 
0518 #endif
0519 #include "moc_videowidget.cpp"