diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -331,6 +331,10 @@ ui/toolaction.cpp ui/videowidget.cpp ui/layers.cpp + mpris2/dbusabstractadaptor.cpp + mpris2/mpris2service.cpp + mpris2/mprismediaplayer2.cpp + mpris2/mprismediaplayer2player.cpp ) if (Qt5TextToSpeech_FOUND) diff --git a/mpris2/dbusabstractadaptor.h b/mpris2/dbusabstractadaptor.h new file mode 100644 --- /dev/null +++ b/mpris2/dbusabstractadaptor.h @@ -0,0 +1,64 @@ +/*************************************************************************** + * 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. * + ***************************************************************************/ + +#ifndef DBUSABSTRACTADAPTOR_H +#define DBUSABSTRACTADAPTOR_H + +// Qt +#include +#include + +namespace Okular +{ + +/** + * Extension of QDBusAbstractAdaptor for proper signalling of D-Bus object property changes + * + * QDBusAbstractAdaptor seems to fail on mapping QObject properties + * to D-Bus object properties when it comes to signalling changes to a property. + * The NOTIFY entry of Q_PROPERTY is not turned into respective D-Bus signalling of a + * property change. So we have to do this explicitly ourselves, instead of using a normal + * QObject signal and expecting the adaptor to translate it. + * + * To reduce D-Bus traffic, all registered property changes are accumulated and squashed + * between event loops where then the D-Bus signal is emitted. + */ +class DBusAbstractAdaptor : public QDBusAbstractAdaptor +{ + Q_OBJECT + +public: + /** + * Ideally we could query the D-Bus path of the object when used, but no idea yet how to do that. + * So one has to additionally pass here the D-Bus path at which the object is registered + * for which this interface is added. + * + * @param objectDBusPath D-Bus name of the property + * @param parent memory management parent or nullptr + */ + explicit DBusAbstractAdaptor(const QString &objectDBusPath, QObject *parent); + +protected: + /** + * @param propertyName D-Bus name of the property + * @param value the new value of the property + */ + void signalPropertyChange(const QString &propertyName, const QVariant &value); + +private Q_SLOTS: + void emitPropertiesChangeDBusSignal(); + +private: + QVariantMap mChangedProperties; + const QString mObjectPath; +}; + +} + +#endif // DBUSABSTRACTADAPTOR_H diff --git a/mpris2/dbusabstractadaptor.cpp b/mpris2/dbusabstractadaptor.cpp new file mode 100644 --- /dev/null +++ b/mpris2/dbusabstractadaptor.cpp @@ -0,0 +1,64 @@ +/*************************************************************************** + * 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. * + ***************************************************************************/ + +#include "dbusabstractadaptor.h" + +// Qt +#include +#include +#include + +namespace Okular +{ + +DBusAbstractAdaptor::DBusAbstractAdaptor(const QString &objectDBusPath, QObject *parent) + : QDBusAbstractAdaptor(parent) + , mObjectPath(objectDBusPath) +{ + Q_ASSERT(!mObjectPath.isEmpty()); +} + +void DBusAbstractAdaptor::signalPropertyChange(const QString &propertyName, const QVariant &value) +{ + const bool firstChange = mChangedProperties.isEmpty(); + + mChangedProperties.insert(propertyName, value); + + if (firstChange) { + // trigger signal emission on next event loop + QMetaObject::invokeMethod(this, "emitPropertiesChangeDBusSignal", Qt::QueuedConnection); + } +} + + +void DBusAbstractAdaptor::emitPropertiesChangeDBusSignal() +{ + if (mChangedProperties.isEmpty()) { + return; + } + + const QMetaObject* metaObject = this->metaObject(); + const int dBusInterfaceNameIndex = metaObject->indexOfClassInfo("D-Bus Interface"); + Q_ASSERT(dBusInterfaceNameIndex >= 0); + const char* dBusInterfaceName = metaObject->classInfo(dBusInterfaceNameIndex).value(); + + QDBusMessage signalMessage = QDBusMessage::createSignal(mObjectPath, + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("PropertiesChanged")); + signalMessage + << dBusInterfaceName + << mChangedProperties + << QStringList(); + + QDBusConnection::sessionBus().send(signalMessage); + + mChangedProperties.clear(); +} + +} diff --git a/mpris2/mpris2service.h b/mpris2/mpris2service.h new file mode 100644 --- /dev/null +++ b/mpris2/mpris2service.h @@ -0,0 +1,56 @@ +/*************************************************************************** + * 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. * + ***************************************************************************/ + +#ifndef MPRIS2SERVICE_H +#define MPRIS2SERVICE_H + +// Qt +#include + +class PresentationWidget; + +class KActionCollection; + +namespace Okular +{ +class Document; +class MprisMediaPlayer2; +class MprisMediaPlayer2Player; + +class Mpris2Service : public QObject +{ + Q_OBJECT + +public: + Mpris2Service(Document *document, + KActionCollection *actionCollection, + PresentationWidget *presentationWidget, + QObject *parent); + ~Mpris2Service() override; + +protected: + bool eventFilter(QObject *object, QEvent *event) override; + +private: + void registerOnDBus(); + void unregisterOnDBus(); + +private: + Document *mDocument; + KActionCollection *mActionCollection; + PresentationWidget *mPresentationWidget; + + QString mMpris2ServiceName; + MprisMediaPlayer2 *mPlayer2; + MprisMediaPlayer2Player *mPlayer2Player; +}; + +} + +#endif diff --git a/mpris2/mpris2service.cpp b/mpris2/mpris2service.cpp new file mode 100644 --- /dev/null +++ b/mpris2/mpris2service.cpp @@ -0,0 +1,127 @@ +/*************************************************************************** + * 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. * + ***************************************************************************/ + +#include "mpris2service.h" + +// lib +#include "mprismediaplayer2.h" +#include "mprismediaplayer2player.h" +#include +// KF +#include +#include +// Qt +#include +#include +#include +// std +#include + +namespace Okular +{ + +inline QString mediaPlayer2ObjectPath() { return QStringLiteral("/org/mpris/MediaPlayer2"); } + + +Mpris2Service::Mpris2Service(Document *document, + KActionCollection *actionCollection, + PresentationWidget *presentationWidget, + QObject *parent) + : QObject(parent) + , mDocument(document) + , mActionCollection(actionCollection) + , mPresentationWidget(presentationWidget) + , mPlayer2(nullptr) + , mPlayer2Player(nullptr) +{ + mPresentationWidget->installEventFilter(this); +} + + +Mpris2Service::~Mpris2Service() +{ + unregisterOnDBus(); +} + +bool Mpris2Service::eventFilter(QObject *object, QEvent *event) +{ + if (object == mPresentationWidget) { + if (event->type() == QEvent::Show) { + registerOnDBus(); + } else if (event->type() == QEvent::Close) { + unregisterOnDBus(); + } + } + + return QObject::eventFilter(object, event); +} + +void Mpris2Service::registerOnDBus() +{ + if (!mMpris2ServiceName.isEmpty()) { + return; + } + + QDBusConnection sessionBus = QDBusConnection::sessionBus(); + + // try to register MPRIS presentation object + // to be done before registering the service name, so it is already present + // when controllers react to the service name having appeared + QAction *priorAction = mActionCollection->action( QString::fromLatin1(KStandardAction::name(KStandardAction::Prior))); + QAction *nextAction = mActionCollection->action( QString::fromLatin1(KStandardAction::name(KStandardAction::Next))); + + QAction *playPauseAction = mActionCollection->action(QStringLiteral("presentation_play_pause")); + + mPlayer2 = new MprisMediaPlayer2(mediaPlayer2ObjectPath(), this); + mPlayer2Player = new MprisMediaPlayer2Player(mediaPlayer2ObjectPath(), mDocument, playPauseAction, priorAction, nextAction, mPresentationWidget, this); + + const bool objectRegistered = sessionBus.registerObject(mediaPlayer2ObjectPath(), this, QDBusConnection::ExportAdaptors); + + // try to register MPRIS presentation service + if (objectRegistered) { + mMpris2ServiceName = QStringLiteral("org.mpris.MediaPlayer2.Okular"); + + bool serviceRegistered = sessionBus.registerService(mMpris2ServiceName); + + // Perhaps not the first instance? Try again with another name, as specified by MPRIS2 spec: + if (!serviceRegistered) { + mMpris2ServiceName = mMpris2ServiceName + QLatin1String(".instance") + QString::number(getpid()); + serviceRegistered = sessionBus.registerService(mMpris2ServiceName); + } + if (!serviceRegistered) { + mMpris2ServiceName.clear(); + sessionBus.unregisterObject(mediaPlayer2ObjectPath()); + } + } + + if (!objectRegistered || mMpris2ServiceName.isEmpty()) { + // clean-up unused objects + delete mPlayer2; + delete mPlayer2Player; + mPlayer2 = nullptr; + mPlayer2Player = nullptr; + } +} + +void Mpris2Service::unregisterOnDBus() +{ + if (mMpris2ServiceName.isEmpty()) { + return; + } + + QDBusConnection sessionBus = QDBusConnection::sessionBus(); + sessionBus.unregisterService(mMpris2ServiceName); + sessionBus.unregisterObject(mediaPlayer2ObjectPath()); + delete mPlayer2; + delete mPlayer2Player; + mPlayer2 = nullptr; + mPlayer2Player = nullptr; +} + +} diff --git a/mpris2/mprismediaplayer2.h b/mpris2/mprismediaplayer2.h new file mode 100644 --- /dev/null +++ b/mpris2/mprismediaplayer2.h @@ -0,0 +1,66 @@ +/*************************************************************************** + * 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. * + ***************************************************************************/ + +#ifndef MPRISMEDIAPLAYER2_H +#define MPRISMEDIAPLAYER2_H + +#include "dbusabstractadaptor.h" +// Qt +#include + +class QAction; + +namespace Okular +{ + +// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html +class MprisMediaPlayer2 : public DBusAbstractAdaptor +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2") + + Q_PROPERTY(bool CanQuit READ canQuit CONSTANT) + Q_PROPERTY(bool CanRaise READ canRaise CONSTANT) + Q_PROPERTY(bool CanSetFullscreen READ canSetFullscreen CONSTANT) + + Q_PROPERTY(QString Identity READ identity CONSTANT) + Q_PROPERTY(QString DesktopEntry READ desktopEntry CONSTANT) + + Q_PROPERTY(bool HasTrackList READ hasTrackList CONSTANT) + + Q_PROPERTY(QStringList SupportedUriSchemes READ supportedUriSchemes CONSTANT) + Q_PROPERTY(QStringList SupportedMimeTypes READ supportedMimeTypes CONSTANT) + +public: + MprisMediaPlayer2(const QString &objectDBusPath, + QObject *parent); + ~MprisMediaPlayer2() override; + +public Q_SLOTS: // D-Bus API + void Quit(); + void Raise(); + +private: + bool canQuit() const; + bool canRaise() const; + bool canSetFullscreen() const; + bool hasTrackList() const; + + QString identity() const; + QString desktopEntry() const; + + QStringList supportedUriSchemes() const; + QStringList supportedMimeTypes() const; + +private: +}; + +} + +#endif diff --git a/mpris2/mprismediaplayer2.cpp b/mpris2/mprismediaplayer2.cpp new file mode 100644 --- /dev/null +++ b/mpris2/mprismediaplayer2.cpp @@ -0,0 +1,80 @@ +/*************************************************************************** + * 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. * + ***************************************************************************/ + +#include "mprismediaplayer2.h" + +// Qt +#include + +namespace Okular +{ + +MprisMediaPlayer2::MprisMediaPlayer2(const QString &objectDBusPath, + QObject *parent) + : DBusAbstractAdaptor(objectDBusPath, parent) +{ +} + +MprisMediaPlayer2::~MprisMediaPlayer2() +{ +} + +bool MprisMediaPlayer2::canQuit() const +{ + return true; +} + +bool MprisMediaPlayer2::canRaise() const +{ + return false; +} + +bool MprisMediaPlayer2::canSetFullscreen() const +{ + // Okular itself currently only does fullscreen slideshows, + // so while in principle capable to support this by wrapping the fullscreen action, + // normal Okular code does not expect a state with non-fullscreen slideshow running + return false; +} + +bool MprisMediaPlayer2::hasTrackList() const +{ + return false; +} + +void MprisMediaPlayer2::Quit() +{ + QGuiApplication::quit(); +} + +void MprisMediaPlayer2::Raise() +{ +} + +QString MprisMediaPlayer2::identity() const +{ + return QGuiApplication::applicationDisplayName(); +} + +QString MprisMediaPlayer2::desktopEntry() const +{ + return QGuiApplication::desktopFileName(); +} + +QStringList MprisMediaPlayer2::supportedUriSchemes() const +{ + return QStringList(); +} + +QStringList MprisMediaPlayer2::supportedMimeTypes() const +{ + return QStringList(); +} + +} diff --git a/mpris2/mprismediaplayer2player.h b/mpris2/mprismediaplayer2player.h new file mode 100644 --- /dev/null +++ b/mpris2/mprismediaplayer2player.h @@ -0,0 +1,169 @@ +/*************************************************************************** + * 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. * + ***************************************************************************/ + +#ifndef MPRISMEDIAPLAYER2PLAYER_H +#define MPRISMEDIAPLAYER2PLAYER_H + +#include "dbusabstractadaptor.h" +#include +// Qt +#include +#include + +class PresentationWidget; + +class QDBusObjectPath; +class QTemporaryDir; +class QAction; +class QUrl; + +namespace Okular +{ +class Document; + +class MprisDocumentObserver : public QObject, public DocumentObserver +{ + Q_OBJECT +public: + MprisDocumentObserver(); + +public: + void notifyCurrentPageChanged(int previous, int current) override; + void notifyPageChanged(int page, int flags) override; + +Q_SIGNALS: + void currentPageChanged(int current); + void pixmapAvailable(int page); +}; + +/** + * Manages thumbnails for the pages, with each thumbnail accessable to 3rd-party + * via the local filesystem. + * Removes all files and thumbnails on destruction. + * + * To assist 3rd-party which is caching thumbnails after loading from the filesystem, + * on an update of a thumbnail a new unique filename is created. + */ +class MprisThumbnailStore +{ +public: + explicit MprisThumbnailStore(Document *document); + ~MprisThumbnailStore(); + +public: + bool addThumbnail(const QPixmap& thumbnail, int pageNumber); + bool hasThumbnailForPage(int pageNumber) const; + /// Returns invalid url if there is no thumbnail + QUrl thumbnailUrlForPage(int pageNumber) const; + +private: + QString thumbnailFilePath(int pageNumber, int version) const; + +private: + QTemporaryDir *mThumbnailCacheDir; + // version 0 -> no thumbnail + QVector mThumbnailVersion; + QBitArray mThumbnailForPageCreated; + int mThumbnailWidth; + int mThumbnailHeight; +}; + +// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html +class MprisMediaPlayer2Player : public DBusAbstractAdaptor +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2.Player") + + Q_PROPERTY(QString PlaybackStatus READ playbackStatus) + Q_PROPERTY(double Rate READ rate WRITE setRate) + Q_PROPERTY(QVariantMap Metadata READ metadata) + Q_PROPERTY(double Volume READ volume WRITE setVolume) + Q_PROPERTY(qlonglong Position READ position) + Q_PROPERTY(double MinimumRate READ minimumRate CONSTANT) + Q_PROPERTY(double MaximumRate READ maximumRate CONSTANT) + + Q_PROPERTY(bool CanControl READ canControl CONSTANT) + Q_PROPERTY(bool CanPlay READ canPlay) + Q_PROPERTY(bool CanPause READ canPause) + Q_PROPERTY(bool CanGoNext READ canGoNext) + Q_PROPERTY(bool CanGoPrevious READ canGoPrevious) + Q_PROPERTY(bool CanSeek READ canSeek CONSTANT) + +public: + MprisMediaPlayer2Player(const QString &objectDBusPath, + Document *document, + QAction *togglePlayPauseAction, + QAction *previousAction, + QAction *nextAction, + PresentationWidget *presentationWidget, + QObject *parent); + ~MprisMediaPlayer2Player() override; + +public Q_SLOTS: // D-Bus API + void Next(); + void Previous(); + void Pause(); + void PlayPause(); + void Stop(); + void Play(); + void Seek(qlonglong Offset); + void SetPosition(const QDBusObjectPath &trackId, qlonglong pos); + void OpenUri(const QString &uri); + +Q_SIGNALS: // D-Bus API + void Seeked(qlonglong Position) const; + +private: + QString playbackStatus() const; + double rate() const; + QVariantMap metadata() const; + double volume() const; + qlonglong position() const; + double minimumRate() const; + double maximumRate() const; + + bool canGoNext() const; + bool canGoPrevious() const; + bool canPlay() const; + bool canPause() const; + bool canSeek() const; + bool canControl() const; + + void setRate(double newRate); + void setVolume(double volume); + +private: + void onCurrentPageChanged(int currentPage); + void onPixmapAvailable(int page); + + void onNextActionChanged(); + void onPreviousActionChanged(); + void onAdvancingSlidesChanged(bool isAdvancingSlides); + + void updateMetaData(); + +private: + QAction *mTogglePlayPauseShowAction; + QAction *mPreviousAction; + QAction *mNextAction; + PresentationWidget *mPresentationWidget; + Document *mDocument; + MprisDocumentObserver mDocumentObserver; + MprisThumbnailStore mThumbnailStore; + int mThumbnailWidth; + int mThumbnailHeight; + + bool mPreviousEnabled; + bool mNextEnabled; + QVariantMap mMetaData; +}; + +} + +#endif diff --git a/mpris2/mprismediaplayer2player.cpp b/mpris2/mprismediaplayer2player.cpp new file mode 100644 --- /dev/null +++ b/mpris2/mprismediaplayer2player.cpp @@ -0,0 +1,446 @@ +/*************************************************************************** + * 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. * + ***************************************************************************/ + +#include "mprismediaplayer2player.h" + +// lib +#include +#include +#include +#include +#include +#include +// KF +#include +// Qt +#include +#include +#include +#include +#include +#include +#include + +namespace Okular +{ + +static const double MAX_RATE = 1.0; +static const double MIN_RATE = 1.0; + + +MprisDocumentObserver::MprisDocumentObserver() +{ +} + +void MprisDocumentObserver::notifyCurrentPageChanged(int previous, int current) +{ + Q_UNUSED(previous); + + emit currentPageChanged(current); +} + +void MprisDocumentObserver::notifyPageChanged(int page, int flags) +{ + if (flags & Pixmap) { + // TODO: can we be sure about this? + emit pixmapAvailable(page); + } +} + + +MprisThumbnailStore::MprisThumbnailStore(Document *document) + : mThumbnailCacheDir(new QTemporaryDir) + // TODO: how to learn that the number of pages has changed? + , mThumbnailVersion(document->pages(), 0) +{ +} + +MprisThumbnailStore::~MprisThumbnailStore() +{ + delete mThumbnailCacheDir; +} + +bool MprisThumbnailStore::addThumbnail(const QPixmap& thumbnail, int pageNumber) +{ + Q_ASSERT(0 <= pageNumber && pageNumber < mThumbnailVersion.size()); + if (pageNumber < 0 || mThumbnailVersion.size() < pageNumber) { + return false; + } + + int& version = mThumbnailVersion[pageNumber]; + + // TODO: do we need to protect against overflow? nobody would need 2^31 updates, right? ;) + const int newVersion = (version + 1); + const QString newThumbnailFilePath = thumbnailFilePath(pageNumber, newVersion); + + const bool success = thumbnail.save(newThumbnailFilePath, "PNG"); + if (success) { + const int oldVersion = version; + version = newVersion; + if (oldVersion > 0) { + QFile::remove(thumbnailFilePath(pageNumber, oldVersion)); + } + } else { + qWarning() << "Could not save thumbbail to file" << newThumbnailFilePath; + } + + return success; +} + +bool MprisThumbnailStore::hasThumbnailForPage(int pageNumber) const +{ + Q_ASSERT(0 <= pageNumber && pageNumber < mThumbnailVersion.size()); + if (pageNumber < 0 || mThumbnailVersion.size() < pageNumber) { + return false; + } + + return (mThumbnailVersion[pageNumber] > 0); +} + +QUrl MprisThumbnailStore::thumbnailUrlForPage(int pageNumber) const +{ + Q_ASSERT(0 <= pageNumber && pageNumber < mThumbnailVersion.size()); + if (pageNumber < 0 || mThumbnailVersion.size() < pageNumber) { + return QUrl(); + } + + const int version = mThumbnailVersion[pageNumber]; + if (version == 0) { + return QUrl(); + } + + return QUrl::fromLocalFile(thumbnailFilePath(pageNumber, version)); +} + +QString MprisThumbnailStore::thumbnailFilePath(int pageNumber, int version) const +{ + const QString fileName = QString::number(pageNumber) + QLatin1Char('-') + QString::number(version) + QLatin1String(".png"); + return mThumbnailCacheDir->path() + QLatin1Char('/') + fileName; +} + + +static inline +QString playbackStatusFromAdvancingSlides(bool isAdvancingSlides) +{ + return isAdvancingSlides ? QStringLiteral("Playing") : QStringLiteral("Paused"); +} + + +MprisMediaPlayer2Player::MprisMediaPlayer2Player(const QString &objectDBusPath, + Document *document, + QAction *togglePlayPauseAction, + QAction *previousAction, + QAction *nextAction, + PresentationWidget *presentationWidget, + QObject *parent) + : DBusAbstractAdaptor(objectDBusPath, parent) + , mTogglePlayPauseShowAction(togglePlayPauseAction) + , mPreviousAction(previousAction) + , mNextAction(nextAction) + , mPresentationWidget(presentationWidget) + , mDocument(document) + , mThumbnailStore(document) + , mThumbnailWidth(256) + , mThumbnailHeight(256) + , mPreviousEnabled(mPreviousAction->isEnabled()) + , mNextEnabled(mNextAction->isEnabled()) +{ + mDocument->addObserver(&mDocumentObserver); + + connect(&mDocumentObserver, &MprisDocumentObserver::currentPageChanged, + this, &MprisMediaPlayer2Player::onCurrentPageChanged); + connect(&mDocumentObserver, &MprisDocumentObserver::pixmapAvailable, + this, &MprisMediaPlayer2Player::onPixmapAvailable); + + connect(mNextAction, &QAction::changed, + this, &MprisMediaPlayer2Player::onNextActionChanged); + connect(mPreviousAction, &QAction::changed, + this, &MprisMediaPlayer2Player::onPreviousActionChanged); + connect(mPresentationWidget, &PresentationWidget::advancingSlidesChanged, + this, &MprisMediaPlayer2Player::onAdvancingSlidesChanged); + + onCurrentPageChanged(mDocument->currentPage()); +} + +MprisMediaPlayer2Player::~MprisMediaPlayer2Player() +{ + mDocument->removeObserver(&mDocumentObserver); +} + +QString MprisMediaPlayer2Player::playbackStatus() const +{ + return playbackStatusFromAdvancingSlides(mPresentationWidget->isAdvancingSlides()); +} + +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 true; +} + +void MprisMediaPlayer2Player::Pause() +{ + if (!mPresentationWidget->isAdvancingSlides()) { + return; + } + + mTogglePlayPauseShowAction->trigger(); +} + +void MprisMediaPlayer2Player::PlayPause() +{ + mTogglePlayPauseShowAction->trigger(); +} + +void MprisMediaPlayer2Player::Stop() +{ + mPresentationWidget->close(); +} + +bool MprisMediaPlayer2Player::canPlay() const +{ + return true; +} + +void MprisMediaPlayer2Player::Play() +{ + if (mPresentationWidget->isAdvancingSlides()) { + return; + } + + mTogglePlayPauseShowAction->trigger(); +} + +double MprisMediaPlayer2Player::volume() const +{ + // TODO: support any audio playing + return 0; +} + +void MprisMediaPlayer2Player::setVolume(double volume) +{ + Q_UNUSED(volume); + // TODO: support any audio playing +} + +QVariantMap MprisMediaPlayer2Player::metadata() const +{ + return mMetaData; +} + +qlonglong MprisMediaPlayer2Player::position() const +{ + return 0; +} + +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::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::onCurrentPageChanged(int currentPage) +{ + // trigger thumbnail generation + QLinkedList thumbnailRequests; + + // TODO: how to find out if there still is another pixmap request ongoing? + // not a QObject, so cannot connect to destroyed signal + if (!mThumbnailStore.hasThumbnailForPage(currentPage)) { + PixmapRequest *currentPageRequest = new PixmapRequest(&mDocumentObserver, currentPage, mThumbnailWidth, mThumbnailHeight, THUMBNAILS_PRIO, PixmapRequest::Asynchronous); + thumbnailRequests.append(currentPageRequest); + } + + // pregenerate thumbnail of one before & after + // TODO: use okular memory settings to decide about generating all + if (currentPage > 0) { + const int previousPage = currentPage-1; + if (!mThumbnailStore.hasThumbnailForPage(previousPage)) { + PixmapRequest *previousPageRequest = new PixmapRequest(&mDocumentObserver, previousPage, mThumbnailWidth, mThumbnailHeight, THUMBNAILS_PRELOAD_PRIO, PixmapRequest::Asynchronous); + thumbnailRequests.append(previousPageRequest); + } + } + if (currentPage < (int)mDocument->pages()-1) { + const int nextPage = currentPage + 1; + if (!mThumbnailStore.hasThumbnailForPage(nextPage)) { + PixmapRequest *nextPageRequest = new PixmapRequest(&mDocumentObserver, nextPage, mThumbnailWidth, mThumbnailHeight, THUMBNAILS_PRELOAD_PRIO, PixmapRequest::Asynchronous); + thumbnailRequests.append(nextPageRequest); + } + } + + if (!thumbnailRequests.isEmpty()) { + mDocument->requestPixmaps(thumbnailRequests); + } + + updateMetaData(); +} + + +void MprisMediaPlayer2Player::updateMetaData() +{ + const int currentPage = mDocument->currentPage(); + + QVariantMap updatedMetaData; + + const QDBusObjectPath trackId(QLatin1String("/org/kde/okular/pagelist/") + QString::number(currentPage)); + updatedMetaData.insert(QStringLiteral("mpris:trackid"), + QVariant::fromValue(trackId)); + + // TODO: implement other MPRIS API for position + // convert seconds in microseconds + // const qlonglong duration = qlonglong(mSlideShow->interval() * 1000000); + // updatedMetaData.insert(QStringLiteral("mpris:length"), duration); + + updatedMetaData.insert(QStringLiteral("xesam:title"), i18n("Page %1", currentPage+1)); + + // slight bending of semantics :) + const Okular::DocumentInfo info = mDocument->documentInfo(QSet { + Okular::DocumentInfo::Title + }); + const QString title = info.get(Okular::DocumentInfo::Title); + if (!title.isEmpty()) { + updatedMetaData.insert(QStringLiteral("xesam:album"), title); + } + + const QUrl thumbnailUrl = mThumbnailStore.thumbnailUrlForPage(currentPage); + if (thumbnailUrl.isValid()) { + updatedMetaData.insert(QStringLiteral("mpris:artUrl"), thumbnailUrl.toString()); + } + + // consider export of other metadata where mapping works + + if (updatedMetaData != mMetaData) { + mMetaData = updatedMetaData; + + signalPropertyChange("Metadata", mMetaData); + } +} + +void MprisMediaPlayer2Player::onPixmapAvailable(int pageNumber) +{ + const Page *page = mDocument->page(pageNumber); + + const float thumbnailRatio = (float)mThumbnailWidth / mThumbnailHeight; + const float pageRatio = page->ratio(); + int pageWidth; + int pageHeight; + if (pageRatio > thumbnailRatio) { + pageHeight = mThumbnailHeight; + pageWidth = (int)((float)pageHeight / pageRatio); + } else { + pageWidth = mThumbnailWidth; + pageHeight = (int)((float)pageWidth * pageRatio); + } + + const QRect rect(0, 0, pageWidth, pageHeight); + QPixmap thumbnail(rect.size()); + QPainter painter(&thumbnail); + const int flags = PagePainter::Accessibility | PagePainter::Highlights | PagePainter::Annotations; + PagePainter::paintPageOnPainter(&painter, page, &mDocumentObserver, flags, + pageWidth, pageHeight, rect); + + const bool success = mThumbnailStore.addThumbnail(thumbnail, pageNumber); + if (success && pageNumber == (int)mDocument->currentPage()) { + updateMetaData(); + } +} + +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); +} + +void MprisMediaPlayer2Player::onAdvancingSlidesChanged(bool isAdvancingSlides) +{ + signalPropertyChange("PlaybackStatus", playbackStatusFromAdvancingSlides(isAdvancingSlides)); +} + +} diff --git a/ui/presentationwidget.h b/ui/presentationwidget.h --- a/ui/presentationwidget.h +++ b/ui/presentationwidget.h @@ -37,6 +37,7 @@ class MovieAction; class Page; class RenditionAction; +class Mpris2Service; } /** @@ -58,9 +59,14 @@ bool canUnloadPixmap( int pageNumber ) const override; void notifyCurrentPageChanged( int previous, int current ) override; + bool isAdvancingSlides() const; + public Q_SLOTS: void slotFind(); + Q_SIGNALS: + void advancingSlidesChanged(bool isAdvancingSlides); + protected: // widget events bool event( QEvent * e ) override; @@ -141,6 +147,7 @@ KActionCollection * m_ac; KSelectAction * m_screenSelect; QDomElement m_currentDrawingToolElement; + Okular::Mpris2Service *m_mpris2Service; bool m_isSetup; bool m_blockNotifications; bool m_inBlackScreenMode; diff --git a/ui/presentationwidget.cpp b/ui/presentationwidget.cpp --- a/ui/presentationwidget.cpp +++ b/ui/presentationwidget.cpp @@ -60,6 +60,7 @@ #include "presentationsearchbar.h" #include "priorities.h" #include "videowidget.h" +#include "mpris2/mpris2service.h" #include "core/action.h" #include "core/annotations.h" #include "core/audioplayer.h" @@ -270,6 +271,8 @@ // inhibit power management inhibitPowerManagement(); + m_mpris2Service = new Okular::Mpris2Service(m_document , m_ac, this, this); + show(); QTimer::singleShot( 0, this, &PresentationWidget::slotDelayedEvents ); @@ -309,6 +312,10 @@ delete *fIt; } +bool PresentationWidget::isAdvancingSlides() const +{ + return m_advanceSlides; +} void PresentationWidget::notifySetup( const QVector< Okular::Page * > & pageSet, int setupFlags ) { @@ -2452,6 +2459,8 @@ { m_nextPageTimer->stop(); } + + emit advancingSlidesChanged(m_advanceSlides); } #include "presentationwidget.moc"