okular/part/videowidget.cpp
2024-03-04 16:55:11 +01:00

520 lines
14 KiB
C++

/*
SPDX-FileCopyrightText: 2008 Pino Toscano <pino@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "videowidget.h"
// qt/kde includes
#include <qaction.h>
#include <qcoreapplication.h>
#include <qdir.h>
#include <qevent.h>
#include <qlabel.h>
#include <qlayout.h>
#include <qmenu.h>
#include <qstackedlayout.h>
#include <qtoolbar.h>
#include <qtoolbutton.h>
#include <qwidgetaction.h>
#include <KLocalizedString>
#include <QIcon>
#include "config-okular.h"
#if HAVE_PHONON
#include <phonon/mediaobject.h>
#include <phonon/seekslider.h>
#include <phonon/videoplayer.h>
#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, &VideoWidget::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->setContentsMargins({});
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<QMouseEvent *>(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<QWheelEvent *>(event);
// forward wheel events to parent widget
QWheelEvent *copy = new QWheelEvent(we->position(), we->globalPosition(), we->pixelDelta(), we->angleDelta(), we->buttons(), we->modifiers(), we->phase(), we->inverted(), we->source());
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"