diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,8 @@ endif() find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED Core Widgets Concurrent Svg OpenGL PrintSupport) +find_package(Qt5DBus ${QT_MIN_VERSION} CONFIG QUIET) +set(HAVE_QTDBUS ${Qt5DBus_FOUND}) find_package(Phonon4Qt5 4.6.60 NO_MODULE REQUIRED) include_directories(BEFORE ${PHONON_INCLUDES}) diff --git a/app/mainwindow.cpp b/app/mainwindow.cpp --- a/app/mainwindow.cpp +++ b/app/mainwindow.cpp @@ -88,6 +88,9 @@ #include #include #include +#ifdef HAVE_QTDBUS +#include +#endif #include #include #include @@ -166,6 +169,9 @@ SaveBar* mSaveBar; bool mStartSlideShowWhenDirListerCompleted; SlideShow* mSlideShow; +#ifdef HAVE_QTDBUS + Mpris2Service* mMpris2Service; +#endif Preloader* mPreloader; bool mPreloadDirectionIsForward; #ifdef KIPI_FOUND @@ -810,6 +816,13 @@ d->setupUndoActions(); d->setupContextManagerItems(); d->setupFullScreenContent(); + +#ifdef HAVE_QTDBUS + d->mMpris2Service = new Mpris2Service(d->mSlideShow, d->mContextManager, + d->mToggleSlideShowAction, d->mFullScreenAction, + d->mGoToPreviousAction, d->mGoToNextAction, this); +#endif + d->updateActions(); updatePreviousNextActions(); d->mSaveBar->initActionDependentWidgets(); diff --git a/config-gwenview.h.cmake b/config-gwenview.h.cmake --- a/config-gwenview.h.cmake +++ b/config-gwenview.h.cmake @@ -5,3 +5,4 @@ #define GV_TEST_DATA_DIR "@CMAKE_CURRENT_SOURCE_DIR@/tests/data" #cmakedefine HAVE_X11 ${HAVE_X11} #cmakedefine HAVE_FITS ${HAVE_FITS} +#cmakedefine HAVE_QTDBUS ${HAVE_QTDBUS} diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -176,6 +176,18 @@ ${GV_JPEG_DIR}/transupp.c ) +if(HAVE_QTDBUS) + set(gwenviewlib_SRCS + ${gwenviewlib_SRCS} + mpris2/lockscreenwatcher.cpp + mpris2/dbusabstractadaptor.cpp + mpris2/mpris2service.cpp + mpris2/mprismediaplayer2.cpp + mpris2/mprismediaplayer2player.cpp + ) + qt5_add_dbus_interface(gwenviewlib_SRCS mpris2/org.freedesktop.ScreenSaver.xml screensaverdbusinterface) +endif() + if(HAVE_FITS) set(gwenviewlib_SRCS ${gwenviewlib_SRCS} @@ -254,6 +266,9 @@ ${PHONON_LIBRARY} ) +if(HAVE_QTDBUS) + target_link_libraries(gwenviewlib Qt5::DBus) +endif() if(HAVE_FITS) target_link_libraries(gwenviewlib ${CFITSIO_LIBRARIES}) endif() diff --git a/lib/mpris2/dbusabstractadaptor.h b/lib/mpris2/dbusabstractadaptor.h new file mode 100644 --- /dev/null +++ b/lib/mpris2/dbusabstractadaptor.h @@ -0,0 +1,74 @@ +/* +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. +*/ + +#ifndef DBUSABSTRACTADAPTOR_H +#define DBUSABSTRACTADAPTOR_H + +// Qt +#include +#include + +namespace Gwenview +{ + +/** + * 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/lib/mpris2/dbusabstractadaptor.cpp b/lib/mpris2/dbusabstractadaptor.cpp new file mode 100644 --- /dev/null +++ b/lib/mpris2/dbusabstractadaptor.cpp @@ -0,0 +1,74 @@ +/* +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 "dbusabstractadaptor.h" + +// Qt +#include +#include +#include + +namespace Gwenview +{ + +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/lib/mpris2/lockscreenwatcher.h b/lib/mpris2/lockscreenwatcher.h new file mode 100644 --- /dev/null +++ b/lib/mpris2/lockscreenwatcher.h @@ -0,0 +1,61 @@ +/* +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. +*/ + +#ifndef LOCKSCREENWATCHER_H +#define LOCKSCREENWATCHER_H + +// Qt +#include + +class OrgFreedesktopScreenSaverInterface; +class QDBusPendingCallWatcher; + +namespace Gwenview +{ + +class LockScreenWatcher : public QObject +{ + Q_OBJECT + +public: + explicit LockScreenWatcher(QObject *parent); + ~LockScreenWatcher() override; + +public: + bool isLocked() const; + +Q_SIGNALS: + void isLockedChanged(bool locked); + +private: + void onScreenSaverActiveChanged(bool isActive); + void onActiveQueried(QDBusPendingCallWatcher *watcher); + void onScreenSaverServiceOwnerChanged(const QString &serviceName, + const QString &oldOwner, const QString &newOwner); + void onServiceRegisteredQueried(); + void onServiceOwnerQueried(); + +private: + OrgFreedesktopScreenSaverInterface *mScreenSaverInterface; + bool mLocked; +}; + +} + +#endif diff --git a/lib/mpris2/lockscreenwatcher.cpp b/lib/mpris2/lockscreenwatcher.cpp new file mode 100644 --- /dev/null +++ b/lib/mpris2/lockscreenwatcher.cpp @@ -0,0 +1,158 @@ +/* +Gwenview: an image viewer +Copyright 2013 Martin Gräßlin +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 "lockscreenwatcher.h" + +// lib +#include +// Qt +#include +#include +#include + +namespace Gwenview +{ + +using DBusBoolReplyWatcher = QFutureWatcher>; +using DBusStringReplyWatcher = QFutureWatcher>; + +inline QString screenSaverServiceName() { return QStringLiteral("org.freedesktop.ScreenSaver"); } + + +LockScreenWatcher::LockScreenWatcher(QObject *parent) + : QObject(parent) + , mScreenSaverInterface(nullptr) + , mLocked(false) +{ + QDBusServiceWatcher *screenLockServiceWatcher = new QDBusServiceWatcher(this); + connect(screenLockServiceWatcher, &QDBusServiceWatcher::serviceOwnerChanged, + this, &LockScreenWatcher::onScreenSaverServiceOwnerChanged); + screenLockServiceWatcher->setWatchMode(QDBusServiceWatcher::WatchForOwnerChange); + screenLockServiceWatcher->addWatchedService(screenSaverServiceName()); + + DBusBoolReplyWatcher *watcher = new DBusBoolReplyWatcher(this); + connect(watcher, &DBusBoolReplyWatcher::finished, + this, &LockScreenWatcher::onServiceRegisteredQueried); + connect(watcher, &DBusBoolReplyWatcher::canceled, + watcher, &DBusBoolReplyWatcher::deleteLater); + + watcher->setFuture(QtConcurrent::run(QDBusConnection::sessionBus().interface(), + &QDBusConnectionInterface::isServiceRegistered, + screenSaverServiceName())); +} + +LockScreenWatcher::~LockScreenWatcher() = default; + +bool LockScreenWatcher::isLocked() const +{ + return mLocked; +} + +void LockScreenWatcher::onScreenSaverServiceOwnerChanged(const QString &serviceName, + const QString &oldOwner, const QString &newOwner) +{ + Q_UNUSED(oldOwner) + + if (serviceName != screenSaverServiceName()) { + return; + } + + delete mScreenSaverInterface; + mScreenSaverInterface = nullptr; + + if (!newOwner.isEmpty()) { + mScreenSaverInterface = new OrgFreedesktopScreenSaverInterface(newOwner, QStringLiteral("/ScreenSaver"), + QDBusConnection::sessionBus(), this); + connect(mScreenSaverInterface, &OrgFreedesktopScreenSaverInterface::ActiveChanged, + this, &LockScreenWatcher::onScreenSaverActiveChanged); + + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(mScreenSaverInterface->GetActive(), this); + connect(watcher, &QDBusPendingCallWatcher::finished, + this, &LockScreenWatcher::onActiveQueried); + } else { + if (mLocked) { + // reset + mLocked = false; + emit isLockedChanged(mLocked); + } + } +} + +void LockScreenWatcher::onServiceRegisteredQueried() +{ + DBusBoolReplyWatcher *watcher = dynamic_cast(sender()); + if (!watcher) { + return; + } + + const QDBusReply &reply = watcher->result(); + + if (reply.isValid() && reply.value()) { + DBusStringReplyWatcher *ownerWatcher = new DBusStringReplyWatcher(this); + connect(ownerWatcher, &DBusStringReplyWatcher::finished, + this, &LockScreenWatcher::onServiceOwnerQueried); + connect(ownerWatcher, &DBusStringReplyWatcher::canceled, + ownerWatcher, &DBusStringReplyWatcher::deleteLater); + ownerWatcher->setFuture(QtConcurrent::run(QDBusConnection::sessionBus().interface(), + &QDBusConnectionInterface::serviceOwner, + screenSaverServiceName())); + } + + watcher->deleteLater(); +} + +void LockScreenWatcher::onServiceOwnerQueried() +{ + DBusStringReplyWatcher *watcher = dynamic_cast(sender()); + if (!watcher) { + return; + } + + const QDBusReply reply = watcher->result(); + + if (reply.isValid()) { + onScreenSaverServiceOwnerChanged(screenSaverServiceName(), QString(), reply.value()); + } + + watcher->deleteLater(); +} + +void LockScreenWatcher::onActiveQueried(QDBusPendingCallWatcher *watcher) +{ + QDBusPendingReply reply = *watcher; + if (!reply.isError()) { + onScreenSaverActiveChanged(reply.value()); + } + + watcher->deleteLater(); +} + +void LockScreenWatcher::onScreenSaverActiveChanged(bool isActive) +{ + if (mLocked == isActive) { + return; + } + + mLocked = isActive; + + emit isLockedChanged(mLocked); +} + +} diff --git a/lib/mpris2/mpris2service.h b/lib/mpris2/mpris2service.h new file mode 100644 --- /dev/null +++ b/lib/mpris2/mpris2service.h @@ -0,0 +1,57 @@ +/* +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. +*/ + +#ifndef MPRIS2SERVICE_H +#define MPRIS2SERVICE_H + +#include +// Qt +#include +#include + +class QAction; + +namespace Gwenview +{ +class SlideShow; +class ContextManager; + +class GWENVIEWLIB_EXPORT Mpris2Service : public QObject +{ + Q_OBJECT + +public: + Mpris2Service(SlideShow* slideShow, ContextManager* contextManager, + QAction* toggleSlideShowAction, QAction* fullScreenAction, + QAction* previousAction, QAction* nextAction, QObject* parent); + ~Mpris2Service() override; + +private: + void registerOnDBus(); + void unregisterOnDBus(); + + void onLockScreenLockedChanged(bool isLocked); + +private: + QString mMpris2ServiceName; +}; + +} + +#endif diff --git a/lib/mpris2/mpris2service.cpp b/lib/mpris2/mpris2service.cpp new file mode 100644 --- /dev/null +++ b/lib/mpris2/mpris2service.cpp @@ -0,0 +1,113 @@ +/* +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 "mpris2service.h" + +// lib +#include "lockscreenwatcher.h" +#include "mprismediaplayer2.h" +#include "mprismediaplayer2player.h" +#include +// Qt +#include +// std +#include + +namespace Gwenview +{ + +inline QString mediaPlayer2ObjectPath() { return QStringLiteral("/org/mpris/MediaPlayer2"); } + + +Mpris2Service::Mpris2Service(SlideShow* slideShow, ContextManager* contextManager, + QAction* toggleSlideShowAction, QAction* fullScreenAction, + QAction* previousAction, QAction* nextAction, + QObject* parent) + : QObject(parent) +{ + new MprisMediaPlayer2(mediaPlayer2ObjectPath(), fullScreenAction, this); + new MprisMediaPlayer2Player(mediaPlayer2ObjectPath(), slideShow, contextManager, + toggleSlideShowAction, fullScreenAction, previousAction, nextAction, + this); + + // To avoid appearing in the media controller on the lock screen, + // which might be not expected or wanted for Gwenview, + // the MPRIS service is unregistered while the lockscreen is active. + LockScreenWatcher *lockScreenWatcher = new LockScreenWatcher(this); + connect(lockScreenWatcher, &LockScreenWatcher::isLockedChanged, + this, &Mpris2Service::onLockScreenLockedChanged); + + if (!lockScreenWatcher->isLocked()) { + registerOnDBus(); + } +} + +Mpris2Service::~Mpris2Service() +{ + unregisterOnDBus(); +} + +void Mpris2Service::registerOnDBus() +{ + 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 + const bool objectRegistered = sessionBus.registerObject(mediaPlayer2ObjectPath(), this, QDBusConnection::ExportAdaptors); + + // try to register MPRIS presentation service + if (objectRegistered) { + mMpris2ServiceName = QStringLiteral("org.mpris.MediaPlayer2.Gwenview"); + + bool serviceRegistered = QDBusConnection::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 = QDBusConnection::sessionBus().registerService(mMpris2ServiceName); + } + if (!serviceRegistered) { + mMpris2ServiceName.clear(); + sessionBus.unregisterObject(mediaPlayer2ObjectPath()); + } + } +} + +void Mpris2Service::unregisterOnDBus() +{ + if (mMpris2ServiceName.isEmpty()) { + return; + } + + QDBusConnection sessionBus = QDBusConnection::sessionBus(); + sessionBus.unregisterService(mMpris2ServiceName); + sessionBus.unregisterObject(mediaPlayer2ObjectPath()); +} + +void Mpris2Service::onLockScreenLockedChanged(bool isLocked) +{ + if (isLocked) { + unregisterOnDBus(); + } else { + registerOnDBus(); + } +} + +} diff --git a/lib/mpris2/mprismediaplayer2.h b/lib/mpris2/mprismediaplayer2.h new file mode 100644 --- /dev/null +++ b/lib/mpris2/mprismediaplayer2.h @@ -0,0 +1,92 @@ +/* +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. +*/ + +#ifndef MPRISMEDIAPLAYER2_H +#define MPRISMEDIAPLAYER2_H + +#include "dbusabstractadaptor.h" +// Qt +#include + +class QAction; + +// Used QGuiApplication::desktopFileName() only available with Qt 5.7 +// DesktopEntry is an optional property for MPRIS MediaPlayer2, +// so simply only supported for Qt 5.7 & later +#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0) +#define GV_SUPPORT_MPRIS_DESKTOPENTRY 1 +#endif + +namespace Gwenview +{ + +// 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) +#ifdef GV_SUPPORT_MPRIS_DESKTOPENTRY + Q_PROPERTY(QString DesktopEntry READ desktopEntry CONSTANT) +#endif + + Q_PROPERTY(bool HasTrackList READ hasTrackList CONSTANT) + Q_PROPERTY(bool Fullscreen READ isFullscreen WRITE setFullscreen) + + Q_PROPERTY(QStringList SupportedUriSchemes READ supportedUriSchemes CONSTANT) + Q_PROPERTY(QStringList SupportedMimeTypes READ supportedMimeTypes CONSTANT) + +public: + MprisMediaPlayer2(const QString &objectDBusPath, QAction* fullScreenAction, 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; +#ifdef GV_SUPPORT_MPRIS_DESKTOPENTRY + QString desktopEntry() const; +#endif + bool isFullscreen() const; + void setFullscreen(bool isFullscreen); + + QStringList supportedUriSchemes() const; + QStringList supportedMimeTypes() const; + + void onFullScreenActionToggled(bool checked); + +private: + QAction* mFullScreenAction; +}; + +} + +#endif diff --git a/lib/mpris2/mprismediaplayer2.cpp b/lib/mpris2/mprismediaplayer2.cpp new file mode 100644 --- /dev/null +++ b/lib/mpris2/mprismediaplayer2.cpp @@ -0,0 +1,116 @@ +/* +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 "mprismediaplayer2.h" + +// Qt +#include +#include + +namespace Gwenview +{ + +MprisMediaPlayer2::MprisMediaPlayer2(const QString &objectDBusPath, + QAction* fullScreenAction, + QObject* parent) + : DBusAbstractAdaptor(objectDBusPath, parent) + , mFullScreenAction(fullScreenAction) +{ + connect(mFullScreenAction, &QAction::toggled, + this, &MprisMediaPlayer2::onFullScreenActionToggled); +} + +MprisMediaPlayer2::~MprisMediaPlayer2() +{ +} + +bool MprisMediaPlayer2::canQuit() const +{ + return true; +} + +bool MprisMediaPlayer2::canRaise() const +{ + // spec: "If true, calling Raise will cause the media application to attempt to bring its user interface to the front, + // Which seems to be about the app specific control UI, less about the rendered media (display). + // Could perhaps be supported by pulling in the toppanel when invoked? + return false; +} + +bool MprisMediaPlayer2::canSetFullscreen() const +{ + return true; +} + +bool MprisMediaPlayer2::hasTrackList() const +{ + return false; +} + +void MprisMediaPlayer2::Quit() +{ + QGuiApplication::quit(); +} + +void MprisMediaPlayer2::Raise() +{ +} + +QString MprisMediaPlayer2::identity() const +{ + return QGuiApplication::applicationDisplayName(); +} + +#ifdef GV_SUPPORT_MPRIS_DESKTOPENTRY +QString MprisMediaPlayer2::desktopEntry() const +{ + return QGuiApplication::desktopFileName(); +} +#endif + +QStringList MprisMediaPlayer2::supportedUriSchemes() const +{ + return QStringList(); +} + +QStringList MprisMediaPlayer2::supportedMimeTypes() const +{ + return QStringList(); +} + +bool MprisMediaPlayer2::isFullscreen() const +{ + return mFullScreenAction->isChecked(); +} + +void MprisMediaPlayer2::setFullscreen(bool isFullscreen) +{ + if (isFullscreen == mFullScreenAction->isChecked()) { + return; + } + + mFullScreenAction->trigger(); +} + +void MprisMediaPlayer2::onFullScreenActionToggled(bool checked) +{ + signalPropertyChange("Fullscreen", checked); +} + +} diff --git a/lib/mpris2/mprismediaplayer2player.h b/lib/mpris2/mprismediaplayer2player.h new file mode 100644 --- /dev/null +++ b/lib/mpris2/mprismediaplayer2player.h @@ -0,0 +1,128 @@ +/* +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. +*/ + +#ifndef MPRISMEDIAPLAYER2PLAYER_H +#define MPRISMEDIAPLAYER2PLAYER_H + +#include "dbusabstractadaptor.h" +// lib +#include + +class QDBusObjectPath; +class QAction; + +namespace Gwenview +{ +class SlideShow; +class ContextManager; + +// 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 Shuffle READ isShuffle WRITE setShuffle) + + 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, + SlideShow* slideShow, ContextManager* contextManager, + QAction* toggleSlideShowAction, QAction* fullScreenAction, + QAction* previousAction, QAction* nextAction, 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 isShuffle() 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); + void setShuffle(bool isShuffle); + + bool updatePlaybackStatus(); + + void onSlideShowStateChanged(); + void onCurrentUrlChanged(const QUrl& url); + void onRandomActionToggled(bool checked); + void onFullScreenActionToggled(); + void onToggleSlideShowActionChanged(); + void onPreviousActionChanged(); + void onNextActionChanged(); + void onMetaInfoUpdated(); + +private: + SlideShow* mSlideShow; + ContextManager* mContextManager; + QAction* mToggleSlideShowAction; + QAction* mFullScreenAction; + QAction* mPreviousAction; + QAction* mNextAction; + + bool mSlideShowEnabled; + bool mPreviousEnabled; + bool mNextEnabled; + QString mPlaybackStatus; + QVariantMap mMetaData; + Document::Ptr mCurrentDocument; +}; + +} + +#endif diff --git a/lib/mpris2/mprismediaplayer2player.cpp b/lib/mpris2/mprismediaplayer2player.cpp new file mode 100644 --- /dev/null +++ b/lib/mpris2/mprismediaplayer2player.cpp @@ -0,0 +1,378 @@ +/* +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(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; +} + +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("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(); +} + +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); + } + + // 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("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("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/mpris2/org.freedesktop.ScreenSaver.xml b/lib/mpris2/org.freedesktop.ScreenSaver.xml new file mode 100644 --- /dev/null +++ b/lib/mpris2/org.freedesktop.ScreenSaver.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/slideshow.h b/lib/slideshow.h --- a/lib/slideshow.h +++ b/lib/slideshow.h @@ -51,6 +51,8 @@ /** @return true if the slideshow is running */ bool isRunning() const; + int interval() const; + public Q_SLOTS: void setInterval(int); void setCurrentUrl(const QUrl &url); diff --git a/lib/slideshow.cpp b/lib/slideshow.cpp --- a/lib/slideshow.cpp +++ b/lib/slideshow.cpp @@ -240,6 +240,11 @@ d->updateTimerInterval(); } +int SlideShow::interval() const +{ + return GwenviewConfig::interval(); +} + void SlideShow::stop() { LOG("Stopping timer");