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"