diff --git a/lib/mpris2/mprismediaplayer2player.cpp b/lib/mpris2/mprismediaplayer2player.cpp index 85ab098b..19abe4a7 100644 --- a/lib/mpris2/mprismediaplayer2player.cpp +++ b/lib/mpris2/mprismediaplayer2player.cpp @@ -1,378 +1,384 @@ /* Gwenview: an image viewer Copyright 2018 Friedrich W. H. Kossebau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "mprismediaplayer2player.h" #include // lib #include #include #include #include #include #include #include // KF #include #include // Qt #include #include #include namespace Gwenview { static const double MAX_RATE = 1.0; static const double MIN_RATE = 1.0; MprisMediaPlayer2Player::MprisMediaPlayer2Player(const QString &objectDBusPath, SlideShow* slideShow, ContextManager* contextManager, QAction* toggleSlideShowAction, QAction* fullScreenAction, QAction* previousAction, QAction* nextAction, QObject* parent) : DBusAbstractAdaptor(objectDBusPath, parent) , mSlideShow(slideShow) , mContextManager(contextManager) , mToggleSlideShowAction(toggleSlideShowAction) , mFullScreenAction(fullScreenAction) , mPreviousAction(previousAction) , mNextAction(nextAction) , mSlideShowEnabled(mToggleSlideShowAction->isEnabled()) , mPreviousEnabled(mPreviousAction->isEnabled()) , mNextEnabled(mNextAction->isEnabled()) { updatePlaybackStatus(); connect(mSlideShow, &SlideShow::stateChanged, this, &MprisMediaPlayer2Player::onSlideShowStateChanged); + connect(mSlideShow, &SlideShow::intervalChanged, + this, &MprisMediaPlayer2Player::onMetaInfoUpdated); connect(mContextManager, &ContextManager::currentUrlChanged, this, &MprisMediaPlayer2Player::onCurrentUrlChanged); connect(mSlideShow->randomAction(), &QAction::toggled, this, &MprisMediaPlayer2Player::onRandomActionToggled); connect(mToggleSlideShowAction, &QAction::changed, this, &MprisMediaPlayer2Player::onToggleSlideShowActionChanged); connect(mFullScreenAction, &QAction::toggled, this, &MprisMediaPlayer2Player::onFullScreenActionToggled); connect(mNextAction, &QAction::changed, this, &MprisMediaPlayer2Player::onNextActionChanged); connect(mPreviousAction, &QAction::changed, this, &MprisMediaPlayer2Player::onPreviousActionChanged); } MprisMediaPlayer2Player::~MprisMediaPlayer2Player() { } bool MprisMediaPlayer2Player::updatePlaybackStatus() { const QString newStatus = (!mSlideShowEnabled || !mFullScreenAction->isChecked()) ? QStringLiteral("Stopped") : mSlideShow->isRunning() ? QStringLiteral("Playing") : /* else */ QStringLiteral("Paused"); const bool changed = (newStatus != mPlaybackStatus); if (changed) { mPlaybackStatus = newStatus; } return changed; } QString MprisMediaPlayer2Player::playbackStatus() const { return mPlaybackStatus; } bool MprisMediaPlayer2Player::canGoNext() const { return mNextEnabled; } void MprisMediaPlayer2Player::Next() { mNextAction->trigger(); } bool MprisMediaPlayer2Player::canGoPrevious() const { return mPreviousEnabled; } void MprisMediaPlayer2Player::Previous() { mPreviousAction->trigger(); } bool MprisMediaPlayer2Player::canPause() const { return mSlideShowEnabled; } void MprisMediaPlayer2Player::Pause() { mSlideShow->stop(); } void MprisMediaPlayer2Player::PlayPause() { mToggleSlideShowAction->trigger(); } void MprisMediaPlayer2Player::Stop() { if (mFullScreenAction->isChecked()) { mFullScreenAction->trigger(); } } bool MprisMediaPlayer2Player::canPlay() const { return mSlideShowEnabled; } void MprisMediaPlayer2Player::Play() { if (mSlideShow->isRunning()) { return; } mToggleSlideShowAction->trigger(); } double MprisMediaPlayer2Player::volume() const { return 0; } void MprisMediaPlayer2Player::setVolume(double volume) { Q_UNUSED(volume); } void MprisMediaPlayer2Player::setShuffle(bool isShuffle) { mSlideShow->randomAction()->setChecked(isShuffle); } QVariantMap MprisMediaPlayer2Player::metadata() const { return mMetaData; } qlonglong MprisMediaPlayer2Player::position() const { - return 0; + // milliseconds -> microseconds + return mSlideShow->position() * 1000; } double MprisMediaPlayer2Player::rate() const { return 1.0; } void MprisMediaPlayer2Player::setRate(double newRate) { Q_UNUSED(newRate); } double MprisMediaPlayer2Player::minimumRate() const { return MIN_RATE; } double MprisMediaPlayer2Player::maximumRate() const { return MAX_RATE; } bool MprisMediaPlayer2Player::isShuffle() const { return mSlideShow->randomAction()->isChecked(); } bool MprisMediaPlayer2Player::canSeek() const { return false; } bool MprisMediaPlayer2Player::canControl() const { return true; } void MprisMediaPlayer2Player::Seek(qlonglong offset) { Q_UNUSED(offset); } void MprisMediaPlayer2Player::SetPosition(const QDBusObjectPath& trackId, qlonglong pos) { Q_UNUSED(trackId); Q_UNUSED(pos); } void MprisMediaPlayer2Player::OpenUri(const QString& uri) { Q_UNUSED(uri); } void MprisMediaPlayer2Player::onSlideShowStateChanged() { if (!updatePlaybackStatus()) { return; } + signalPropertyChange("Position", position()); signalPropertyChange("PlaybackStatus", mPlaybackStatus); } void MprisMediaPlayer2Player::onCurrentUrlChanged(const QUrl& url) { if (url.isEmpty()) { mCurrentDocument = Document::Ptr(); } else { mCurrentDocument = DocumentFactory::instance()->load(url); connect(mCurrentDocument.data(), &Document::metaInfoUpdated, this, &MprisMediaPlayer2Player::onMetaInfoUpdated); } onMetaInfoUpdated(); + signalPropertyChange("Position", position()); } void MprisMediaPlayer2Player::onMetaInfoUpdated() { QVariantMap updatedMetaData; if (mCurrentDocument) { const QUrl url = mCurrentDocument->url(); ImageMetaInfoModel* metaInfoModel = mCurrentDocument->metaInfo(); // We need some unique id mapping to urls. The index in the list is not reliable, // as images can be added/removed during a running slideshow // To allow some bidrectional mapping, convert the url to base64 to encode it for // matching the D-Bus object path spec const QString slideId = QString::fromLatin1(url.toString().toUtf8().toBase64(QByteArray::OmitTrailingEquals)); const QDBusObjectPath trackId(QLatin1String("/org/kde/gwenview/imagelist/") + slideId); updatedMetaData.insert(QStringLiteral("mpris:trackid"), QVariant::fromValue(trackId)); // TODO: for videos also get and report the length if (MimeTypeUtils::urlKind(url) != MimeTypeUtils::KIND_VIDEO) { - // TODO: implement other MPRIS API for position // convert seconds in microseconds - // const qlonglong duration = qlonglong(mSlideShow->interval() * 1000000); - // updatedMetaData.insert(QStringLiteral("mpris:length"), duration); + const qlonglong duration = qlonglong(mSlideShow->interval() * 1000000); + updatedMetaData.insert(QStringLiteral("mpris:length"), duration); } // TODO: update on metadata changes, given user can edit most of this data const QString name = metaInfoModel->getValueForKey(QStringLiteral("General.Name")); updatedMetaData.insert(QStringLiteral("xesam:title"), name); const QString comment = metaInfoModel->getValueForKey(QStringLiteral("General.Comment")); if (!comment.isEmpty()) { updatedMetaData.insert(QStringLiteral("xesam:comment"), comment); } updatedMetaData.insert(QStringLiteral("xesam:url"), url.toString()); // slight bending of semantics :) const KFileItem folderItem(mContextManager->currentDirUrl()); updatedMetaData.insert(QStringLiteral("xesam:album"), folderItem.text()); // TODO: hook up with thumbnail cache and pass that as arturl // updatedMetaData.insert(QStringLiteral("mpris:artUrl"), url.toString()); #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE const QModelIndex index = mContextManager->dirModel()->indexForUrl(url); if (index.isValid()) { const double rating = index.data(SemanticInfoDirModel::RatingRole).toInt() / 10.0; updatedMetaData.insert(QStringLiteral("xesam:userRating"), rating); } #endif // consider export of other metadata where mapping works } if (updatedMetaData != mMetaData) { mMetaData = updatedMetaData; signalPropertyChange("Metadata", mMetaData); } } void MprisMediaPlayer2Player::onRandomActionToggled(bool checked) { signalPropertyChange("Shuffle", checked); } void MprisMediaPlayer2Player::onFullScreenActionToggled() { if (!updatePlaybackStatus()) { return; } + signalPropertyChange("Position", position()); signalPropertyChange("PlaybackStatus", mPlaybackStatus); } void MprisMediaPlayer2Player::onToggleSlideShowActionChanged() { const bool isEnabled = mToggleSlideShowAction->isEnabled(); if (mSlideShowEnabled == isEnabled) { return; } mSlideShowEnabled = isEnabled; const bool playbackStatusChanged = updatePlaybackStatus(); signalPropertyChange("CanPlay", mSlideShowEnabled); signalPropertyChange("CanPause", mSlideShowEnabled); if (playbackStatusChanged) { + signalPropertyChange("Position", position()); signalPropertyChange("PlaybackStatus", mPlaybackStatus); } } void MprisMediaPlayer2Player::onNextActionChanged() { const bool isEnabled = mNextAction->isEnabled(); if (mNextEnabled == isEnabled) { return; } mNextEnabled = isEnabled; signalPropertyChange("CanGoNext", mNextEnabled); } void MprisMediaPlayer2Player::onPreviousActionChanged() { const bool isEnabled = mPreviousAction->isEnabled(); if (mPreviousEnabled == isEnabled) { return; } mPreviousEnabled = isEnabled; signalPropertyChange("CanGoPrevious", mPreviousEnabled); } } diff --git a/lib/slideshow.cpp b/lib/slideshow.cpp index c3676ede..f14a27e9 100644 --- a/lib/slideshow.cpp +++ b/lib/slideshow.cpp @@ -1,308 +1,327 @@ /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "slideshow.h" // libc #include // STL #include // Qt #include #include #include // KDE #include // Local #include #include namespace Gwenview { #undef ENABLE_LOG #undef LOG //#define ENABLE_LOG #ifdef ENABLE_LOG #define LOG(x) qDebug() << x #else #define LOG(x) ; #endif enum State { Stopped, Started, WaitForEndOfUrl }; /** * This class generate random numbers which are not the same between two runs * of Gwenview. See bug #132334 */ class RandomNumberGenerator { public: RandomNumberGenerator() : mSeed(time(0)) { } int operator()(int n) { return rand_r(&mSeed) % n; } private: unsigned int mSeed; }; struct SlideShowPrivate { QTimer* mTimer; State mState; QVector mUrls; QVector mShuffledUrls; QVector::ConstIterator mStartIt; QUrl mCurrentUrl; QUrl mLastShuffledUrl; QAction* mLoopAction; QAction* mRandomAction; QUrl findNextUrl() { if (GwenviewConfig::random()) { return findNextRandomUrl(); } else { return findNextOrderedUrl(); } } QUrl findNextOrderedUrl() { QVector::ConstIterator it = qFind(mUrls.constBegin(), mUrls.constEnd(), mCurrentUrl); GV_RETURN_VALUE_IF_FAIL2(it != mUrls.constEnd(), QUrl(), "Current url not found in list."); ++it; if (GwenviewConfig::loop()) { // Looping, if we reach the end, start again if (it == mUrls.constEnd()) { it = mUrls.constBegin(); } } else { // Not looping, have we reached the end? // FIXME: stopAtEnd if (/*(it==mUrls.end() && GwenviewConfig::stopAtEnd()) ||*/ it == mStartIt) { it = mUrls.constEnd(); } } if (it != mUrls.constEnd()) { return *it; } else { return QUrl(); } } void initShuffledUrls() { mShuffledUrls = mUrls; RandomNumberGenerator generator; std::random_shuffle(mShuffledUrls.begin(), mShuffledUrls.end(), generator); // Ensure the first url is different from the previous last one, so that // last url does not stay visible twice longer than usual if (mLastShuffledUrl == mShuffledUrls.first() && mShuffledUrls.count() > 1) { qSwap(mShuffledUrls[0], mShuffledUrls[1]); } mLastShuffledUrl = mShuffledUrls.last(); } QUrl findNextRandomUrl() { if (mShuffledUrls.empty()) { if (GwenviewConfig::loop()) { initShuffledUrls(); } else { return QUrl(); } } QUrl url = mShuffledUrls.last(); mShuffledUrls.pop_back(); return url; } void updateTimerInterval() { mTimer->setInterval(int(GwenviewConfig::interval() * 1000)); } void doStart() { if (MimeTypeUtils::urlKind(mCurrentUrl) == MimeTypeUtils::KIND_VIDEO) { LOG("mState = WaitForEndOfUrl"); // Just in case mTimer->stop(); mState = WaitForEndOfUrl; } else { LOG("mState = Started"); mTimer->start(); mState = Started; } } }; SlideShow::SlideShow(QObject* parent) : QObject(parent) , d(new SlideShowPrivate) { d->mState = Stopped; d->mTimer = new QTimer(this); connect(d->mTimer, &QTimer::timeout, this, &SlideShow::goToNextUrl); d->mLoopAction = new QAction(this); d->mLoopAction->setText(i18nc("@item:inmenu toggle loop in slideshow", "Loop")); d->mLoopAction->setCheckable(true); connect(d->mLoopAction, &QAction::triggered, this, &SlideShow::updateConfig); d->mRandomAction = new QAction(this); d->mRandomAction->setText(i18nc("@item:inmenu toggle random order in slideshow", "Random")); d->mRandomAction->setCheckable(true); connect(d->mRandomAction, &QAction::toggled, this, &SlideShow::slotRandomActionToggled); connect(d->mRandomAction, &QAction::triggered, this, &SlideShow::updateConfig); d->mLoopAction->setChecked(GwenviewConfig::loop()); d->mRandomAction->setChecked(GwenviewConfig::random()); } SlideShow::~SlideShow() { GwenviewConfig::self()->save(); delete d; } QAction* SlideShow::loopAction() const { return d->mLoopAction; } QAction* SlideShow::randomAction() const { return d->mRandomAction; } void SlideShow::start(const QList& urls) { d->mUrls.resize(urls.size()); qCopy(urls.begin(), urls.end(), d->mUrls.begin()); d->mStartIt = qFind(d->mUrls.constBegin(), d->mUrls.constEnd(), d->mCurrentUrl); if (d->mStartIt == d->mUrls.constEnd()) { qWarning() << "Current url not found in list, aborting.\n"; return; } if (GwenviewConfig::random()) { d->initShuffledUrls(); } d->updateTimerInterval(); d->mTimer->setSingleShot(false); d->doStart(); stateChanged(true); } void SlideShow::setInterval(int intervalInSeconds) { GwenviewConfig::setInterval(double(intervalInSeconds)); d->updateTimerInterval(); + emit intervalChanged(intervalInSeconds); } int SlideShow::interval() const { return GwenviewConfig::interval(); } +int SlideShow::position() const +{ + // TODO: also support videos + + // QTimer::remainingTime() returns -1 if inactive + // and there are moments where mState == Started but timer already done but not yet next url reached + // so handle that + if (d->mState == Started) { + if (d->mTimer->isActive()) { + return interval() * 1000 - d->mTimer->remainingTime(); + } + // already timeout reached, but not yet progressed to next url + return interval(); + } + + return 0; +} + void SlideShow::stop() { LOG("Stopping timer"); d->mTimer->stop(); d->mState = Stopped; stateChanged(false); } void SlideShow::resumeAndGoToNextUrl() { LOG(""); if (d->mState == WaitForEndOfUrl) { goToNextUrl(); } } void SlideShow::goToNextUrl() { LOG(""); QUrl url = d->findNextUrl(); LOG("url:" << url); if (!url.isValid()) { stop(); return; } goToUrl(url); } void SlideShow::setCurrentUrl(const QUrl &url) { LOG(url); if (d->mCurrentUrl == url) { return; } d->mCurrentUrl = url; // Restart timer to avoid showing new url for the remaining time of the old // url if (d->mState != Stopped) { d->doStart(); } } bool SlideShow::isRunning() const { return d->mState != Stopped; } void SlideShow::updateConfig() { GwenviewConfig::setLoop(d->mLoopAction->isChecked()); GwenviewConfig::setRandom(d->mRandomAction->isChecked()); } void SlideShow::slotRandomActionToggled(bool on) { if (on && d->mState != Stopped) { d->initShuffledUrls(); } } } // namespace diff --git a/lib/slideshow.h b/lib/slideshow.h index 44d739a9..f7058064 100644 --- a/lib/slideshow.h +++ b/lib/slideshow.h @@ -1,83 +1,95 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef SLIDESHOW_H #define SLIDESHOW_H #include // Qt #include // KDE #include class QAction; namespace Gwenview { struct SlideShowPrivate; class GWENVIEWLIB_EXPORT SlideShow : public QObject { Q_OBJECT public: SlideShow(QObject* parent); virtual ~SlideShow(); void start(const QList& urls); void stop(); QAction* loopAction() const; QAction* randomAction() const; /** @return true if the slideshow is running */ bool isRunning() const; + /** + * @return interval in seconds + */ int interval() const; + /** + * @return position in time slot for current image in milliseconds + */ + int position() const; public Q_SLOTS: void setInterval(int); void setCurrentUrl(const QUrl &url); /** * Resume slideshow and go to next url. */ void resumeAndGoToNextUrl(); Q_SIGNALS: void goToUrl(const QUrl&); /** * Slideshow has been started or stopped */ void stateChanged(bool running); + /** + * Emitted when interval has been changed + * @param interval interval in seconds + */ + void intervalChanged(int interval); private Q_SLOTS: void goToNextUrl(); void updateConfig(); void slotRandomActionToggled(bool on); private: SlideShowPrivate* const d; }; } // namespace #endif // SLIDESHOW_H