diff --git a/CMakeLists.txt b/CMakeLists.txt index a06c713..7b8543d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,7 +28,9 @@ set(SRC_FILES set(WIDGETS_HEADER_FILES application/settings.h - gui/tray.h + gui/trayicon.h + gui/traywidget.h + gui/traymenu.h gui/settingsdialog.h gui/webpage.h gui/webviewdialog.h @@ -41,7 +43,9 @@ set(WIDGETS_HEADER_FILES set(WIDGETS_SRC_FILES application/main.cpp application/settings.cpp - gui/tray.cpp + gui/trayicon.cpp + gui/traywidget.cpp + gui/traymenu.cpp gui/settingsdialog.cpp gui/webpage.cpp gui/webviewdialog.cpp @@ -56,7 +60,7 @@ set(WIDGETS_UI_FILES gui/connectionoptionpage.ui gui/notificationsoptionpage.ui gui/appearanceoptionpage.ui - gui/launcheroptionpage.ui + gui/autostartoptionpage.ui gui/webviewoptionpage.ui ) diff --git a/application/main.cpp b/application/main.cpp index 0764257..849806f 100644 --- a/application/main.cpp +++ b/application/main.cpp @@ -1,5 +1,6 @@ #include "./settings.h" -#include "../gui/tray.h" +#include "../gui/trayicon.h" +#include "../gui/traywidget.h" #include "resources/config.h" @@ -9,6 +10,7 @@ #include #include +#include #include #include @@ -28,6 +30,9 @@ int main(int argc, char *argv[]) HelpArgument helpArg(parser); // Qt configuration arguments QT_CONFIG_ARGUMENTS qtConfigArgs; + Argument windowedArg("windowed", 'w', "shows the UI in a regular window"); + windowedArg.setCombinable(true); + qtConfigArgs.qtWidgetsGuiArg().addSubArgument(&windowedArg); parser.setMainArguments({&qtConfigArgs.qtWidgetsGuiArg(), &helpArg}); try { parser.parseArgs(argc, argv); @@ -42,10 +47,16 @@ int main(int argc, char *argv[]) int res; #ifndef QT_NO_SYSTEMTRAYICON if(QSystemTrayIcon::isSystemTrayAvailable()) { - application.setQuitOnLastWindowClosed(false); - TrayIcon trayIcon; - trayIcon.show(); - res = application.exec(); + if(windowedArg.isPresent()) { + TrayWidget trayWidget; + trayWidget.show(); + res = application.exec(); + } else { + application.setQuitOnLastWindowClosed(false); + TrayIcon trayIcon; + trayIcon.show(); + res = application.exec(); + } } else { QMessageBox::critical(nullptr, QApplication::applicationName(), QApplication::translate("main", "The system tray is (currently) not available.")); res = -1; diff --git a/data/syncthingconnection.cpp b/data/syncthingconnection.cpp index 7afb108..c303236 100644 --- a/data/syncthingconnection.cpp +++ b/data/syncthingconnection.cpp @@ -1,5 +1,7 @@ #include "./syncthingconnection.h" +#include + #include #include #include @@ -10,10 +12,13 @@ #include #include #include +#include #include using namespace std; +using namespace ChronoUtilities; +using namespace ConversionUtilities; namespace Data { @@ -75,6 +80,8 @@ SyncthingConnection::SyncthingConnection(const QString &syncthingUrl, const QByt m_lastEventId(0), m_totalIncomingTraffic(0), m_totalOutgoingTraffic(0), + m_totalIncomingRate(0), + m_totalOutgoingRate(0), m_configReply(nullptr), m_statusReply(nullptr), m_eventsReply(nullptr), @@ -542,9 +549,20 @@ void SyncthingConnection::readConnections() if(jsonError.error == QJsonParseError::NoError) { const QJsonObject replyObj(replyDoc.object()); const QJsonObject totalObj(replyObj.value(QStringLiteral("total")).toObject()); - m_totalIncomingTraffic = totalObj.value(QStringLiteral("inBytesTotal")).toInt(0); - m_totalOutgoingTraffic = totalObj.value(QStringLiteral("outBytesTotal")).toInt(0); - emit trafficChanged(m_totalIncomingTraffic, m_totalOutgoingTraffic); + + // read traffic + const int totalIncomingTraffic = totalObj.value(QStringLiteral("inBytesTotal")).toInt(0); + const int totalOutgoingTraffic = totalObj.value(QStringLiteral("outBytesTotal")).toInt(0); + double transferTime; + if(!m_lastConnectionsUpdate.isNull() && ((transferTime = (DateTime::gmtNow() - m_lastConnectionsUpdate).totalSeconds()) != 0.0)) { + m_totalIncomingRate = (totalIncomingTraffic - m_totalIncomingTraffic) * 0.008 / transferTime, + m_totalOutgoingRate = (totalOutgoingTraffic - m_totalOutgoingTraffic) * 0.008 / transferTime; + } else { + m_totalIncomingRate = m_totalOutgoingRate = 0.0; + } + emit trafficChanged(m_totalIncomingTraffic = totalIncomingTraffic, m_totalOutgoingTraffic = totalOutgoingTraffic); + + // read connection status const QJsonObject connectionsObj(replyObj.value(QStringLiteral("connections")).toObject()); int index = 0; for(SyncthingDev &dev : m_devs) { @@ -576,7 +594,13 @@ void SyncthingConnection::readConnections() } ++index; } - m_lastConnectionsUpdate = QDateTime::currentDateTime(); + + m_lastConnectionsUpdate = DateTime::gmtNow(); + + // since there seems no event for this data, just request every 2 seconds, FIXME: make interval configurable + if(m_keepPolling) { + QTimer::singleShot(2000, Qt::VeryCoarseTimer, this, SLOT(requestConnections())); + } } else { emit error(tr("Unable to parse connections: ") + jsonError.errorString()); } @@ -609,7 +633,12 @@ void SyncthingConnection::readEvents() for(const QJsonValue &eventVal : replyArray) { const QJsonObject event = eventVal.toObject(); m_lastEventId = event.value(QStringLiteral("id")).toInt(m_lastEventId); - const QDateTime eventTime(QDateTime::fromString(event.value(QStringLiteral("time")).toString(), Qt::ISODate)); + DateTime eventTime; + try { + eventTime = DateTime::fromIsoString(event.value(QStringLiteral("time")).toString().toLocal8Bit().data()).first; + } catch(const ConversionException &) { + // ignore conversion error + } const QString eventType(event.value(QStringLiteral("type")).toString()); const QJsonObject eventData(event.value(QStringLiteral("data")).toObject()); if(eventType == QLatin1String("Starting")) { @@ -731,7 +760,7 @@ void SyncthingConnection::readDirEvent(const QString &eventType, const QJsonObje dirInfo->localFiles = summary.value(QStringLiteral("localFiles")).toInt(); dirInfo->neededByted = summary.value(QStringLiteral("needByted")).toInt(); dirInfo->neededFiles = summary.value(QStringLiteral("needFiles")).toInt(); - //dirInfo->assignStatus(summary.value(QStringLiteral("state")).toString()); + // FIXME: dirInfo->assignStatus(summary.value(QStringLiteral("state")).toString()); emit dirStatusChanged(*dirInfo, index); } } else if(eventType == QLatin1String("FolderCompletion")) { @@ -743,6 +772,16 @@ void SyncthingConnection::readDirEvent(const QString &eventType, const QJsonObje // just show the smallest percentage for now dirInfo->progressPercentage = percentage; } + } else if(eventType == QLatin1String("FolderScanProgress")) { + // FIXME: for some reason current is always 0 + int current = eventData.value(QStringLiteral("current")).toInt(0); + int total = eventData.value(QStringLiteral("total")).toInt(0); + int rate = eventData.value(QStringLiteral("rate")).toInt(0); + if(current > 0 && total > 0) { + dirInfo->progressPercentage = current * 100 / total; + dirInfo->progressRate = rate; + dirInfo->status = DirStatus::Scanning; // ensure state is scanning + } } } } @@ -751,9 +790,9 @@ void SyncthingConnection::readDirEvent(const QString &eventType, const QJsonObje /*! * \brief Reads results of requestEvents(). */ -void SyncthingConnection::readDeviceEvent(const QDateTime &eventTime, const QString &eventType, const QJsonObject &eventData) +void SyncthingConnection::readDeviceEvent(DateTime eventTime, const QString &eventType, const QJsonObject &eventData) { - if(eventTime.isValid() && m_lastConnectionsUpdate.isValid() && eventTime < m_lastConnectionsUpdate) { + if(eventTime.isNull() && m_lastConnectionsUpdate.isNull() && eventTime < m_lastConnectionsUpdate) { return; // ignore device events happened before the last connections update } const QString dev(eventData.value(QStringLiteral("device")).toString()); @@ -773,6 +812,7 @@ void SyncthingConnection::readDeviceEvent(const QDateTime &eventTime, const QStr status = DevStatus::Rejected; } else if(eventType == QLatin1String("DeviceResumed")) { paused = false; + status = DevStatus::Disconnected; // FIXME: correct to assume device which has just been resumed is still disconnected? } else if(eventType == QLatin1String("DeviceDiscovered")) { // we know about this device already, set status anyways because it might still be unknown status = DevStatus::Disconnected; diff --git a/data/syncthingconnection.h b/data/syncthingconnection.h index ede7519..b2a99f1 100644 --- a/data/syncthingconnection.h +++ b/data/syncthingconnection.h @@ -1,8 +1,9 @@ #ifndef SYNCTHINGCONNECTION_H #define SYNCTHINGCONNECTION_H +#include + #include -#include #include #include @@ -60,6 +61,7 @@ struct SyncthingDir int minDiskFreePercentage = 0; DirStatus status = DirStatus::Unknown; int progressPercentage = 0; + int progressRate = 0; std::vector errors; int globalBytes = 0, globalDeleted = 0, globalFiles = 0; int localBytes = 0, localDeleted = 0, localFiles = 0; @@ -76,7 +78,7 @@ enum class DevStatus Idle, Synchronizing, OutOfSync, - Rejected, + Rejected }; struct SyncthingDev @@ -88,6 +90,7 @@ struct SyncthingDev QString certName; DevStatus status; int progressPercentage = 0; + int progressRate = 0; bool introducer = false; bool paused = false; int totalIncomingTraffic = 0; @@ -132,6 +135,8 @@ public: const QString &myId() const; int totalIncomingTraffic() const; int totalOutgoingTraffic() const; + double totalIncomingRate() const; + double totalOutgoingRate() const; const std::vector &dirInfo() const; const std::vector &devInfo() const; void requestQrCode(const QString &text, std::function callback); @@ -234,7 +239,7 @@ private Q_SLOTS: void readStatusChangedEvent(const QJsonObject &eventData); void readDownloadProgressEvent(const QJsonObject &eventData); void readDirEvent(const QString &eventType, const QJsonObject &eventData); - void readDeviceEvent(const QDateTime &eventTime, const QString &eventType, const QJsonObject &eventData); + void readDeviceEvent(ChronoUtilities::DateTime eventTime, const QString &eventType, const QJsonObject &eventData); void readRescan(); void readPauseResume(); @@ -260,6 +265,8 @@ private: QString m_myId; int m_totalIncomingTraffic; int m_totalOutgoingTraffic; + double m_totalIncomingRate; + double m_totalOutgoingRate; QNetworkReply *m_configReply; QNetworkReply *m_statusReply; QNetworkReply *m_connectionsReply; @@ -269,7 +276,7 @@ private: bool m_hasStatus; std::vector m_dirs; std::vector m_devs; - QDateTime m_lastConnectionsUpdate; + ChronoUtilities::DateTime m_lastConnectionsUpdate; }; /*! @@ -361,7 +368,7 @@ inline const QString &SyncthingConnection::myId() const } /*! - * \brief Returns the total incoming traffic. + * \brief Returns the total incoming traffic in byte. */ inline int SyncthingConnection::totalIncomingTraffic() const { @@ -369,13 +376,29 @@ inline int SyncthingConnection::totalIncomingTraffic() const } /*! - * \brief Returns the total outgoing traffic. + * \brief Returns the total outgoing traffic in byte. */ inline int SyncthingConnection::totalOutgoingTraffic() const { return m_totalOutgoingTraffic; } +/*! + * \brief Returns the total incoming transfer rate in kbit/s. + */ +inline double SyncthingConnection::totalIncomingRate() const +{ + return m_totalIncomingRate; +} + +/*! + * \brief Returns the total outgoing transfer rate in kbit/s. + */ +inline double SyncthingConnection::totalOutgoingRate() const +{ + return m_totalOutgoingRate; +} + /*! * \brief Returns all available directory info. * \remarks The returned object container object is persistent. However, the contained diff --git a/gui/autostartoptionpage.ui b/gui/autostartoptionpage.ui new file mode 100644 index 0000000..ce4eb3f --- /dev/null +++ b/gui/autostartoptionpage.ui @@ -0,0 +1,20 @@ + + + QtGui::AutostartOptionPage + + + Autostart + + + + + + <html><head/><body><p><span style=" font-weight:600;">Not implemented yet</span></p><p>This will allow launching the tray when the desktop environment starts and to launch Syncthing when the tray is started.</p></body></html> + + + + + + + + diff --git a/gui/devview.cpp b/gui/devview.cpp index 5f28ccc..7cc13a5 100644 --- a/gui/devview.cpp +++ b/gui/devview.cpp @@ -41,7 +41,12 @@ void DevView::showContextMenu() { if(selectionModel() && selectionModel()->selectedRows(0).size() == 1) { QMenu menu; - connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-copy.svg"))), tr("Copy")), &QAction::triggered, this, &DevView::copySelectedItem); + if(selectionModel()->selectedRows(0).at(0).parent().isValid()) { + connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-copy.svg"))), tr("Copy value")), &QAction::triggered, this, &DevView::copySelectedItem); + } else { + connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-copy.svg"))), tr("Copy name")), &QAction::triggered, this, &DevView::copySelectedItem); + connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-copy.svg"))), tr("Copy ID")), &QAction::triggered, this, &DevView::copySelectedItemId); + } menu.exec(QCursor::pos()); } } @@ -64,4 +69,21 @@ void DevView::copySelectedItem() } } +void DevView::copySelectedItemId() +{ + if(selectionModel() && selectionModel()->selectedRows(0).size() == 1) { + const QModelIndex selectedIndex = selectionModel()->selectedRows(0).at(0); + QString text; + if(selectedIndex.parent().isValid()) { + // dev attribute: should be handled by copySelectedItemId() + } else { + // dev name/id + text = model()->data(model()->index(0, 1, selectedIndex)).toString(); + } + if(!text.isEmpty()) { + QGuiApplication::clipboard()->setText(text); + } + } +} + } diff --git a/gui/devview.h b/gui/devview.h index e5f98d3..21b34f2 100644 --- a/gui/devview.h +++ b/gui/devview.h @@ -20,6 +20,7 @@ protected: private Q_SLOTS: void showContextMenu(); void copySelectedItem(); + void copySelectedItemId(); }; diff --git a/gui/dirview.cpp b/gui/dirview.cpp index eaa5a15..f536bc5 100644 --- a/gui/dirview.cpp +++ b/gui/dirview.cpp @@ -41,7 +41,12 @@ void DirView::showContextMenu() { if(selectionModel() && selectionModel()->selectedRows(0).size() == 1) { QMenu menu; - connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-copy.svg"))), tr("Copy")), &QAction::triggered, this, &DirView::copySelectedItem); + if(selectionModel()->selectedRows(0).at(0).parent().isValid()) { + connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-copy.svg"))), tr("Copy value")), &QAction::triggered, this, &DirView::copySelectedItem); + } else { + connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-copy.svg"))), tr("Copy label/ID")), &QAction::triggered, this, &DirView::copySelectedItem); + connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-copy.svg"))), tr("Copy path")), &QAction::triggered, this, &DirView::copySelectedItemPath); + } menu.exec(QCursor::pos()); } } @@ -64,4 +69,21 @@ void DirView::copySelectedItem() } } +void DirView::copySelectedItemPath() +{ + if(selectionModel() && selectionModel()->selectedRows(0).size() == 1) { + const QModelIndex selectedIndex = selectionModel()->selectedRows(0).at(0); + QString text; + if(selectedIndex.parent().isValid()) { + // dev attribute: should be handled by copySelectedItem() only + } else { + // dev path + text = model()->data(model()->index(1, 1, selectedIndex)).toString(); + } + if(!text.isEmpty()) { + QGuiApplication::clipboard()->setText(text); + } + } +} + } diff --git a/gui/dirview.h b/gui/dirview.h index af39465..3093791 100644 --- a/gui/dirview.h +++ b/gui/dirview.h @@ -21,6 +21,7 @@ protected: private Q_SLOTS: void showContextMenu(); void copySelectedItem(); + void copySelectedItemPath(); }; diff --git a/gui/launcheroptionpage.ui b/gui/launcheroptionpage.ui deleted file mode 100644 index d6c6741..0000000 --- a/gui/launcheroptionpage.ui +++ /dev/null @@ -1,28 +0,0 @@ - - - QtGui::LauncherOptionPage - - - - 0 - 0 - 229 - 164 - - - - Launcher - - - - - - <html><head/><body><p><span style=" font-weight:600;">Not implemented yet.</span></p><p>This will allow launching Syncthing when the tray is started.</p></body></html> - - - - - - - - diff --git a/gui/settingsdialog.cpp b/gui/settingsdialog.cpp index 3333da9..cfe9daa 100644 --- a/gui/settingsdialog.cpp +++ b/gui/settingsdialog.cpp @@ -6,7 +6,7 @@ #include "ui_connectionoptionpage.h" #include "ui_notificationsoptionpage.h" #include "ui_appearanceoptionpage.h" -#include "ui_launcheroptionpage.h" +#include "ui_autostartoptionpage.h" #include "ui_webviewoptionpage.h" #include @@ -138,21 +138,21 @@ void AppearanceOptionPage::reset() } // LauncherOptionPage -LauncherOptionPage::LauncherOptionPage(QWidget *parentWidget) : - LauncherOptionPageBase(parentWidget) +AutostartOptionPage::AutostartOptionPage(QWidget *parentWidget) : + AutostartOptionPageBase(parentWidget) {} -LauncherOptionPage::~LauncherOptionPage() +AutostartOptionPage::~AutostartOptionPage() {} -bool LauncherOptionPage::apply() +bool AutostartOptionPage::apply() { if(hasBeenShown()) { } return true; } -void LauncherOptionPage::reset() +void AutostartOptionPage::reset() { if(hasBeenShown()) { } @@ -211,7 +211,7 @@ SettingsDialog::SettingsDialog(Data::SyncthingConnection *connection, QWidget *p category->setDisplayName(tr("Tray")); category->assignPages(QList() << new ConnectionOptionPage(connection) << new NotificationsOptionPage - << new AppearanceOptionPage << new LauncherOptionPage); + << new AppearanceOptionPage << new AutostartOptionPage); category->setIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/app/syncthingtray.svg"))); categories << category; diff --git a/gui/settingsdialog.h b/gui/settingsdialog.h index e4c2dad..0f4fb07 100644 --- a/gui/settingsdialog.h +++ b/gui/settingsdialog.h @@ -32,7 +32,7 @@ DECLARE_UI_FILE_BASED_OPTION_PAGE(NotificationsOptionPage) DECLARE_UI_FILE_BASED_OPTION_PAGE(AppearanceOptionPage) -DECLARE_UI_FILE_BASED_OPTION_PAGE(LauncherOptionPage) +DECLARE_UI_FILE_BASED_OPTION_PAGE(AutostartOptionPage) #if defined(SYNCTHINGTRAY_USE_WEBENGINE) || defined(SYNCTHINGTRAY_USE_WEBKIT) DECLARE_UI_FILE_BASED_OPTION_PAGE(WebViewOptionPage) diff --git a/gui/trayicon.cpp b/gui/trayicon.cpp new file mode 100644 index 0000000..e7e26cc --- /dev/null +++ b/gui/trayicon.cpp @@ -0,0 +1,144 @@ +#include "./trayicon.h" +#include "./traywidget.h" + +#include "../application/settings.h" +#include "../data/syncthingconnection.h" + +#include + +#include +#include +#include +#include + +using namespace Dialogs; +using namespace Data; + +namespace QtGui { + +/*! + * \brief Instantiates a new tray icon. + */ +TrayIcon::TrayIcon(QObject *parent) : + QSystemTrayIcon(parent), + m_size(QSize(128, 128)), + m_statusIconDisconnected(QIcon(renderSvgImage(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-disconnected.svg")))), + m_statusIconDefault(QIcon(renderSvgImage(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-default.svg")))), + m_statusIconNotify(QIcon(renderSvgImage(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-notify.svg")))), + m_statusIconPause(QIcon(renderSvgImage(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-pause.svg")))), + m_statusIconSync(QIcon(renderSvgImage(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-sync.svg")))), + m_status(SyncthingStatus::Disconnected) +{ + // set context menu + connect(m_contextMenu.addAction(QIcon::fromTheme(QStringLiteral("internet-web-browser"), QIcon(QStringLiteral(":/icons/hicolor/scalable/apps/internet-web-browser.svg"))), tr("Web UI")), &QAction::triggered, m_trayMenu.widget(), &TrayWidget::showWebUi); + connect(m_contextMenu.addAction(QIcon::fromTheme(QStringLiteral("preferences-other"), QIcon(QStringLiteral(":/icons/hicolor/scalable/apps/preferences-other.svg"))), tr("Settings")), &QAction::triggered, m_trayMenu.widget(), &TrayWidget::showSettingsDialog); + connect(m_contextMenu.addAction(QIcon::fromTheme(QStringLiteral("help-about"), QIcon(QStringLiteral(":/icons/hicolor/scalable/apps/help-about.svg"))), tr("About")), &QAction::triggered, m_trayMenu.widget(), &TrayWidget::showAboutDialog); + m_contextMenu.addSeparator(); + connect(m_contextMenu.addAction(QIcon::fromTheme(QStringLiteral("window-close"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/window-close.svg"))), tr("Close")), &QAction::triggered, &QCoreApplication::quit); + setContextMenu(&m_contextMenu); + + // set initial status + updateStatusIconAndText(SyncthingStatus::Disconnected); + + // connect signals and slots + SyncthingConnection *connection = &(m_trayMenu.widget()->connection()); + connect(this, &TrayIcon::activated, this, &TrayIcon::handleActivated); + connect(connection, &SyncthingConnection::error, this, &TrayIcon::showSyncthingError); + connect(connection, &SyncthingConnection::newNotification, this, &TrayIcon::showSyncthingNotification); + connect(connection, &SyncthingConnection::statusChanged, this, &TrayIcon::updateStatusIconAndText); +} + +void TrayIcon::handleActivated(QSystemTrayIcon::ActivationReason reason) +{ + switch(reason) { + case QSystemTrayIcon::Context: + // can't catch that event on Plasma 5 anyways + break; + case QSystemTrayIcon::Trigger: + // either show web UI or context menu + if(false) { + m_trayMenu.widget()->showWebUi(); + } else { + m_trayMenu.resize(m_trayMenu.sizeHint()); + // when showing the menu manually + // move the menu to the closest of the currently available screen + // this implies that the tray icon is located near the edge of the screen; otherwise this behavior makes no sense + cornerWidget(&m_trayMenu); + m_trayMenu.show(); + } + break; + default: + ; + } +} + +void TrayIcon::showSyncthingError(const QString &errorMsg) +{ + if(Settings::notifyOnErrors()) { + showMessage(tr("Syncthing error"), errorMsg, QSystemTrayIcon::Critical); + } +} + +void TrayIcon::showSyncthingNotification(const QString &message) +{ + if(Settings::showSyncthingNotifications()) { + showMessage(tr("Syncthing notification"), message, QSystemTrayIcon::Information); + } +} + +void TrayIcon::updateStatusIconAndText(SyncthingStatus status) +{ + switch(status) { + case SyncthingStatus::Disconnected: + setIcon(m_statusIconDisconnected); + setToolTip(tr("Not connected to Syncthing")); + if(Settings::notifyOnDisconnect()) { + showMessage(QCoreApplication::applicationName(), tr("Disconnected from Syncthing"), QSystemTrayIcon::Warning); + } + break; + case SyncthingStatus::Default: + setIcon(m_statusIconDefault); + setToolTip(tr("Syncthing is running")); + break; + case SyncthingStatus::NotificationsAvailable: + setIcon(m_statusIconNotify); + setToolTip(tr("Notifications available")); + break; + case SyncthingStatus::Paused: + setIcon(m_statusIconPause); + setToolTip(tr("At least one device is paused")); + break; + case SyncthingStatus::Synchronizing: + setIcon(m_statusIconSync); + setToolTip(tr("Synchronization is ongoing")); + break; + } + switch(status) { + case SyncthingStatus::Disconnected: + case SyncthingStatus::Synchronizing: + break; + default: + if(m_status == SyncthingStatus::Synchronizing && Settings::notifyOnSyncComplete()) { + showMessage(QCoreApplication::applicationName(), tr("Synchronization complete"), QSystemTrayIcon::Information); + } + } + + m_status = status; +} + +/*! + * \brief Renders an SVG image to a QPixmap. + * \remarks If instantiating QIcon directly from SVG image the icon is not displayed under Plasma 5. It would work + * with Tint2, tough. + */ +QPixmap TrayIcon::renderSvgImage(const QString &path) +{ + QSvgRenderer renderer(path); + QPixmap pm(m_size); + pm.fill(QColor(Qt::transparent)); + QPainter painter(&pm); + renderer.render(&painter, pm.rect()); + return pm; +} + +} diff --git a/gui/trayicon.h b/gui/trayicon.h new file mode 100644 index 0000000..24ebea6 --- /dev/null +++ b/gui/trayicon.h @@ -0,0 +1,48 @@ +#ifndef TRAY_ICON_H +#define TRAY_ICON_H + +#include "./traymenu.h" + +#include + +#include +#include + +QT_FORWARD_DECLARE_CLASS(QPixmap) + +namespace Data { +enum class SyncthingStatus; +} + +namespace QtGui { + +class TrayIcon : public QSystemTrayIcon +{ + Q_OBJECT + +public: + TrayIcon(QObject *parent = nullptr); + +private slots: + void handleActivated(QSystemTrayIcon::ActivationReason reason); + void showSyncthingError(const QString &errorMsg); + void showSyncthingNotification(const QString &message); + void updateStatusIconAndText(Data::SyncthingStatus status); + +private: + QPixmap renderSvgImage(const QString &path); + + const QSize m_size; + const QIcon m_statusIconDisconnected; + const QIcon m_statusIconDefault; + const QIcon m_statusIconNotify; + const QIcon m_statusIconPause; + const QIcon m_statusIconSync; + TrayMenu m_trayMenu; + QMenu m_contextMenu; + Data::SyncthingStatus m_status; +}; + +} + +#endif // TRAY_ICON_H diff --git a/gui/traymenu.cpp b/gui/traymenu.cpp new file mode 100644 index 0000000..cc83719 --- /dev/null +++ b/gui/traymenu.cpp @@ -0,0 +1,23 @@ +#include "./traymenu.h" +#include "./traywidget.h" + +#include + +namespace QtGui { + +TrayMenu::TrayMenu(QWidget *parent) : + QMenu(parent) +{ + auto *menuLayout = new QHBoxLayout; + menuLayout->setMargin(0), menuLayout->setSpacing(0); + menuLayout->addWidget(m_trayWidget = new TrayWidget(this)); + setLayout(menuLayout); + setPlatformMenu(nullptr); +} + +QSize TrayMenu::sizeHint() const +{ + return QSize(350, 300); +} + +} diff --git a/gui/traymenu.h b/gui/traymenu.h new file mode 100644 index 0000000..c1e58f6 --- /dev/null +++ b/gui/traymenu.h @@ -0,0 +1,31 @@ +#ifndef TRAY_MENU_H +#define TRAY_MENU_H + +#include + +namespace QtGui { + +class TrayWidget; + +class TrayMenu : public QMenu +{ + Q_OBJECT + +public: + TrayMenu(QWidget *parent = nullptr); + + QSize sizeHint() const; + TrayWidget *widget(); + +private: + TrayWidget *m_trayWidget; +}; + +inline TrayWidget *TrayMenu::widget() +{ + return m_trayWidget; +} + +} + +#endif // TRAY_MENU_H diff --git a/gui/tray.cpp b/gui/traywidget.cpp similarity index 64% rename from gui/tray.cpp rename to gui/traywidget.cpp index 87a3ec8..77e05a9 100644 --- a/gui/tray.cpp +++ b/gui/traywidget.cpp @@ -1,17 +1,15 @@ -#include "./tray.h" +#include "./traywidget.h" +#include "./traymenu.h" #include "./settingsdialog.h" #include "./webviewdialog.h" -#include "./dirbuttonsitemdelegate.h" #include "../application/settings.h" #include "resources/config.h" - #include "ui_traywidget.h" #include #include -#include #include #include #include @@ -19,17 +17,10 @@ #include -#include -#include -#include -#include -#include +#include #include -#include #include -#include #include -#include #include #include #include @@ -98,7 +89,7 @@ TrayWidget::TrayWidget(TrayMenu *parent) : // connect signals and slots connect(m_ui->statusPushButton, &QPushButton::clicked, this, &TrayWidget::changeStatus); - connect(m_ui->closePushButton, &QPushButton::clicked, &QApplication::quit); + connect(m_ui->closePushButton, &QPushButton::clicked, &QCoreApplication::quit); connect(m_ui->aboutPushButton, &QPushButton::clicked, this, &TrayWidget::showAboutDialog); connect(m_ui->webUiPushButton, &QPushButton::clicked, this, &TrayWidget::showWebUi); connect(m_ui->settingsPushButton, &QPushButton::clicked, this, &TrayWidget::showSettingsDialog); @@ -266,6 +257,9 @@ void TrayWidget::applySettings() } m_connection.reconnect(); m_ui->trafficFrame->setVisible(Settings::showTraffic()); + if(Settings::showTraffic()) { + updateTraffic(); + } } void TrayWidget::openDir(const QModelIndex &dirIndex) @@ -314,10 +308,26 @@ void TrayWidget::changeStatus() } } -void TrayWidget::updateTraffic(int totalIncomingTraffic, int totalOutgoingTraffic) +void TrayWidget::updateTraffic() { - m_ui->inTrafficLabel->setText(totalIncomingTraffic >= 0 ? QString::fromUtf8(dataSizeToString(totalIncomingTraffic).data()) : tr("unknown")); - m_ui->outTrafficLabel->setText(totalOutgoingTraffic >= 0 ? QString::fromUtf8(dataSizeToString(totalOutgoingTraffic).data()) : tr("unknown")); + if(m_ui->trafficFrame->isHidden()) { + return; + } + if(m_connection.totalIncomingRate() != 0.0) { + m_ui->inTrafficLabel->setText(m_connection.totalIncomingTraffic() >= 0 + ? QStringLiteral("%1 (%2)").arg(QString::fromUtf8(bitrateToString(m_connection.totalIncomingRate(), true).data()), QString::fromUtf8(dataSizeToString(m_connection.totalIncomingTraffic()).data())) + : QString::fromUtf8(bitrateToString(m_connection.totalIncomingRate(), true).data())); + } else { + m_ui->inTrafficLabel->setText(m_connection.totalIncomingTraffic() >= 0 ? QString::fromUtf8(dataSizeToString(m_connection.totalIncomingTraffic()).data()) : tr("unknown")); + } + if(m_connection.totalOutgoingRate() != 0.0) { + m_ui->outTrafficLabel->setText(m_connection.totalIncomingTraffic() >= 0 + ? QStringLiteral("%1 (%2)").arg(QString::fromUtf8(bitrateToString(m_connection.totalOutgoingRate(), true).data()), QString::fromUtf8(dataSizeToString(m_connection.totalOutgoingTraffic()).data())) + : QString::fromUtf8(bitrateToString(m_connection.totalOutgoingRate(), true).data())); + } else { + m_ui->outTrafficLabel->setText(m_connection.totalOutgoingTraffic() >= 0 ? QString::fromUtf8(dataSizeToString(m_connection.totalOutgoingTraffic()).data()) : tr("unknown")); + } + } void TrayWidget::handleWebViewDeleted() @@ -325,144 +335,4 @@ void TrayWidget::handleWebViewDeleted() m_webViewDlg = nullptr; } -TrayMenu::TrayMenu(QWidget *parent) : - QMenu(parent) -{ - auto *menuLayout = new QHBoxLayout; - menuLayout->setMargin(0), menuLayout->setSpacing(0); - menuLayout->addWidget(m_trayWidget = new TrayWidget(this)); - setLayout(menuLayout); - setPlatformMenu(nullptr); -} - -QSize TrayMenu::sizeHint() const -{ - return QSize(350, 300); -} - -/*! - * \brief Instantiates a new tray icon. - */ -TrayIcon::TrayIcon(QObject *parent) : - QSystemTrayIcon(parent), - m_size(QSize(128, 128)), - m_statusIconDisconnected(QIcon(renderSvgImage(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-disconnected.svg")))), - m_statusIconDefault(QIcon(renderSvgImage(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-default.svg")))), - m_statusIconNotify(QIcon(renderSvgImage(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-notify.svg")))), - m_statusIconPause(QIcon(renderSvgImage(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-pause.svg")))), - m_statusIconSync(QIcon(renderSvgImage(QStringLiteral(":/icons/hicolor/scalable/status/syncthing-sync.svg")))), - m_status(SyncthingStatus::Disconnected) -{ - // set context menu - connect(m_contextMenu.addAction(QIcon::fromTheme(QStringLiteral("internet-web-browser"), QIcon(QStringLiteral(":/icons/hicolor/scalable/apps/internet-web-browser.svg"))), tr("Web UI")), &QAction::triggered, m_trayMenu.widget(), &TrayWidget::showWebUi); - connect(m_contextMenu.addAction(QIcon::fromTheme(QStringLiteral("preferences-other"), QIcon(QStringLiteral(":/icons/hicolor/scalable/apps/preferences-other.svg"))), tr("Settings")), &QAction::triggered, m_trayMenu.widget(), &TrayWidget::showSettingsDialog); - connect(m_contextMenu.addAction(QIcon::fromTheme(QStringLiteral("help-about"), QIcon(QStringLiteral(":/icons/hicolor/scalable/apps/help-about.svg"))), tr("About")), &QAction::triggered, m_trayMenu.widget(), &TrayWidget::showAboutDialog); - m_contextMenu.addSeparator(); - connect(m_contextMenu.addAction(QIcon::fromTheme(QStringLiteral("window-close"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/window-close.svg"))), tr("Close")), &QAction::triggered, &QCoreApplication::quit); - setContextMenu(&m_contextMenu); - - // set initial status - updateStatusIconAndText(SyncthingStatus::Disconnected); - - // connect signals and slots - SyncthingConnection *connection = &(m_trayMenu.widget()->connection()); - connect(this, &TrayIcon::activated, this, &TrayIcon::handleActivated); - connect(connection, &SyncthingConnection::error, this, &TrayIcon::showSyncthingError); - connect(connection, &SyncthingConnection::newNotification, this, &TrayIcon::showSyncthingNotification); - connect(connection, &SyncthingConnection::statusChanged, this, &TrayIcon::updateStatusIconAndText); -} - -void TrayIcon::handleActivated(QSystemTrayIcon::ActivationReason reason) -{ - switch(reason) { - case QSystemTrayIcon::Context: - // can't catch that event on Plasma 5 anyways - break; - case QSystemTrayIcon::Trigger: - // either show web UI or context menu - if(false) { - m_trayMenu.widget()->showWebUi(); - } else { - m_trayMenu.resize(m_trayMenu.sizeHint()); - // when showing the menu manually - // move the menu to the closest of the currently available screen - // this implies that the tray icon is located near the edge of the screen; otherwise this behavior makes no sense - cornerWidget(&m_trayMenu); - m_trayMenu.show(); - } - break; - default: - ; - } -} - -void TrayIcon::showSyncthingError(const QString &errorMsg) -{ - if(Settings::notifyOnErrors()) { - showMessage(tr("Syncthing error"), errorMsg, QSystemTrayIcon::Critical); - } -} - -void TrayIcon::showSyncthingNotification(const QString &message) -{ - if(Settings::showSyncthingNotifications()) { - showMessage(tr("Syncthing notification"), message, QSystemTrayIcon::Information); - } -} - -void TrayIcon::updateStatusIconAndText(SyncthingStatus status) -{ - switch(status) { - case SyncthingStatus::Disconnected: - setIcon(m_statusIconDisconnected); - setToolTip(tr("Not connected to Syncthing")); - if(Settings::notifyOnDisconnect()) { - showMessage(QCoreApplication::applicationName(), tr("Disconnected from Syncthing"), QSystemTrayIcon::Warning); - } - break; - case SyncthingStatus::Default: - setIcon(m_statusIconDefault); - setToolTip(tr("Syncthing is running")); - break; - case SyncthingStatus::NotificationsAvailable: - setIcon(m_statusIconNotify); - setToolTip(tr("Notifications available")); - break; - case SyncthingStatus::Paused: - setIcon(m_statusIconPause); - setToolTip(tr("At least one device is paused")); - break; - case SyncthingStatus::Synchronizing: - setIcon(m_statusIconSync); - setToolTip(tr("Synchronization is ongoing")); - break; - } - switch(status) { - case SyncthingStatus::Disconnected: - case SyncthingStatus::Synchronizing: - break; - default: - if(m_status == SyncthingStatus::Synchronizing && Settings::notifyOnSyncComplete()) { - showMessage(QCoreApplication::applicationName(), tr("Synchronization complete"), QSystemTrayIcon::Information); - } - } - - m_status = status; -} - -/*! - * \brief Renders an SVG image to a QPixmap. - * \remarks If instantiating QIcon directly from SVG image the icon is not displayed under Plasma 5. It would work - * with Tint2, tough. - */ -QPixmap TrayIcon::renderSvgImage(const QString &path) -{ - QSvgRenderer renderer(path); - QPixmap pm(m_size); - pm.fill(QColor(Qt::transparent)); - QPainter painter(&pm); - renderer.render(&painter, pm.rect()); - return pm; -} - } diff --git a/gui/tray.h b/gui/traywidget.h similarity index 54% rename from gui/tray.h rename to gui/traywidget.h index f50f5f4..8acb5cd 100644 --- a/gui/tray.h +++ b/gui/traywidget.h @@ -1,23 +1,20 @@ -#ifndef TRAY_H -#define TRAY_H +#ifndef TRAY_WIDGET_H +#define TRAY_WIDGET_H #include "./webviewprovider.h" + #include "../data/syncthingconnection.h" #include "../data/syncthingdirectorymodel.h" #include "../data/syncthingdevicemodel.h" #include -#include -#include #include -QT_FORWARD_DECLARE_CLASS(QString) -QT_FORWARD_DECLARE_CLASS(QPixmap) - namespace ApplicationUtilities { class QtConfigArguments; } + namespace Dialogs { class AboutDialog; } @@ -27,6 +24,7 @@ namespace QtGui { class WebViewDialog; class SettingsDialog; class TrayMenu; + namespace Ui { class TrayWidget; } @@ -55,7 +53,7 @@ private slots: void scanDir(const QModelIndex &dirIndex); void pauseResumeDev(const QModelIndex &devIndex); void changeStatus(); - void updateTraffic(int totalIncomingTraffic, int totalOutgoingTraffic); + void updateTraffic(); void handleWebViewDeleted(); private: @@ -76,52 +74,6 @@ inline Data::SyncthingConnection &TrayWidget::connection() return m_connection; } -class TrayMenu : public QMenu -{ - Q_OBJECT - -public: - TrayMenu(QWidget *parent = nullptr); - - QSize sizeHint() const; - TrayWidget *widget(); - -private: - TrayWidget *m_trayWidget; -}; - -inline TrayWidget *TrayMenu::widget() -{ - return m_trayWidget; } -class TrayIcon : public QSystemTrayIcon -{ - Q_OBJECT - -public: - TrayIcon(QObject *parent = nullptr); - -private slots: - void handleActivated(QSystemTrayIcon::ActivationReason reason); - void showSyncthingError(const QString &errorMsg); - void showSyncthingNotification(const QString &message); - void updateStatusIconAndText(Data::SyncthingStatus status); - -private: - QPixmap renderSvgImage(const QString &path); - - const QSize m_size; - const QIcon m_statusIconDisconnected; - const QIcon m_statusIconDefault; - const QIcon m_statusIconNotify; - const QIcon m_statusIconPause; - const QIcon m_statusIconSync; - TrayMenu m_trayMenu; - QMenu m_contextMenu; - Data::SyncthingStatus m_status; -}; - -} - -#endif // TRAY_H +#endif // TRAY_WIDGET_H diff --git a/gui/traywidget.ui b/gui/traywidget.ui index 9f82536..66e8da6 100644 --- a/gui/traywidget.ui +++ b/gui/traywidget.ui @@ -11,7 +11,11 @@ - Form + Syncthing Tray + + + + :/icons/hicolor/scalable/app/syncthingtray.svg:/icons/hicolor/scalable/app/syncthingtray.svg @@ -219,6 +223,9 @@ + + Incoming traffic + unknown @@ -236,6 +243,9 @@ + + Outgoing traffic + unknown @@ -335,6 +345,8 @@
./gui/devview.h
- + + + diff --git a/translations/syncthingtray_de_DE.ts b/translations/syncthingtray_de_DE.ts index 1843efd..995d9ed 100644 --- a/translations/syncthingtray_de_DE.ts +++ b/translations/syncthingtray_de_DE.ts @@ -4,89 +4,89 @@ Data::SyncthingConnection - + disconnected - + connected - + connected, notifications available - + connected, paused - + connected, synchronizing - + unknown - + Unable to parse Syncthing log: - + Unable to request system log: - - + + Unable to parse Syncthing config: - - + + Unable to request Syncthing config: - + Unable to parse connections: - + Unable to request connections: - + Unable to parse Syncthing events: - + Unable to request Syncthing events: - + Unable to request rescan: - + Unable to request pause/resume: - + Unable to request QR-Code: @@ -282,6 +282,19 @@ + + QtGui::AutostartOptionPage + + + Autostart + + + + + <html><head/><body><p><span style=" font-weight:600;">Not implemented yet</span></p><p>This will allow launching the tray when the desktop environment starts and to launch Syncthing when the tray is started.</p></body></html> + + + QtGui::ConnectionOptionPage @@ -333,29 +346,36 @@ QtGui::DevView - - Copy + + Copy value + + + + + Copy name + + + + + Copy ID QtGui::DirView - - Copy - - - - - QtGui::LauncherOptionPage - - - Launcher + + Copy value - - <html><head/><body><p><span style=" font-weight:600;">Not implemented yet.</span></p><p>This will allow launching Syncthing when the tray is started.</p></body></html> + + Copy label/ID + + + + + Copy path @@ -403,67 +423,67 @@ QtGui::TrayIcon - + Web UI - + Settings - + About - + Close - + Syncthing error - + Syncthing notification - + Not connected to Syncthing - + Disconnected from Syncthing - + Syncthing is running - + Notifications available - + At least one device is paused - + Synchronization is ongoing - + Synchronization complete @@ -471,146 +491,156 @@ QtGui::TrayWidget - - Form - - - - + Close - - + + Connect - + Traffic - + In - - - - - unknown + + Incoming traffic + + + + + Outgoing traffic + + + + unknown + + + + + Syncthing Tray + + + + Out - + Directories - + Devices - + About - + Settings - + Web UI - + View own device ID - + Settings - Syncthing tray - + Tray application for Syncthing - + Rescan all directories - + Show Syncthing log - + About - Syncthing Tray - + Own device ID - Syncthing Tray - + device ID is unknown - + Copy to clipboard - + Log - Syncthing - + Not connected to Syncthing, click to connect - + Pause - + Syncthing is running, click to pause all devices - + At least one device is paused, click to resume - + Continue - + The directly <i>%1</i> does not exist on the local machine. @@ -666,12 +696,12 @@ The Web UI will be opened in the default web browser instead. main - + The system tray is (currently) not available. - + The Qt libraries have not been built with tray icon support. diff --git a/translations/syncthingtray_en_US.ts b/translations/syncthingtray_en_US.ts index 959aa61..94e7b1c 100644 --- a/translations/syncthingtray_en_US.ts +++ b/translations/syncthingtray_en_US.ts @@ -4,89 +4,89 @@ Data::SyncthingConnection - + disconnected - + connected - + connected, notifications available - + connected, paused - + connected, synchronizing - + unknown - + Unable to parse Syncthing log: - + Unable to request system log: - - + + Unable to parse Syncthing config: - - + + Unable to request Syncthing config: - + Unable to parse connections: - + Unable to request connections: - + Unable to parse Syncthing events: - + Unable to request Syncthing events: - + Unable to request rescan: - + Unable to request pause/resume: - + Unable to request QR-Code: @@ -282,6 +282,19 @@ + + QtGui::AutostartOptionPage + + + Autostart + + + + + <html><head/><body><p><span style=" font-weight:600;">Not implemented yet</span></p><p>This will allow launching the tray when the desktop environment starts and to launch Syncthing when the tray is started.</p></body></html> + + + QtGui::ConnectionOptionPage @@ -333,29 +346,36 @@ QtGui::DevView - - Copy + + Copy value + + + + + Copy name + + + + + Copy ID QtGui::DirView - - Copy - - - - - QtGui::LauncherOptionPage - - - Launcher + + Copy value - - <html><head/><body><p><span style=" font-weight:600;">Not implemented yet.</span></p><p>This will allow launching Syncthing when the tray is started.</p></body></html> + + Copy label/ID + + + + + Copy path @@ -403,67 +423,67 @@ QtGui::TrayIcon - + Web UI - + Settings - + About - + Close - + Syncthing error - + Syncthing notification - + Not connected to Syncthing - + Disconnected from Syncthing - + Syncthing is running - + Notifications available - + At least one device is paused - + Synchronization is ongoing - + Synchronization complete @@ -471,146 +491,156 @@ QtGui::TrayWidget - - Form - - - - + Close - - + + Connect - + Traffic - + In - - - - - unknown + + Incoming traffic + + + + + Outgoing traffic + + + + unknown + + + + + Syncthing Tray + + + + Out - + Directories - + Devices - + About - + Settings - + Web UI - + View own device ID - + Settings - Syncthing tray - + Tray application for Syncthing - + Rescan all directories - + Show Syncthing log - + About - Syncthing Tray - + Own device ID - Syncthing Tray - + device ID is unknown - + Copy to clipboard - + Log - Syncthing - + Not connected to Syncthing, click to connect - + Pause - + Syncthing is running, click to pause all devices - + At least one device is paused, click to resume - + Continue - + The directly <i>%1</i> does not exist on the local machine. @@ -666,12 +696,12 @@ The Web UI will be opened in the default web browser instead. main - + The system tray is (currently) not available. - + The Qt libraries have not been built with tray icon support.