/* SPDX-FileCopyrightText: 2008 Pino Toscano SPDX-License-Identifier: GPL-2.0-or-later */ #include "videowidget.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config-okular.h" #if HAVE_PHONON #include #include #include #endif #include "core/annotations.h" #include "core/area.h" #include "core/document.h" #include "core/movie.h" #include "snapshottaker.h" #if HAVE_PHONON static QAction *createToolBarButtonWithWidgetPopup(QToolBar *toolBar, QWidget *widget, const QIcon &icon) { QToolButton *button = new QToolButton(toolBar); QAction *action = toolBar->addWidget(button); button->setAutoRaise(true); button->setIcon(icon); button->setPopupMode(QToolButton::InstantPopup); QMenu *menu = new QMenu(button); button->setMenu(menu); QWidgetAction *widgetAction = new QWidgetAction(menu); QWidget *dummy = new QWidget(menu); widgetAction->setDefaultWidget(dummy); QVBoxLayout *dummyLayout = new QVBoxLayout(dummy); dummyLayout->setContentsMargins(5, 5, 5, 5); dummyLayout->addWidget(widget); menu->addAction(widgetAction); return action; } /* Private storage. */ class VideoWidget::Private { public: Private(Okular::Movie *m, Okular::Document *doc, VideoWidget *qq) : q(qq) , movie(m) , document(doc) , player(nullptr) , loaded(false) { } ~Private() { if (player) { player->stop(); } } enum PlayPauseMode { PlayMode, PauseMode }; void load(); void setupPlayPauseAction(PlayPauseMode mode); void setPosterImage(const QImage &); void takeSnapshot(); void videoStopped(); void stateChanged(Phonon::State newState); // slots void finished(); void playOrPause(); VideoWidget *q; Okular::Movie *movie; Okular::Document *document; Okular::NormalizedRect geom; Phonon::VideoPlayer *player; Phonon::SeekSlider *seekSlider; QToolBar *controlBar; QAction *playPauseAction; QAction *stopAction; QAction *seekSliderAction; QAction *seekSliderMenuAction; QStackedLayout *pageLayout; QLabel *posterImagePage; bool loaded : 1; double repetitionsLeft; }; static QUrl urlFromUrlString(const QString &url, Okular::Document *document) { QUrl newurl; if (url.startsWith(QLatin1Char('/'))) { newurl = QUrl::fromLocalFile(url); } else { newurl = QUrl(url); if (newurl.isRelative()) { newurl = document->currentDocument().adjusted(QUrl::RemoveFilename); newurl.setPath(newurl.path() + url); } } return newurl; } void VideoWidget::Private::load() { repetitionsLeft = movie->playRepetitions(); if (loaded) { return; } loaded = true; player->load(urlFromUrlString(movie->url(), document)); connect(player->mediaObject(), &Phonon::MediaObject::stateChanged, q, [this](Phonon::State s) { stateChanged(s); }); seekSlider->setEnabled(true); } void VideoWidget::Private::setupPlayPauseAction(PlayPauseMode mode) { if (mode == PlayMode) { playPauseAction->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-start"))); playPauseAction->setText(i18nc("start the movie playback", "Play")); } else if (mode == PauseMode) { playPauseAction->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-pause"))); playPauseAction->setText(i18nc("pause the movie playback", "Pause")); } } void VideoWidget::Private::takeSnapshot() { const QUrl url = urlFromUrlString(movie->url(), document); SnapshotTaker *taker = new SnapshotTaker(url, q); q->connect(taker, &SnapshotTaker::finished, q, [this](const QImage &image) { setPosterImage(image); }); } void VideoWidget::Private::videoStopped() { if (movie->showPosterImage()) { pageLayout->setCurrentIndex(1); } else { q->hide(); } } void VideoWidget::Private::finished() { switch (movie->playMode()) { case Okular::Movie::PlayLimited: case Okular::Movie::PlayOpen: repetitionsLeft -= 1.0; if (repetitionsLeft < 1e-5) { // allow for some calculation error // playback has ended stopAction->setEnabled(false); setupPlayPauseAction(PlayMode); if (movie->playMode() == Okular::Movie::PlayLimited) { controlBar->setVisible(false); } videoStopped(); } else { // not done yet, repeat // if repetitionsLeft is less than 1, we are supposed to stop midway, but not even Adobe reader does this player->play(); } break; case Okular::Movie::PlayRepeat: // repeat the playback player->play(); break; case Okular::Movie::PlayPalindrome: // FIXME we should play backward, but we cannot player->play(); break; } } void VideoWidget::Private::playOrPause() { if (player->isPlaying()) { player->pause(); setupPlayPauseAction(PlayMode); } else { q->play(); } } void VideoWidget::Private::setPosterImage(const QImage &image) { if (!image.isNull()) { // cache the snapshot image movie->setPosterImage(image); } posterImagePage->setPixmap(QPixmap::fromImage(image)); } void VideoWidget::Private::stateChanged(Phonon::State newState) { if (newState == Phonon::PlayingState) { pageLayout->setCurrentIndex(0); } } VideoWidget::VideoWidget(const Okular::Annotation *annotation, Okular::Movie *movie, Okular::Document *document, QWidget *parent) : QWidget(parent) , d(new Private(movie, document, this)) { // do not propagate the mouse events to the parent widget; // they should be tied to this widget, not spread around... setAttribute(Qt::WA_NoMousePropagation); // Setup player page QWidget *playerPage = new QWidget(this); QVBoxLayout *mainlay = new QVBoxLayout(playerPage); mainlay->setContentsMargins(0, 0, 0, 0); mainlay->setSpacing(0); d->player = new Phonon::VideoPlayer(Phonon::NoCategory, playerPage); d->player->installEventFilter(playerPage); mainlay->addWidget(d->player); d->controlBar = new QToolBar(playerPage); d->controlBar->setIconSize(QSize(16, 16)); d->controlBar->setAutoFillBackground(true); mainlay->addWidget(d->controlBar); d->playPauseAction = new QAction(d->controlBar); d->controlBar->addAction(d->playPauseAction); d->setupPlayPauseAction(Private::PlayMode); d->stopAction = d->controlBar->addAction(QIcon::fromTheme(QStringLiteral("media-playback-stop")), i18nc("stop the movie playback", "Stop"), this, SLOT(stop())); d->stopAction->setEnabled(false); d->controlBar->addSeparator(); d->seekSlider = new Phonon::SeekSlider(d->player->mediaObject(), d->controlBar); d->seekSliderAction = d->controlBar->addWidget(d->seekSlider); d->seekSlider->setEnabled(false); Phonon::SeekSlider *verticalSeekSlider = new Phonon::SeekSlider(d->player->mediaObject(), nullptr); verticalSeekSlider->setMaximumHeight(100); d->seekSliderMenuAction = createToolBarButtonWithWidgetPopup(d->controlBar, verticalSeekSlider, QIcon::fromTheme(QStringLiteral("player-time"))); d->seekSliderMenuAction->setVisible(false); d->controlBar->setVisible(movie->showControls()); connect(d->player, &Phonon::VideoPlayer::finished, this, [this] { d->finished(); }); connect(d->playPauseAction, &QAction::triggered, this, [this] { d->playOrPause(); }); d->geom = annotation->transformedBoundingRectangle(); // Setup poster image page d->posterImagePage = new QLabel; d->posterImagePage->setScaledContents(true); d->posterImagePage->installEventFilter(this); d->posterImagePage->setCursor(Qt::PointingHandCursor); d->pageLayout = new QStackedLayout(this); d->pageLayout->setMargin(0); d->pageLayout->setSpacing(0); d->pageLayout->addWidget(playerPage); d->pageLayout->addWidget(d->posterImagePage); if (movie->showPosterImage()) { d->pageLayout->setCurrentIndex(1); const QImage posterImage = movie->posterImage(); if (posterImage.isNull()) { d->takeSnapshot(); } else { d->setPosterImage(posterImage); } } else { d->pageLayout->setCurrentIndex(0); } } VideoWidget::~VideoWidget() { delete d; } void VideoWidget::setNormGeometry(const Okular::NormalizedRect &rect) { d->geom = rect; } Okular::NormalizedRect VideoWidget::normGeometry() const { return d->geom; } bool VideoWidget::isPlaying() const { return d->player->isPlaying(); } void VideoWidget::pageInitialized() { hide(); } void VideoWidget::pageEntered() { if (d->movie->showPosterImage()) { d->pageLayout->setCurrentIndex(1); show(); } if (d->movie->autoPlay()) { show(); QMetaObject::invokeMethod(this, "play", Qt::QueuedConnection); if (d->movie->startPaused()) { QMetaObject::invokeMethod(this, "pause", Qt::QueuedConnection); } } } void VideoWidget::pageLeft() { d->player->stop(); d->videoStopped(); hide(); } void VideoWidget::play() { d->controlBar->setVisible(d->movie->showControls()); d->load(); // if d->repetitionsLeft is less than 1, we are supposed to stop midway, but not even Adobe reader does this d->player->play(); d->stopAction->setEnabled(true); d->setupPlayPauseAction(Private::PauseMode); } void VideoWidget::stop() { d->player->stop(); d->stopAction->setEnabled(false); d->setupPlayPauseAction(Private::PlayMode); } void VideoWidget::pause() { d->player->pause(); d->setupPlayPauseAction(Private::PlayMode); } bool VideoWidget::eventFilter(QObject *object, QEvent *event) { if (object == d->player || object == d->posterImagePage) { switch (event->type()) { case QEvent::MouseButtonPress: { QMouseEvent *me = static_cast(event); if (me->button() == Qt::LeftButton) { if (!d->player->isPlaying()) { play(); } event->accept(); } break; } case QEvent::Wheel: { if (object == d->posterImagePage) { QWheelEvent *we = static_cast(event); // forward wheel events to parent widget QWheelEvent *copy = new QWheelEvent(we->pos(), we->globalPos(), we->angleDelta().y(), we->buttons(), we->modifiers(), we->orientation()); QCoreApplication::postEvent(parentWidget(), copy); } break; } default:; } } return false; } bool VideoWidget::event(QEvent *event) { switch (event->type()) { case QEvent::ToolTip: // "eat" the help events (= tooltips), avoid parent widgets receiving them event->accept(); return true; break; default:; } return QWidget::event(event); } void VideoWidget::resizeEvent(QResizeEvent *event) { const QSize &s = event->size(); int usedSpace = d->seekSlider->geometry().left() + d->seekSlider->iconSize().width(); // try to give the slider at least 30px of space if (s.width() < (usedSpace + 30)) { d->seekSliderAction->setVisible(false); d->seekSliderMenuAction->setVisible(true); } else { d->seekSliderAction->setVisible(true); d->seekSliderMenuAction->setVisible(false); } } #else class VideoWidget::Private { public: Okular::NormalizedRect geom; }; bool VideoWidget::event(QEvent *event) { return QWidget::event(event); } bool VideoWidget::eventFilter(QObject *object, QEvent *event) { return QWidget::eventFilter(object, event); } bool VideoWidget::isPlaying() const { return false; } Okular::NormalizedRect VideoWidget::normGeometry() const { return d->geom; } void VideoWidget::pageEntered() { show(); } void VideoWidget::pageInitialized() { } void VideoWidget::pageLeft() { } void VideoWidget::pause() { } void VideoWidget::play() { } void VideoWidget::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); } void VideoWidget::setNormGeometry(const Okular::NormalizedRect &rect) { d->geom = rect; } void VideoWidget::stop() { } VideoWidget::VideoWidget(const Okular::Annotation *annotation, Okular::Movie *movie, Okular::Document *document, QWidget *parent) : QWidget(parent) , d(new VideoWidget::Private) { auto layout = new QVBoxLayout(); d->geom = annotation->transformedBoundingRectangle(); auto poster = new QLabel; if (movie->showPosterImage()) { auto posterImage = movie->posterImage(); if (!posterImage.isNull()) { poster->setPixmap(QPixmap::fromImage(posterImage)); } } Q_EMIT document->warning(i18n("Videos not supported in this okular"), 5000); poster->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); layout->addWidget(poster, 2); auto label = new QLabel(i18n("Videos not supported in this Okular")); label->setAutoFillBackground(true); label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); layout->addWidget(label, 1, Qt::AlignCenter); setLayout(layout); } VideoWidget::~VideoWidget() noexcept { } #endif #include "moc_videowidget.cpp"