File indexing completed on 2024-05-12 04:19:41

0001 // vim: set tabstop=4 shiftwidth=4 expandtab:
0002 /*
0003 Gwenview: an image viewer
0004 Copyright 2009 Aurélien Gâteau <agateau@kde.org>
0005 
0006 This program is free software; you can redistribute it and/or
0007 modify it under the terms of the GNU General Public License
0008 as published by the Free Software Foundation; either version 2
0009 of the License, or (at your option) any later version.
0010 
0011 This program is distributed in the hope that it will be useful,
0012 but WITHOUT ANY WARRANTY; without even the implied warranty of
0013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0014 GNU General Public License for more details.
0015 
0016 You should have received a copy of the GNU General Public License
0017 along with this program; if not, write to the Free Software
0018 Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA.
0019 
0020 */
0021 // Self
0022 #include "videoviewadapter.h"
0023 
0024 // Qt
0025 #include <QAction>
0026 #include <QElapsedTimer>
0027 #include <QGraphicsLinearLayout>
0028 #include <QGraphicsProxyWidget>
0029 #include <QIcon>
0030 #include <QLabel>
0031 #include <QMouseEvent>
0032 
0033 // Phonon
0034 #include <phonon/AudioOutput>
0035 #include <phonon/MediaObject>
0036 #include <phonon/Path>
0037 #include <phonon/VideoWidget>
0038 
0039 // KF
0040 
0041 // Local
0042 #include "gwenview_lib_debug.h"
0043 #include <document/documentfactory.h>
0044 #include <graphicswidgetfloater.h>
0045 #include <hud/hudbutton.h>
0046 #include <hud/hudslider.h>
0047 #include <hud/hudwidget.h>
0048 #include <lib/gwenviewconfig.h>
0049 
0050 namespace Gwenview
0051 {
0052 struct VideoViewAdapterPrivate {
0053     VideoViewAdapter *q = nullptr;
0054     Phonon::MediaObject *mMediaObject = nullptr;
0055     Phonon::VideoWidget *mVideoWidget = nullptr;
0056     Phonon::AudioOutput *mAudioOutput = nullptr;
0057     HudWidget *mHud = nullptr;
0058     GraphicsWidgetFloater *mFloater = nullptr;
0059 
0060     HudSlider *mSeekSlider = nullptr;
0061     QElapsedTimer mLastSeekSliderActionTime;
0062 
0063     QLabel *mCurrentTime = nullptr;
0064     QLabel *mRemainingTime = nullptr;
0065 
0066     QAction *mPlayPauseAction = nullptr;
0067     QAction *mMuteAction = nullptr;
0068     QGraphicsProxyWidget *mProxy = nullptr;
0069 
0070     HudSlider *mVolumeSlider = nullptr;
0071     QElapsedTimer mLastVolumeSliderChangeTime;
0072 
0073     Document::Ptr mDocument;
0074 
0075     void setupActions()
0076     {
0077         mPlayPauseAction = new QAction(q);
0078         mPlayPauseAction->setShortcut(Qt::Key_P);
0079         QObject::connect(mPlayPauseAction, &QAction::triggered, q, &VideoViewAdapter::slotPlayPauseClicked);
0080         QObject::connect(mMediaObject, &Phonon::MediaObject::stateChanged, q, &VideoViewAdapter::updatePlayUi);
0081 
0082         mMuteAction = new QAction(q);
0083         mMuteAction->setShortcut(Qt::Key_M);
0084         QObject::connect(mMuteAction, &QAction::triggered, q, &VideoViewAdapter::slotMuteClicked);
0085         QObject::connect(mAudioOutput, &Phonon::AudioOutput::mutedChanged, q, &VideoViewAdapter::updateMuteAction);
0086     }
0087 
0088     void setupHud(QGraphicsWidget *parent)
0089     {
0090         // Play/Pause
0091         auto playPauseButton = new HudButton;
0092         playPauseButton->setDefaultAction(mPlayPauseAction);
0093 
0094         // Seek
0095         mSeekSlider = new HudSlider;
0096         mSeekSlider->setPageStep(5000);
0097         mSeekSlider->setSingleStep(200);
0098         QObject::connect(mSeekSlider, &HudSlider::actionTriggered, q, &VideoViewAdapter::slotSeekSliderActionTriggered);
0099         QObject::connect(mMediaObject, &Phonon::MediaObject::tick, q, &VideoViewAdapter::slotTicked);
0100         QObject::connect(mMediaObject, &Phonon::MediaObject::totalTimeChanged, q, &VideoViewAdapter::updatePlayUi);
0101         QObject::connect(mMediaObject, &Phonon::MediaObject::seekableChanged, q, &VideoViewAdapter::updatePlayUi);
0102 
0103         // Mute
0104         auto muteButton = new HudButton;
0105         muteButton->setDefaultAction(mMuteAction);
0106 
0107         // Volume
0108         mVolumeSlider = new HudSlider;
0109         mVolumeSlider->setMinimumWidth(100);
0110         mVolumeSlider->setRange(0, 100);
0111         mVolumeSlider->setPageStep(5);
0112         mVolumeSlider->setSingleStep(1);
0113         QObject::connect(mVolumeSlider, &HudSlider::valueChanged, q, &VideoViewAdapter::slotVolumeSliderChanged);
0114         QObject::connect(mAudioOutput, &Phonon::AudioOutput::volumeChanged, q, &VideoViewAdapter::slotOutputVolumeChanged);
0115 
0116         // Timestamps
0117         mCurrentTime = new QLabel(QStringLiteral("--:--"));
0118         mCurrentTime->setAttribute(Qt::WA_TranslucentBackground);
0119         mCurrentTime->setStyleSheet(QStringLiteral("QLabel { color : white; }"));
0120         mCurrentTime->setAlignment(Qt::AlignCenter);
0121         mRemainingTime = new QLabel(QStringLiteral("--:--"));
0122         mRemainingTime->setAttribute(Qt::WA_TranslucentBackground);
0123         mRemainingTime->setStyleSheet(QStringLiteral("QLabel { color : white; }"));
0124         mRemainingTime->setAlignment(Qt::AlignCenter);
0125         QObject::connect(mMediaObject, &Phonon::MediaObject::stateChanged, q, &VideoViewAdapter::updateTimestamps);
0126         QObject::connect(mMediaObject, &Phonon::MediaObject::tick, q, &VideoViewAdapter::updateTimestamps);
0127 
0128         // Layout
0129         auto hudContent = new QGraphicsWidget;
0130         auto layout = new QGraphicsLinearLayout(hudContent);
0131 
0132         auto currentTimeProxy = new QGraphicsProxyWidget(hudContent);
0133         currentTimeProxy->setWidget(mCurrentTime);
0134 
0135         auto remainingTimeProxy = new QGraphicsProxyWidget(hudContent);
0136         remainingTimeProxy->setWidget(mRemainingTime);
0137 
0138         layout->addItem(playPauseButton);
0139         layout->addItem(currentTimeProxy);
0140         layout->setStretchFactor(currentTimeProxy, 1);
0141         layout->addItem(mSeekSlider);
0142         layout->setStretchFactor(mSeekSlider, 6);
0143         layout->addItem(remainingTimeProxy);
0144         layout->setStretchFactor(remainingTimeProxy, 1);
0145         layout->addItem(muteButton);
0146         layout->addItem(mVolumeSlider);
0147         layout->setStretchFactor(mVolumeSlider, 2);
0148 
0149         // Create hud
0150         mHud = new HudWidget(parent);
0151         mHud->init(hudContent, HudWidget::OptionNone);
0152         mHud->setZValue(1);
0153 
0154         // Init floater
0155         mFloater = new GraphicsWidgetFloater(parent);
0156         mFloater->setChildWidget(mHud);
0157         mFloater->setAlignment(Qt::AlignJustify | Qt::AlignBottom);
0158     }
0159 
0160     bool isPlaying() const
0161     {
0162         switch (mMediaObject->state()) {
0163         case Phonon::PlayingState:
0164         case Phonon::BufferingState:
0165             return true;
0166         default:
0167             return false;
0168         }
0169     }
0170 
0171     void updateHudVisibility(int yPos)
0172     {
0173         const int floaterY = mVideoWidget->height() - mFloater->verticalMargin() - mHud->effectiveSizeHint(Qt::MinimumSize).height() * 3 / 2;
0174         if (yPos < floaterY) {
0175             mHud->fadeOut();
0176         } else {
0177             mHud->fadeIn();
0178         }
0179     }
0180 
0181     void keyPressEvent(QKeyEvent *event)
0182     {
0183         if (event->modifiers() != Qt::NoModifier) {
0184             return;
0185         }
0186 
0187         switch (event->key()) {
0188         case Qt::Key_Left:
0189             mSeekSlider->triggerAction(QAbstractSlider::SliderSingleStepSub);
0190             break;
0191         case Qt::Key_Right:
0192             mSeekSlider->triggerAction(QAbstractSlider::SliderSingleStepAdd);
0193             break;
0194         case Qt::Key_Up:
0195             Q_EMIT q->previousImageRequested();
0196             break;
0197         case Qt::Key_Down:
0198             Q_EMIT q->nextImageRequested();
0199             break;
0200         default:
0201             break;
0202         }
0203     }
0204 };
0205 
0206 /**
0207  * This is a workaround for a bug in QGraphicsProxyWidget: it does not forward
0208  * double-click events to the proxy-fied widget.
0209  *
0210  * QGraphicsProxyWidget::mouseDoubleClickEvent() correctly forwards the event
0211  * to its QWidget, but it is never called. This is because for it to be called,
0212  * the implementation of mousePressEvent() must call
0213  * QGraphicsItem::mousePressEvent() but it does not.
0214  */
0215 class DoubleClickableProxyWidget : public QGraphicsProxyWidget
0216 {
0217 protected:
0218     void mousePressEvent(QGraphicsSceneMouseEvent *event) override
0219     {
0220         QGraphicsWidget::mousePressEvent(event);
0221     }
0222 };
0223 
0224 VideoViewAdapter::VideoViewAdapter()
0225     : d(new VideoViewAdapterPrivate)
0226 {
0227     d->q = this;
0228     d->mMediaObject = new Phonon::MediaObject(this);
0229     d->mMediaObject->setTickInterval(350);
0230     connect(d->mMediaObject, &Phonon::MediaObject::finished, this, &VideoViewAdapter::videoFinished);
0231 
0232     d->mVideoWidget = new Phonon::VideoWidget;
0233     d->mVideoWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
0234     d->mVideoWidget->setAttribute(Qt::WA_Hover);
0235     d->mVideoWidget->installEventFilter(this);
0236 
0237     Phonon::createPath(d->mMediaObject, d->mVideoWidget);
0238 
0239     d->mAudioOutput = new Phonon::AudioOutput(Phonon::VideoCategory, this);
0240     Phonon::createPath(d->mMediaObject, d->mAudioOutput);
0241 
0242     d->mProxy = new DoubleClickableProxyWidget;
0243     d->mProxy->setFlag(QGraphicsItem::ItemIsSelectable); // Needed for doubleclick to work
0244     d->mProxy->setWidget(d->mVideoWidget);
0245     d->mProxy->setAcceptHoverEvents(GwenviewConfig::autoplayVideos()); // Makes hud visible when autoplay is disabled
0246     setWidget(d->mProxy);
0247 
0248     d->setupActions();
0249     d->setupHud(d->mProxy);
0250 
0251     updatePlayUi();
0252     updateMuteAction();
0253 }
0254 
0255 VideoViewAdapter::~VideoViewAdapter()
0256 {
0257     // This prevents a memory leak that can occur after switching
0258     // to the next/previous video. For details see:
0259     // https://git.reviewboard.kde.org/r/108070/
0260     d->mMediaObject->stop();
0261 
0262     delete d;
0263 }
0264 
0265 void VideoViewAdapter::setDocument(const Document::Ptr &doc)
0266 {
0267     d->mHud->show();
0268     d->mDocument = doc;
0269     d->mMediaObject->setCurrentSource(d->mDocument->url());
0270     if (GwenviewConfig::autoplayVideos()) {
0271         d->mMediaObject->play();
0272     }
0273     // If we do not use a queued connection, the signal arrives too early,
0274     // preventing the listing of the dir content when Gwenview is started with
0275     // a video as an argument.
0276     QMetaObject::invokeMethod(this, &VideoViewAdapter::completed, Qt::QueuedConnection);
0277 }
0278 
0279 Document::Ptr VideoViewAdapter::document() const
0280 {
0281     return d->mDocument;
0282 }
0283 
0284 void VideoViewAdapter::slotPlayPauseClicked()
0285 {
0286     if (d->isPlaying()) {
0287         d->mMediaObject->pause();
0288         d->mHud->fadeIn();
0289         d->mProxy->setAcceptHoverEvents(false);
0290     } else {
0291         d->mMediaObject->play();
0292         d->mProxy->setAcceptHoverEvents(true);
0293     }
0294 }
0295 
0296 void VideoViewAdapter::slotMuteClicked()
0297 {
0298     d->mAudioOutput->setMuted(!d->mAudioOutput->isMuted());
0299 }
0300 
0301 bool VideoViewAdapter::eventFilter(QObject *, QEvent *event)
0302 {
0303     if (event->type() == QEvent::MouseMove) {
0304         d->updateHudVisibility(static_cast<QMouseEvent *>(event)->y());
0305     } else if (event->type() == QEvent::KeyPress) {
0306         d->keyPressEvent(static_cast<QKeyEvent *>(event));
0307     } else if (event->type() == QEvent::MouseButtonDblClick) {
0308         if (static_cast<QMouseEvent *>(event)->modifiers() == Qt::NoModifier) {
0309             Q_EMIT toggleFullScreenRequested();
0310         }
0311     }
0312     return false;
0313 }
0314 
0315 void VideoViewAdapter::updatePlayUi()
0316 {
0317     if (d->isPlaying()) {
0318         d->mPlayPauseAction->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-pause")));
0319     } else {
0320         d->mPlayPauseAction->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-start")));
0321     }
0322 
0323     d->mLastSeekSliderActionTime.restart();
0324     d->mSeekSlider->setRange(0, d->mMediaObject->totalTime());
0325 
0326     switch (d->mMediaObject->state()) {
0327     case Phonon::PlayingState:
0328     case Phonon::BufferingState:
0329     case Phonon::PausedState:
0330         d->mSeekSlider->setEnabled(true);
0331         break;
0332     case Phonon::StoppedState:
0333     case Phonon::LoadingState:
0334     case Phonon::ErrorState:
0335         d->mSeekSlider->setEnabled(false);
0336         d->mSeekSlider->setValue(0);
0337         break;
0338     }
0339 }
0340 
0341 void VideoViewAdapter::updateMuteAction()
0342 {
0343     d->mMuteAction->setIcon(QIcon::fromTheme(d->mAudioOutput->isMuted() ? QStringLiteral("player-volume-muted") : QStringLiteral("player-volume")));
0344 }
0345 
0346 void VideoViewAdapter::slotVolumeSliderChanged(int value)
0347 {
0348     d->mLastVolumeSliderChangeTime.restart();
0349     d->mAudioOutput->setVolume(value / 100.);
0350 }
0351 
0352 void VideoViewAdapter::slotOutputVolumeChanged(qreal value)
0353 {
0354     if (d->mLastVolumeSliderChangeTime.isValid() && d->mLastVolumeSliderChangeTime.elapsed() < 2000) {
0355         return;
0356     }
0357     d->mVolumeSlider->setValue(qRound(value * 100));
0358 }
0359 
0360 void VideoViewAdapter::slotSeekSliderActionTriggered(int /*action*/)
0361 {
0362     d->mLastSeekSliderActionTime.restart();
0363     d->mMediaObject->seek(d->mSeekSlider->sliderPosition());
0364 }
0365 
0366 void VideoViewAdapter::updateTimestamps()
0367 {
0368     QString currentTime(QStringLiteral("--:--"));
0369     QString remainingTime(QStringLiteral("--:--"));
0370 
0371     switch (d->mMediaObject->state()) {
0372     case Phonon::PlayingState:
0373     case Phonon::BufferingState:
0374     case Phonon::PausedState: {
0375         qint64 current = d->mMediaObject->currentTime();
0376         currentTime = QDateTime::fromSecsSinceEpoch(current / 1000).toUTC().toString(QStringLiteral("h:mm:ss"));
0377         if (currentTime.startsWith(QStringLiteral("0:"))) {
0378             currentTime.remove(0, 2);
0379         }
0380 
0381         qint64 remaining = d->mMediaObject->remainingTime();
0382         remainingTime = QDateTime::fromSecsSinceEpoch(remaining / 1000).toUTC().toString(QStringLiteral("h:mm:ss"));
0383         if (remainingTime.startsWith(QStringLiteral("0:"))) {
0384             remainingTime.remove(0, 2);
0385         }
0386         remainingTime = QStringLiteral("-") + remainingTime;
0387         break;
0388     }
0389 
0390     default:
0391         break;
0392     }
0393 
0394     d->mCurrentTime->setText(currentTime);
0395     d->mRemainingTime->setText(remainingTime);
0396 }
0397 
0398 void VideoViewAdapter::slotTicked(qint64 value)
0399 {
0400     if (d->mLastSeekSliderActionTime.isValid() && d->mLastSeekSliderActionTime.elapsed() < 2000) {
0401         return;
0402     }
0403     if (!d->mSeekSlider->isSliderDown()) {
0404         d->mSeekSlider->setValue(value);
0405     }
0406 }
0407 
0408 } // namespace
0409 
0410 #include "moc_videoviewadapter.cpp"