diff --git a/connector/CMakeLists.txt b/connector/CMakeLists.txt index 447c427..dc58883 100644 --- a/connector/CMakeLists.txt +++ b/connector/CMakeLists.txt @@ -45,6 +45,35 @@ list(APPEND CMAKE_MODULE_PATH ${QT_UTILITIES_MODULE_DIRS}) # link also explicitely against the following Qt 5 modules list(APPEND ADDITIONAL_QT_MODULES Network) +# configure support for D-Bus notifications +option(SYSTEMD_SUPPORT "enables support for controlling Syncthing systemd service" ${UNIX}) +if(DBUS_NOTIFICATIONS) + list(APPEND HEADER_FILES + syncthingservice.h + ) + list(APPEND SRC_FILES + syncthingservice.cpp + ) + list(APPEND DBUS_FILES + org.freedesktop.DBus.Properties.xml + org.freedesktop.systemd1.Manager.xml + org.freedesktop.systemd1.Service.xml + org.freedesktop.systemd1.Unit.xml + ) + set_source_files_properties( + org.freedesktop.systemd1.Manager.xml + PROPERTIES INCLUDE syncthingservice.h + ) + list(APPEND META_PUBLIC_COMPILE_DEFINITIONS LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD) + message(STATUS "systemd support enabled") +else() + list(APPEND DOC_ONLY_FILES + syncthingservice.h + syncthingservice.cpp + ) + message(STATUS "systemd support disabled") +endif() + # include modules to apply configuration include(BasicConfig) include(QtConfig) diff --git a/connector/org.freedesktop.DBus.Properties.xml b/connector/org.freedesktop.DBus.Properties.xml new file mode 100644 index 0000000..a0af918 --- /dev/null +++ b/connector/org.freedesktop.DBus.Properties.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/connector/org.freedesktop.systemd1.Manager.xml b/connector/org.freedesktop.systemd1.Manager.xml new file mode 100644 index 0000000..472c2ed --- /dev/null +++ b/connector/org.freedesktop.systemd1.Manager.xml @@ -0,0 +1,573 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/connector/org.freedesktop.systemd1.Service.xml b/connector/org.freedesktop.systemd1.Service.xml new file mode 100644 index 0000000..3e8897e --- /dev/null +++ b/connector/org.freedesktop.systemd1.Service.xml @@ -0,0 +1,524 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/connector/org.freedesktop.systemd1.Unit.xml b/connector/org.freedesktop.systemd1.Unit.xml new file mode 100644 index 0000000..fb6ad67 --- /dev/null +++ b/connector/org.freedesktop.systemd1.Unit.xml @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/connector/syncthingconnection.cpp b/connector/syncthingconnection.cpp index e60b1b1..731c7bf 100644 --- a/connector/syncthingconnection.cpp +++ b/connector/syncthingconnection.cpp @@ -1,6 +1,7 @@ #include "./syncthingconnection.h" #include "./syncthingconfig.h" #include "./syncthingconnectionsettings.h" +#include "./utils.h" #include #include @@ -632,11 +633,7 @@ void SyncthingConnection::loadSelfSignedCertificate() } // only possible if the Syncthing instance is running on the local machine - const QString host(syncthingUrl.host()); - const QHostAddress hostAddress(host); - if(host.compare(QLatin1String("localhost"), Qt::CaseInsensitive) != 0 - && !hostAddress.isLoopback() - && !QNetworkInterface::allAddresses().contains(hostAddress)) { + if(!isLocal(syncthingUrl)) { return; } diff --git a/connector/syncthingservice.cpp b/connector/syncthingservice.cpp new file mode 100644 index 0000000..23a9ef5 --- /dev/null +++ b/connector/syncthingservice.cpp @@ -0,0 +1,248 @@ +#include "./syncthingservice.h" + +#include "managerinterface.h" +#include "unitinterface.h" +#include "serviceinterface.h" +#include "propertiesinterface.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace std; +using namespace std::placeholders; + +namespace Data { + +QDBusArgument &operator<<(QDBusArgument &argument, const ManagerDBusUnitFileChange &unitFileChange) +{ + argument.beginStructure(); + argument << unitFileChange.type << unitFileChange.path << unitFileChange.source; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, ManagerDBusUnitFileChange &unitFileChange) +{ + argument.beginStructure(); + argument >> unitFileChange.type >> unitFileChange.path >> unitFileChange.source; + argument.endStructure(); + return argument; +} + +OrgFreedesktopSystemd1ManagerInterface *SyncthingService::s_manager = nullptr; + +SyncthingService::SyncthingService(QObject *parent) : + QObject(parent), + m_unit(nullptr), + m_service(nullptr), + m_properties(nullptr) +{ + if(!s_manager) { + // register custom data types + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + s_manager = new OrgFreedesktopSystemd1ManagerInterface( + QStringLiteral("org.freedesktop.systemd1"), + QStringLiteral("/org/freedesktop/systemd1"), + QDBusConnection::sessionBus() + ); + + // enable systemd to emit signals + s_manager->Subscribe(); + } + connect(s_manager, &OrgFreedesktopSystemd1ManagerInterface::UnitNew, this, &SyncthingService::handleUnitAdded); + connect(s_manager, &OrgFreedesktopSystemd1ManagerInterface::UnitRemoved, this, &SyncthingService::handleUnitRemoved); + m_serviceWatcher = new QDBusServiceWatcher(s_manager->service(), s_manager->connection()); +} + +void SyncthingService::setUnitName(const QString &unitName) +{ + if(m_unitName != unitName) { + m_unitName = unitName; + + delete m_service, delete m_unit, delete m_properties; + m_service = nullptr, m_unit = nullptr, m_properties = nullptr; + setProperties(QString(), QString(), QString(), QString()); + + if(s_manager->isValid()) { + connect(new QDBusPendingCallWatcher(s_manager->GetUnit(m_unitName), this), &QDBusPendingCallWatcher::finished, this, &SyncthingService::handleUnitGet); + } + } +} + +bool SyncthingService::isSystemdAvailable() const +{ + return s_manager && s_manager->isValid(); +} + +bool SyncthingService::isUnitAvailable() const +{ + return m_unit && m_unit->isValid(); +} + +void SyncthingService::setRunning(bool running) +{ + if(running) { + registerErrorHandler(s_manager->StartUnit(m_unitName, QStringLiteral("replace")), QT_TR_NOOP_UTF8("starting unit")); + } else { + registerErrorHandler(s_manager->StopUnit(m_unitName, QStringLiteral("replace")), QT_TR_NOOP_UTF8("stopping unit")); + } +} + +void SyncthingService::setEnabled(bool enabled) +{ + if(enabled) { + registerErrorHandler(s_manager->EnableUnitFiles(QStringList(m_unitName), false, true), QT_TR_NOOP_UTF8("enabling unit")); + } else { + registerErrorHandler(s_manager->DisableUnitFiles(QStringList(m_unitName), false), QT_TR_NOOP_UTF8("disabling unit")); + } +} + +void SyncthingService::handleUnitAdded(const QString &unitName, const QDBusObjectPath &unitPath) +{ + if(unitName == m_unitName) { + setUnit(unitPath); + } +} + +void SyncthingService::handleUnitRemoved(const QString &unitName, const QDBusObjectPath &unitPath) +{ + Q_UNUSED(unitPath) + if(unitName == m_unitName) { + setUnit(QDBusObjectPath()); + } +} + +void SyncthingService::handleUnitGet(QDBusPendingCallWatcher *watcher) +{ + watcher->deleteLater(); + + const QDBusPendingReply unitReply = *watcher; + if(unitReply.isError()) { + return; + } + + setUnit(unitReply.value()); +} + +void SyncthingService::handlePropertiesChanged(const QString &interface, const QVariantMap &changedProperties, const QStringList &invalidatedProperties) +{ + if(interface == m_unit->interface()) { + const bool running = isRunning(); + if(handlePropertyChanged(m_activeState, &SyncthingService::activeStateChanged, QStringLiteral("ActiveState"), changedProperties, invalidatedProperties) + | handlePropertyChanged(m_subState, &SyncthingService::subStateChanged, QStringLiteral("SubState"), changedProperties, invalidatedProperties)) { + emit stateChanged(m_activeState, m_subState); + } + if(running != isRunning()) { + emit runningChanged(isRunning()); + } + + const bool enabled = isEnabled(); + handlePropertyChanged(m_unitFileState, &SyncthingService::unitFileStateChanged, QStringLiteral("UnitFileState"), changedProperties, invalidatedProperties); + if(enabled != isEnabled()) { + emit enabledChanged(isEnabled()); + } + + handlePropertyChanged(m_description, &SyncthingService::descriptionChanged, QStringLiteral("Description"), changedProperties, invalidatedProperties); + } +} + +void SyncthingService::handleError(const char *context, QDBusPendingCallWatcher *watcher) +{ + watcher->deleteLater(); + const QDBusError error = watcher->error(); + if(error.isValid()) { + emit errorOccurred(tr(context), error.name(), error.message()); + } +} + +bool SyncthingService::handlePropertyChanged(QString &variable, void (SyncthingService::*signal)(const QString &), const QString &propertyName, const QVariantMap &changedProperties, const QStringList &invalidatedProperties) +{ + const QVariant valueVariant(changedProperties[propertyName]); + if(valueVariant.isValid()) { + const QString valueString(valueVariant.toString()); + if(valueString != variable) { + emit (this->*signal)(variable = valueString); + return true; + } + } else if(invalidatedProperties.contains(propertyName) && !variable.isEmpty()) { + variable.clear(); + emit (this->*signal)(variable); + return true; + } + return false; +} + +void SyncthingService::registerErrorHandler(const QDBusPendingCall &call, const char *context) +{ + connect(new QDBusPendingCallWatcher(call, this), &QDBusPendingCallWatcher::finished, bind(&SyncthingService::handleError, this, context, _1)); +} + +void SyncthingService::setUnit(const QDBusObjectPath &objectPath) +{ + // cleanup + delete m_service, delete m_unit, delete m_properties; + m_service = nullptr, m_unit = nullptr, m_properties = nullptr; + + const QString path = objectPath.path(); + if(path.isEmpty()) { + setProperties(QString(), QString(), QString(), QString()); + return; + } + + // init unit + m_unit = new OrgFreedesktopSystemd1UnitInterface(s_manager->service(), path, s_manager->connection()); + setProperties(m_unit->activeState(), m_unit->subState(), m_unit->unitFileState(), m_unit->description()); + + // init properties + m_properties = new OrgFreedesktopDBusPropertiesInterface(s_manager->service(), path, s_manager->connection()); + connect(m_properties, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged, this, &SyncthingService::handlePropertiesChanged); +} + +void SyncthingService::setProperties(const QString &activeState, const QString &subState, const QString &unitFileState, const QString &description) +{ + const bool running = isRunning(); + bool anyStateChanged = false; + if(m_activeState != activeState) { + emit activeStateChanged(m_activeState = activeState); + anyStateChanged = true; + } + if(m_subState != subState) { + emit subStateChanged(m_subState = subState); + anyStateChanged = true; + } + if(anyStateChanged) { + emit stateChanged(m_activeState, m_subState); + } + if(running != isRunning()) { + emit runningChanged(isRunning()); + } + + const bool enabled = isEnabled(); + if(m_unitFileState != unitFileState) { + emit unitFileStateChanged(m_unitFileState = unitFileState); + } + if(enabled != isEnabled()) { + emit enabledChanged(isEnabled()); + } + + if(m_description != description) { + emit descriptionChanged(m_description = description); + } +} + +SyncthingService &syncthingService() +{ + static SyncthingService service; + return service; +} + +} // namespace Data diff --git a/connector/syncthingservice.h b/connector/syncthingservice.h new file mode 100644 index 0000000..bc42fed --- /dev/null +++ b/connector/syncthingservice.h @@ -0,0 +1,170 @@ +#ifndef DATA_SYNCTHINGSERVICE_H +#define DATA_SYNCTHINGSERVICE_H + +#include +#include + +QT_FORWARD_DECLARE_CLASS(QDBusServiceWatcher) +QT_FORWARD_DECLARE_CLASS(QDBusArgument) +QT_FORWARD_DECLARE_CLASS(QDBusObjectPath) +QT_FORWARD_DECLARE_CLASS(QDBusPendingCall) +QT_FORWARD_DECLARE_CLASS(QDBusPendingCallWatcher) + +class OrgFreedesktopSystemd1ManagerInterface; +class OrgFreedesktopSystemd1UnitInterface; +class OrgFreedesktopSystemd1ServiceInterface; +class OrgFreedesktopDBusPropertiesInterface; + +namespace Data { + +struct ManagerDBusUnitFileChange { + QString type; + QString path; + QString source; +}; + +QDBusArgument &operator<<(QDBusArgument &argument, const ManagerDBusUnitFileChange &unitFileChange); +const QDBusArgument &operator>>(const QDBusArgument &argument, ManagerDBusUnitFileChange &unitFileChange); + +typedef QList ManagerDBusUnitFileChangeList; + +class SyncthingService : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString unitName READ unitName WRITE setUnitName) + Q_PROPERTY(bool systemdAvailable READ isSystemdAvailable NOTIFY systemdAvailableChanged) + Q_PROPERTY(bool unitAvailable READ isUnitAvailable) + Q_PROPERTY(QString activeState READ activeState NOTIFY activeStateChanged) + Q_PROPERTY(QString subState READ subState NOTIFY subStateChanged) + Q_PROPERTY(QString unitFileState READ unitFileState NOTIFY unitFileStateChanged) + Q_PROPERTY(QString description READ description NOTIFY descriptionChanged) + Q_PROPERTY(bool running READ isRunning WRITE setRunning NOTIFY runningChanged) + Q_PROPERTY(bool enable READ isEnabled WRITE setEnabled NOTIFY enabledChanged) + +public: + SyncthingService(QObject *parent = nullptr); + + const QString &unitName() const; + bool isSystemdAvailable() const; + bool isUnitAvailable() const; + const QString &activeState() const; + const QString &subState() const; + const QString &unitFileState() const; + const QString &description() const; + bool isRunning() const; + bool isEnabled() const; + +public Q_SLOTS: + void setUnitName(const QString &unitName); + void setRunning(bool running); + void start(); + void stop(); + void toggleRunning(); + void setEnabled(bool enable); + void enable(); + void disable(); + +Q_SIGNALS: + void systemdAvailableChanged(bool available); + void stateChanged(const QString &activeState, const QString &subState); + void activeStateChanged(const QString &activeState); + void subStateChanged(const QString &subState); + void unitFileStateChanged(const QString &unitFileState); + void descriptionChanged(const QString &description); + void runningChanged(bool running); + void enabledChanged(bool enable); + void errorOccurred(const QString &context, const QString &name, const QString &message); + +private Q_SLOTS: + void handleUnitAdded(const QString &unitName, const QDBusObjectPath &unitPath); + void handleUnitRemoved(const QString &unitName, const QDBusObjectPath &unitPath); + void handleUnitGet(QDBusPendingCallWatcher *watcher); + void handlePropertiesChanged(const QString &interface, const QVariantMap &changedProperties, const QStringList &invalidatedProperties); + void handleError(const char *error, QDBusPendingCallWatcher *watcher); + void setUnit(const QDBusObjectPath &objectPath); + void setProperties(const QString &activeState, const QString &subState, const QString &unitFileState, const QString &description); + +private: + bool handlePropertyChanged(QString &variable, void(SyncthingService::*signal)(const QString &), const QString &propertyName, const QVariantMap &changedProperties, const QStringList &invalidatedProperties); + void registerErrorHandler(const QDBusPendingCall &call, const char *context); + + static OrgFreedesktopSystemd1ManagerInterface *s_manager; + QString m_unitName; + QDBusServiceWatcher *m_serviceWatcher; + OrgFreedesktopSystemd1UnitInterface *m_unit; + OrgFreedesktopSystemd1ServiceInterface *m_service; + OrgFreedesktopDBusPropertiesInterface *m_properties; + QString m_description; + QString m_activeState; + QString m_subState; + QString m_unitFileState; +}; + +inline const QString &SyncthingService::unitName() const +{ + return m_unitName; +} + +inline const QString &SyncthingService::activeState() const +{ + return m_activeState; +} + +inline const QString &SyncthingService::subState() const +{ + return m_subState; +} + +inline const QString &SyncthingService::unitFileState() const +{ + return m_unitFileState; +} + +inline const QString &SyncthingService::description() const +{ + return m_description; +} + +inline bool SyncthingService::isRunning() const +{ + return m_activeState == QLatin1String("active") && m_subState == QLatin1String("running"); +} + +inline void SyncthingService::start() +{ + setRunning(true); +} + +inline void SyncthingService::stop() +{ + setRunning(false); +} + +inline void SyncthingService::toggleRunning() +{ + setRunning(!isRunning()); +} + +inline bool SyncthingService::isEnabled() const +{ + return m_unitFileState == QLatin1String("enabled"); +} + +inline void SyncthingService::enable() +{ + setEnabled(true); +} + +inline void SyncthingService::disable() +{ + setEnabled(false); +} + +SyncthingService &syncthingService(); + +} // namespace Data + +Q_DECLARE_METATYPE(Data::ManagerDBusUnitFileChange) +Q_DECLARE_METATYPE(Data::ManagerDBusUnitFileChangeList) + +#endif // DATA_SYNCTHINGSERVICE_H diff --git a/connector/utils.cpp b/connector/utils.cpp index f0e92f6..8f5ed7f 100644 --- a/connector/utils.cpp +++ b/connector/utils.cpp @@ -3,20 +3,38 @@ #include #include +#include +#include +#include #include using namespace ChronoUtilities; namespace Data { +/*! + * \brief Returns a string like "2 min 45 s ago" for the specified \a dateTime. + */ QString agoString(DateTime dateTime) { const TimeSpan delta(DateTime::now() - dateTime); if(!delta.isNegative() && static_cast(delta.totalTicks()) > (TimeSpan::ticksPerMinute / 4uL)) { - return QCoreApplication::translate("Data::Utils", "%1 ago").arg(QString::fromLatin1(delta.toString(TimeSpanOutputFormat::WithMeasures, true).data())); + return QCoreApplication::translate("Data::Utils", "%1 ago").arg(QString::fromUtf8(delta.toString(TimeSpanOutputFormat::WithMeasures, true).data())); } else { return QCoreApplication::translate("Data::Utils", "right now"); } } +/*! + * \brief Returns whether the host specified by the given \a url is the local machine. + */ +bool isLocal(const QUrl &url) +{ + const QString host(url.host()); + const QHostAddress hostAddress(host); + return host.compare(QLatin1String("localhost"), Qt::CaseInsensitive) == 0 + || hostAddress.isLoopback() + || QNetworkInterface::allAddresses().contains(hostAddress); +} + } diff --git a/connector/utils.h b/connector/utils.h index 72044e4..54ab912 100644 --- a/connector/utils.h +++ b/connector/utils.h @@ -6,6 +6,7 @@ #include QT_FORWARD_DECLARE_CLASS(QString) +QT_FORWARD_DECLARE_CLASS(QUrl) namespace ChronoUtilities { class DateTime; @@ -14,6 +15,7 @@ class DateTime; namespace Data { QString LIB_SYNCTHING_CONNECTOR_EXPORT agoString(ChronoUtilities::DateTime dateTime); +bool LIB_SYNCTHING_CONNECTOR_EXPORT isLocal(const QUrl &url); } diff --git a/tray/CMakeLists.txt b/tray/CMakeLists.txt index fb85e98..f15a2f3 100644 --- a/tray/CMakeLists.txt +++ b/tray/CMakeLists.txt @@ -52,6 +52,7 @@ set(WIDGETS_UI_FILES gui/appearanceoptionpage.ui gui/autostartoptionpage.ui gui/launcheroptionpage.ui + gui/systemdoptionpage.ui gui/webviewoptionpage.ui ) diff --git a/tray/application/main.cpp b/tray/application/main.cpp index 52a77d8..832b179 100644 --- a/tray/application/main.cpp +++ b/tray/application/main.cpp @@ -5,6 +5,9 @@ #include "../gui/traywidget.h" #include "../../connector/syncthingprocess.h" +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD +# include "../../connector/syncthingservice.h" +#endif #include "resources/config.h" @@ -20,6 +23,7 @@ #include #include #include +#include #include @@ -28,9 +32,25 @@ using namespace ApplicationUtilities; using namespace QtGui; using namespace Data; +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD +void handleSystemdServiceError(const QString &context, const QString &name, const QString &message) +{ + QMessageBox msgBox; + msgBox.setIcon(QMessageBox::Critical); + msgBox.setText(QCoreApplication::translate("main", "Unable to ") + context); + msgBox.setInformativeText(name % QStringLiteral(": ") % message); + msgBox.exec(); +} +#endif + int initSyncthingTray(bool windowed, bool waitForTray) { auto &v = Settings::values(); +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD + SyncthingService &service = syncthingService(); + service.setUnitName(v.systemd.syncthingUnit); + QObject::connect(&service, &SyncthingService::errorOccurred, &handleSystemdServiceError); +#endif if(windowed) { if(v.launcher.enabled) { syncthingProcess().startSyncthing(v.launcher.syncthingCmd()); diff --git a/tray/application/settings.cpp b/tray/application/settings.cpp index a6c2b12..b932833 100644 --- a/tray/application/settings.cpp +++ b/tray/application/settings.cpp @@ -96,6 +96,11 @@ void restore() launcher.enabled = settings.value(QStringLiteral("launchSynchting"), launcher.enabled).toBool(); launcher.syncthingPath = settings.value(QStringLiteral("syncthingPath"), launcher.syncthingPath).toString(); launcher.syncthingArgs = settings.value(QStringLiteral("syncthingArgs"), launcher.syncthingArgs).toString(); +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD + auto &systemd = v.systemd; + systemd.syncthingUnit = settings.value(QStringLiteral("syncthingUnit"), systemd.syncthingUnit).toString(); + systemd.showButton = settings.value(QStringLiteral("showButton"), systemd.showButton).toBool(); +#endif settings.endGroup(); #if defined(SYNCTHINGTRAY_USE_WEBENGINE) || defined(SYNCTHINGTRAY_USE_WEBKIT) @@ -158,6 +163,11 @@ void save() settings.setValue(QStringLiteral("launchSynchting"), launcher.enabled); settings.setValue(QStringLiteral("syncthingPath"), launcher.syncthingPath); settings.setValue(QStringLiteral("syncthingArgs"), launcher.syncthingArgs); +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD + const auto &systemd = v.systemd; + settings.setValue(QStringLiteral("syncthingUnit"), systemd.syncthingUnit); + settings.setValue(QStringLiteral("showButton"), systemd.showButton); +#endif settings.endGroup(); #if defined(SYNCTHINGTRAY_USE_WEBENGINE) || defined(SYNCTHINGTRAY_USE_WEBKIT) diff --git a/tray/application/settings.h b/tray/application/settings.h index 9ccda72..819a395 100644 --- a/tray/application/settings.h +++ b/tray/application/settings.h @@ -62,6 +62,14 @@ struct Launcher QString syncthingCmd() const; }; +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD +struct Systemd +{ + QString syncthingUnit = QStringLiteral("syncthing.service"); + bool showButton = true; +}; +#endif + #if defined(SYNCTHINGTRAY_USE_WEBENGINE) || defined(SYNCTHINGTRAY_USE_WEBKIT) struct WebView { @@ -82,6 +90,9 @@ struct Settings #endif Appearance appearance; Launcher launcher; +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD + Systemd systemd; +#endif #if defined(SYNCTHINGTRAY_USE_WEBENGINE) || defined(SYNCTHINGTRAY_USE_WEBKIT) WebView webView; #endif diff --git a/tray/gui/settingsdialog.cpp b/tray/gui/settingsdialog.cpp index d82f6d2..20b593b 100644 --- a/tray/gui/settingsdialog.cpp +++ b/tray/gui/settingsdialog.cpp @@ -3,12 +3,19 @@ #include "../../connector/syncthingconnection.h" #include "../../connector/syncthingconfig.h" #include "../../connector/syncthingprocess.h" +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD +# include "../../connector/syncthingservice.h" +# include "../../model/colors.h" +#endif #include "ui_connectionoptionpage.h" #include "ui_notificationsoptionpage.h" #include "ui_appearanceoptionpage.h" #include "ui_autostartoptionpage.h" #include "ui_launcheroptionpage.h" +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD +# include "ui_systemdoptionpage.h" +#endif #include "ui_webviewoptionpage.h" #include "resources/config.h" @@ -19,6 +26,9 @@ #ifdef QT_UTILITIES_SUPPORT_DBUS_NOTIFICATIONS # include #endif +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD +# include +#endif #include #include @@ -556,6 +566,98 @@ void LauncherOptionPage::stop() } } +// SystemdOptionPage +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD +SystemdOptionPage::SystemdOptionPage(QWidget *parentWidget) : + SystemdOptionPageBase(parentWidget), + m_service(syncthingService()) +{} + +SystemdOptionPage::~SystemdOptionPage() +{} + +QWidget *SystemdOptionPage::setupWidget() +{ + auto *widget = SystemdOptionPageBase::setupWidget(); + QObject::connect(ui()->syncthingUnitLineEdit, &QLineEdit::textChanged, &m_service, &SyncthingService::setUnitName); + QObject::connect(ui()->startPushButton, &QPushButton::clicked, &m_service, &SyncthingService::start); + QObject::connect(ui()->stopPushButton, &QPushButton::clicked, &m_service, &SyncthingService::stop); + QObject::connect(ui()->enablePushButton, &QPushButton::clicked, &m_service, &SyncthingService::enable); + QObject::connect(ui()->disablePushButton, &QPushButton::clicked, &m_service, &SyncthingService::disable); + QObject::connect(&m_service, &SyncthingService::descriptionChanged, bind(&SystemdOptionPage::handleDescriptionChanged, this, _1)); + QObject::connect(&m_service, &SyncthingService::stateChanged, bind(&SystemdOptionPage::handleStatusChanged, this, _1, _2)); + QObject::connect(&m_service, &SyncthingService::unitFileStateChanged, bind(&SystemdOptionPage::handleEnabledChanged, this, _1)); + return widget; +} + +bool SystemdOptionPage::apply() +{ + if(hasBeenShown()) { + auto &settings = values().systemd; + settings.syncthingUnit = ui()->syncthingUnitLineEdit->text(); + settings.showButton = ui()->showButtonCheckBox->isChecked(); + } + return true; +} + +void SystemdOptionPage::reset() +{ + if(hasBeenShown()) { + const auto &settings = values().systemd; + ui()->syncthingUnitLineEdit->setText(settings.syncthingUnit); + ui()->showButtonCheckBox->setChecked(settings.showButton); + handleDescriptionChanged(m_service.description()); + handleStatusChanged(m_service.activeState(), m_service.subState()); + handleEnabledChanged(m_service.unitFileState()); + } +} + +void SystemdOptionPage::handleDescriptionChanged(const QString &description) +{ + ui()->descriptionValueLabel->setText(description.isEmpty() ? QCoreApplication::translate("QtGui::SystemdOptionPage", "specified unit is unknown") : description); +} + +void setIndicatorColor(QWidget *indicator, const QColor &color) +{ + indicator->setStyleSheet(QStringLiteral("border-radius:8px;background-color:") + color.name()); +} + +void SystemdOptionPage::handleStatusChanged(const QString &activeState, const QString &subState) +{ + QStringList status; + if(!activeState.isEmpty()) { + status << activeState; + } + if(!subState.isEmpty()) { + status << subState; + } + const bool isRunning = m_service.isRunning(); + + ui()->statusValueLabel->setText(status.isEmpty() + ? QCoreApplication::translate("QtGui::SystemdOptionPage", "unknown") + : status.join(QStringLiteral(" - "))); + setIndicatorColor(ui()->statusIndicator, status.isEmpty() + ? Colors::gray(values().appearance.brightTextColors) + : (isRunning + ? Colors::green(values().appearance.brightTextColors) + : Colors::red(values().appearance.brightTextColors)) + ); + ui()->startPushButton->setVisible(!status.isEmpty() && !isRunning); + ui()->stopPushButton->setVisible(!status.isEmpty() && isRunning); +} + +void SystemdOptionPage::handleEnabledChanged(const QString &unitFileState) +{ + const bool isEnabled = m_service.isEnabled(); + ui()->unitFileStateValueLabel->setText(unitFileState.isEmpty() ? QCoreApplication::translate("QtGui::SystemdOptionPage", "unknown") : unitFileState); + setIndicatorColor(ui()->enabledIndicator, isEnabled + ? Colors::green(values().appearance.brightTextColors) + : Colors::gray(values().appearance.brightTextColors)); + ui()->enablePushButton->setVisible(!unitFileState.isEmpty() && !isEnabled); + ui()->disablePushButton->setVisible(!unitFileState.isEmpty() && isEnabled); +} +#endif + // WebViewOptionPage WebViewOptionPage::WebViewOptionPage(QWidget *parentWidget) : WebViewOptionPageBase(parentWidget) @@ -621,7 +723,11 @@ SettingsDialog::SettingsDialog(Data::SyncthingConnection *connection, QWidget *p category = new OptionCategory(this); category->setDisplayName(tr("Startup")); - category->assignPages(QList() << new AutostartOptionPage << new LauncherOptionPage); + category->assignPages(QList() << new AutostartOptionPage << new LauncherOptionPage +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD + << new SystemdOptionPage +#endif + ); category->setIcon(QIcon::fromTheme(QStringLiteral("system-run"), QIcon(QStringLiteral(":/icons/hicolor/scalable/apps/system-run.svg")))); categories << category; @@ -647,6 +753,9 @@ INSTANTIATE_UI_FILE_BASED_OPTION_PAGE_NS(QtGui, NotificationsOptionPage) INSTANTIATE_UI_FILE_BASED_OPTION_PAGE_NS(QtGui, AppearanceOptionPage) INSTANTIATE_UI_FILE_BASED_OPTION_PAGE_NS(QtGui, AutostartOptionPage) INSTANTIATE_UI_FILE_BASED_OPTION_PAGE_NS(QtGui, LauncherOptionPage) +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD +INSTANTIATE_UI_FILE_BASED_OPTION_PAGE_NS(QtGui, SystemdOptionPage) +#endif #ifndef SYNCTHINGTRAY_NO_WEBVIEW INSTANTIATE_UI_FILE_BASED_OPTION_PAGE_NS(QtGui, WebViewOptionPage) #endif diff --git a/tray/gui/settingsdialog.h b/tray/gui/settingsdialog.h index 916c0c1..f5b8118 100644 --- a/tray/gui/settingsdialog.h +++ b/tray/gui/settingsdialog.h @@ -12,6 +12,7 @@ namespace Data { class SyncthingConnection; +class SyncthingService; } namespace QtGui { @@ -52,6 +53,17 @@ private: bool m_kill; END_DECLARE_OPTION_PAGE +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD +BEGIN_DECLARE_UI_FILE_BASED_OPTION_PAGE(SystemdOptionPage) +private: + DECLARE_SETUP_WIDGETS + void handleDescriptionChanged(const QString &description); + void handleStatusChanged(const QString &activeState, const QString &subState); + void handleEnabledChanged(const QString &unitFileState); + Data::SyncthingService &m_service; +END_DECLARE_OPTION_PAGE +#endif + #ifndef SYNCTHINGTRAY_NO_WEBVIEW DECLARE_UI_FILE_BASED_OPTION_PAGE(WebViewOptionPage) #else @@ -73,6 +85,9 @@ DECLARE_EXTERN_UI_FILE_BASED_OPTION_PAGE_NS(QtGui, NotificationsOptionPage) DECLARE_EXTERN_UI_FILE_BASED_OPTION_PAGE_NS(QtGui, AppearanceOptionPage) DECLARE_EXTERN_UI_FILE_BASED_OPTION_PAGE_NS(QtGui, AutostartOptionPage) DECLARE_EXTERN_UI_FILE_BASED_OPTION_PAGE_NS(QtGui, LauncherOptionPage) +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD +DECLARE_EXTERN_UI_FILE_BASED_OPTION_PAGE_NS(QtGui, SystemdOptionPage) +#endif #ifndef SYNCTHINGTRAY_NO_WEBVIEW DECLARE_EXTERN_UI_FILE_BASED_OPTION_PAGE_NS(QtGui, WebViewOptionPage) #endif diff --git a/tray/gui/systemdoptionpage.ui b/tray/gui/systemdoptionpage.ui new file mode 100644 index 0000000..6ac091e --- /dev/null +++ b/tray/gui/systemdoptionpage.ui @@ -0,0 +1,278 @@ + + + QtGui::SystemdOptionPage + + + Systemd + + + + + + + 75 + true + + + + Show start/stop button on tray for local instance when specified unit available + + + + + + + + 30 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Syncthing unit + + + + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + Description + + + + + + + + + + 0 + 0 + + + + + 0 + 32 + + + + unknown + + + + + + + + + + 0 + 0 + + + + Current status + + + + + + + + + + 0 + 0 + + + + + 16 + 16 + + + + true + + + + + + + + 0 + 0 + + + + + 0 + 32 + + + + unknown + + + + + + + Start + + + + .. + + + + + + + Stop + + + + .. + + + + + + + + + + 0 + 0 + + + + Unit file state + + + + + + + + + + 0 + 0 + + + + + 16 + 16 + + + + true + + + + + + + + 0 + 0 + + + + + 0 + 32 + + + + unknown + + + + + + + Enable + + + + .. + + + + + + + Disable + + + + .. + + + + + + + + + + + + + Widgets::ClearLineEdit + QLineEdit +
qtutilities/widgets/clearlineedit.h
+
+
+ + showButtonCheckBox + syncthingUnitLineEdit + startPushButton + stopPushButton + enablePushButton + disablePushButton + + + +
diff --git a/tray/gui/traywidget.cpp b/tray/gui/traywidget.cpp index 1e2ef3f..10050cb 100644 --- a/tray/gui/traywidget.cpp +++ b/tray/gui/traywidget.cpp @@ -7,6 +7,11 @@ #include "../application/settings.h" +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD +# include "../../connector/syncthingservice.h" +# include "../../connector/utils.h" +#endif + #include "resources/config.h" #include "ui_traywidget.h" @@ -129,6 +134,11 @@ TrayWidget::TrayWidget(TrayMenu *parent) : connect(m_ui->notificationsPushButton, &QPushButton::clicked, this, &TrayWidget::showNotifications); connect(restartButton, &QPushButton::clicked, this, &TrayWidget::restartSyncthing); connect(m_connectionsActionGroup, &QActionGroup::triggered, this, &TrayWidget::handleConnectionSelected); +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD + const SyncthingService &service = syncthingService(); + connect(m_ui->startStopPushButton, &QPushButton::clicked, &service, &SyncthingService::toggleRunning); + connect(&service, &SyncthingService::stateChanged, this, &TrayWidget::updateStartStopButton); +#endif } TrayWidget::~TrayWidget() @@ -354,6 +364,11 @@ void TrayWidget::applySettings() } #endif + // systemd +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD + instance->updateStartStopButton(); +#endif + // update visual appearance instance->m_ui->trafficFormWidget->setVisible(settings.appearance.showTraffic); instance->m_ui->trafficIconLabel->setVisible(settings.appearance.showTraffic); @@ -458,6 +473,29 @@ void TrayWidget::updateTraffic() } +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD +void TrayWidget::updateStartStopButton() +{ + const SyncthingService &service = syncthingService(); + const Settings::Systemd &settings = Settings::values().systemd; + + if(settings.showButton && service.isUnitAvailable() && m_selectedConnection && isLocal(QUrl(m_selectedConnection->syncthingUrl))) { + m_ui->startStopPushButton->setVisible(true); + if(service.isRunning()) { + m_ui->startStopPushButton->setText(tr("Stop")); + m_ui->startStopPushButton->setToolTip(QStringLiteral("systemctl --user stop ") + service.unitName()); + m_ui->startStopPushButton->setIcon(QIcon::fromTheme(QStringLiteral("process-stop"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/process-stop.svg")))); + } else { + m_ui->startStopPushButton->setText(tr("Start")); + m_ui->startStopPushButton->setToolTip(QStringLiteral("systemctl --user start ") + service.unitName()); + m_ui->startStopPushButton->setIcon(QIcon::fromTheme(QStringLiteral("system-run"), QIcon(QStringLiteral(":/icons/hicolor/scalable/apps/system-run.svg")))); + } + } else { + m_ui->startStopPushButton->setVisible(false); + } +} +#endif + #ifndef SYNCTHINGTRAY_NO_WEBVIEW void TrayWidget::handleWebViewDeleted() { @@ -480,6 +518,9 @@ void TrayWidget::handleConnectionSelected(QAction *connectionAction) : &Settings::values().connection.secondary[static_cast(index - 1)]; m_ui->connectionsPushButton->setText(m_selectedConnection->label); m_connection.reconnect(*m_selectedConnection); +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD + updateStartStopButton(); +#endif #ifndef SYNCTHINGTRAY_NO_WEBVIEW if(m_webViewDlg) { m_webViewDlg->applySettings(*m_selectedConnection); diff --git a/tray/gui/traywidget.h b/tray/gui/traywidget.h index 1f4e0a8..521776c 100644 --- a/tray/gui/traywidget.h +++ b/tray/gui/traywidget.h @@ -70,6 +70,9 @@ private slots: void pauseResumeDev(const Data::SyncthingDev &dev); void changeStatus(); void updateTraffic(); +#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD + void updateStartStopButton(); +#endif #ifndef SYNCTHINGTRAY_NO_WEBVIEW void handleWebViewDeleted(); #endif diff --git a/tray/gui/traywidget.ui b/tray/gui/traywidget.ui index c57bc38..0409513 100644 --- a/tray/gui/traywidget.ui +++ b/tray/gui/traywidget.ui @@ -2,6 +2,14 @@ QtGui::TrayWidget + + + 0 + 0 + 300 + 314 + + Syncthing Tray @@ -107,6 +115,20 @@ + + + + Start + + + + :/icons/hicolor/scalable/apps/system-run.svg:/icons/hicolor/scalable/apps/system-run.svg + + + true + + +