Expose slideshow to MPRIS controllers

Summary:
Taking the abstraction "Media" in "Media Player Remote Interfacing
Specification" (MPRIS) into use, a plain slide in a slideshow can be seen
to be the same as e.g. a still picture in a movie without any sound.
Following that, a slideshow with pictures and videos as in the UI model
of Gwenview can be roughly mapped onto the concept of a list of tracks as
with in the data model of MPRIS.

This patch exposes the list of images/movies in the current folder and
the related slideshow feature as MPRIS D-Bus object, implementing for a
start the interfaces
* "org.mpris.MediaPlayer2"
* "org.mpris.MediaPlayer2.Player"
trying to map those to the Gwenview UX closely.

This allows the slideshow and some other navigation to be controlled
by any MPRIS controllers, which includes e.g.
* keyboard mediakeys (Play/Pause, Stop, Next, Previous),
  as handled by Plasma MPRIS dataengine
* KDE Connect media player controller plugin

Additionally the MRPIS D-Bus object is unregistered while the workspace
lockscreen is activated. Because at least the Plasma lockscreen has
the feature to show controls for any currently running MPRIS players,
which for one does not make sense currently for an image player as all
displays are locked, and then also can be surprising for some users and
result in data leaks via the image metadata used.

Future:
The great plan is to enhance the MPRIS spec to also work well for
presentation-like media shows. So there can and will be cross-app
rich (remote) controllers also for classical presentation application
instead of only per-app ones.
Supporting the existing MPRIS interfaces, even if slightly bending the
semantics, is a first step into that direction and already adds value
as it allows reuse of existing MPRIS controllers e.g. for remote control.

So follow-up work will be to also implement the other MPRIS interfaces
like org.mpris.MediaPlayer2.TrackList, for exposing the whole set of
"slides". This will enable MPRIS controllers to show the user a complete
visual list and e.g. navigate directly to a given slide or give a preview
for the next/previous slides.

CCBUG: 359381

Reviewers: #gwenview, rkflx

Reviewed By: #gwenview, rkflx

Subscribers: mtijink, ngraham, nicolasfella, #kde_connect, rkflx, broulik

Tags: #gwenview

Differential Revision: https://phabricator.kde.org/D10972
parent 8bd2f625
......@@ -71,6 +71,8 @@ else()
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})
......
......@@ -88,6 +88,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include <lib/gvdebug.h>
#include <lib/gwenviewconfig.h>
#include <lib/mimetypeutils.h>
#ifdef HAVE_QTDBUS
#include <lib/mpris2/mpris2service.h>
#endif
#include <lib/print/printhelper.h>
#include <lib/slideshow.h>
#include <lib/signalblocker.h>
......@@ -166,6 +169,9 @@ struct MainWindow::Private
SaveBar* mSaveBar;
bool mStartSlideShowWhenDirListerCompleted;
SlideShow* mSlideShow;
#ifdef HAVE_QTDBUS
Mpris2Service* mMpris2Service;
#endif
Preloader* mPreloader;
bool mPreloadDirectionIsForward;
#ifdef KIPI_FOUND
......@@ -810,6 +816,13 @@ MainWindow::MainWindow()
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();
......
......@@ -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}
......@@ -176,6 +176,18 @@ set(gwenviewlib_SRCS
${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 @@ target_link_libraries(gwenviewlib
${PHONON_LIBRARY}
)
if(HAVE_QTDBUS)
target_link_libraries(gwenviewlib Qt5::DBus)
endif()
if(HAVE_FITS)
target_link_libraries(gwenviewlib ${CFITSIO_LIBRARIES})
endif()
......
/*
Gwenview: an image viewer
Copyright 2018 Friedrich W. H. Kossebau <kossebau@kde.org>
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 <QMetaClassInfo>
#include <QDBusMessage>
#include <QDBusConnection>
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();
}
}
/*
Gwenview: an image viewer
Copyright 2018 Friedrich W. H. Kossebau <kossebau@kde.org>
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 <QDBusAbstractAdaptor>
#include <QVariantMap>
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
/*
Gwenview: an image viewer
Copyright 2013 Martin Gräßlin <mgraesslin@kde.org>
Copyright 2018 Friedrich W. H. Kossebau <kossebau@kde.org>
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 <screensaverdbusinterface.h>
// Qt
#include <QDBusServiceWatcher>
#include <QFutureWatcher>
#include <QtConcurrentRun>
namespace Gwenview
{
using DBusBoolReplyWatcher = QFutureWatcher<QDBusReply<bool>>;
using DBusStringReplyWatcher = QFutureWatcher<QDBusReply<QString>>;
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<DBusBoolReplyWatcher*>(sender());
if (!watcher) {
return;
}
const QDBusReply<bool> &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<DBusStringReplyWatcher*>(sender());
if (!watcher) {
return;
}
const QDBusReply<QString> reply = watcher->result();
if (reply.isValid()) {
onScreenSaverServiceOwnerChanged(screenSaverServiceName(), QString(), reply.value());
}
watcher->deleteLater();
}
void LockScreenWatcher::onActiveQueried(QDBusPendingCallWatcher *watcher)
{
QDBusPendingReply<bool> reply = *watcher;
if (!reply.isError()) {
onScreenSaverActiveChanged(reply.value());
}
watcher->deleteLater();
}
void LockScreenWatcher::onScreenSaverActiveChanged(bool isActive)
{
if (mLocked == isActive) {
return;
}
mLocked = isActive;
emit isLockedChanged(mLocked);
}
}
/*
Gwenview: an image viewer
Copyright 2018 Friedrich W. H. Kossebau <kossebau@kde.org>
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 <QObject>
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
/*
Gwenview: an image viewer
Copyright 2018 Friedrich W. H. Kossebau <kossebau@kde.org>
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 <slideshow.h>
// Qt
#include <QDBusConnection>
// std
#include <unistd.h>
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();
}
}
}
/*
Gwenview: an image viewer
Copyright 2018 Friedrich W. H. Kossebau <kossebau@kde.org>
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 <gwenviewlib_export.h>
// Qt
#include <QObject>
#include <QString>
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
/*
Gwenview: an image viewer
Copyright 2018 Friedrich W. H. Kossebau <kossebau@kde.org>
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 <QGuiApplication>
#include <QAction>
namespace Gwenview
{