syncthingtray/tray/gui/traywidget.cpp

998 lines
39 KiB
C++

#include "./traywidget.h"
#include "./helper.h"
#include "./trayicon.h"
#include "./traymenu.h"
#include <syncthingwidgets/misc/internalerrorsdialog.h>
#include <syncthingwidgets/misc/otherdialogs.h>
#include <syncthingwidgets/misc/syncthinglauncher.h>
#include <syncthingwidgets/misc/textviewdialog.h>
#include <syncthingwidgets/settings/settingsdialog.h>
#include <syncthingwidgets/settings/wizard.h>
#include <syncthingwidgets/webview/webviewdialog.h>
#include <syncthingmodel/syncthingicons.h>
#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD
#include <syncthingconnector/syncthingservice.h>
#endif
#include <syncthingconnector/utils.h>
// use meta-data of syncthingtray application here
#include "resources/../../tray/resources/config.h"
#include "resources/../../tray/resources/qtconfig.h"
#include "ui_traywidget.h"
#include <qtforkawesome/icon.h>
#include <qtutilities/aboutdialog/aboutdialog.h>
#include <qtutilities/misc/desktoputils.h>
#include <qtutilities/misc/dialogutils.h>
#include <qtutilities/resources/qtconfigarguments.h>
#include <qtutilities/resources/resources.h>
#include <qtutilities/settingsdialog/qtsettings.h>
#include <c++utilities/application/argumentparser.h>
#include <c++utilities/conversion/stringconversion.h>
#include <QActionGroup>
#include <QClipboard>
#include <QCoreApplication>
#include <QDesktopServices>
#include <QDir>
#include <QFontDatabase>
#include <QGuiApplication>
#include <QMessageBox>
#include <QPalette>
#include <QStringBuilder>
#include <QTextBrowser>
#include <algorithm>
#include <functional>
using namespace CppUtilities;
using namespace QtUtilities;
using namespace Data;
using namespace std;
namespace QtGui {
SettingsDialog *TrayWidget::s_settingsDlg = nullptr;
Wizard *TrayWidget::s_wizard = nullptr;
QtUtilities::AboutDialog *TrayWidget::s_aboutDlg = nullptr;
vector<TrayWidget *> TrayWidget::s_instances;
/*!
* \brief Instantiates a new tray widget.
* \remarks Doesn't apply the settings (and won't connect according to settings). This must be done manually by calling
* TrayWidget::applySettings(). This allows postponing connecting until all signals of the TrayWidget::connection()
* are connected as required.
*/
TrayWidget::TrayWidget(TrayMenu *parent)
: QWidget(parent)
, m_menu(parent)
, m_ui(new Ui::TrayWidget)
, m_webViewDlg(nullptr)
, m_internalErrorsButton(nullptr)
, m_notifier(m_connection)
, m_dirModel(m_connection)
, m_sortFilterDirModel(&m_dirModel)
, m_devModel(m_connection)
, m_sortFilterDevModel(&m_devModel)
, m_dlModel(m_connection)
, m_recentChangesModel(m_connection)
, m_selectedConnection(nullptr)
, m_startStopButtonTarget(StartStopButtonTarget::None)
, m_tabTextsShown(true)
, m_applyingSettingsForWizard(false)
{
// don't show connection status within connection settings if there are multiple tray widgets/icons (would be ambiguous)
if (!s_instances.empty() && s_settingsDlg) {
s_settingsDlg->hideConnectionStatus();
}
// take record of the newly created instance
s_instances.push_back(this);
// show the connection configuration in the previous icon's tooltip as soon as there's a 2nd icon
if (s_instances.size() == 2) {
s_instances.front()->updateIconAndTooltip();
}
m_ui->setupUi(this);
const auto tabCount = m_ui->tabWidget->count();
m_tabTexts.reserve(tabCount);
for (decltype(m_ui->tabWidget->count()) i = 0; i != tabCount; ++i) {
m_tabTexts << m_ui->tabWidget->tabText(i);
}
// setup models and views
m_ui->dirsTreeView->header()->setSortIndicator(0, Qt::AscendingOrder);
m_ui->dirsTreeView->setModel(&m_sortFilterDirModel);
m_ui->devsTreeView->header()->setSortIndicator(0, Qt::AscendingOrder);
m_ui->devsTreeView->setModel(&m_sortFilterDevModel);
m_ui->downloadsTreeView->setModel(&m_dlModel);
m_ui->recentChangesTreeView->setModel(&m_recentChangesModel);
m_ui->recentChangesTreeView->setContextMenuPolicy(Qt::CustomContextMenu);
setBrightColorsOfModelsAccordingToPalette();
// setup sync-all button
m_cornerFrame = new QFrame(this);
auto *cornerFrameLayout = new QHBoxLayout(m_cornerFrame);
cornerFrameLayout->setSpacing(0);
cornerFrameLayout->setContentsMargins(0, 0, 0, 0);
m_cornerFrame->setLayout(cornerFrameLayout);
auto *viewIdButton = new QPushButton(m_cornerFrame);
viewIdButton->setToolTip(tr("View own device ID"));
viewIdButton->setIcon(QIcon(QStringLiteral("qrcode.fa")));
viewIdButton->setFlat(true);
cornerFrameLayout->addWidget(viewIdButton);
auto *restartButton = new QPushButton(m_cornerFrame);
restartButton->setToolTip(tr("Restart Syncthing"));
restartButton->setIcon(QIcon(QStringLiteral("power-off.fa")));
restartButton->setFlat(true);
cornerFrameLayout->addWidget(restartButton);
auto *showLogButton = new QPushButton(m_cornerFrame);
showLogButton->setToolTip(tr("Show Syncthing log"));
showLogButton->setIcon(QIcon(QStringLiteral("file-text.fa")));
showLogButton->setFlat(true);
cornerFrameLayout->addWidget(showLogButton);
auto *scanAllButton = new QPushButton(m_cornerFrame);
scanAllButton->setToolTip(tr("Rescan all folders"));
scanAllButton->setIcon(QIcon(QStringLiteral("refresh.fa")));
scanAllButton->setFlat(true);
cornerFrameLayout->addWidget(scanAllButton);
m_ui->tabWidget->setCornerWidget(m_cornerFrame, Qt::BottomRightCorner);
// setup connection menu
m_connectionsActionGroup = new QActionGroup(m_connectionsMenu = new QMenu(tr("Connection"), this));
m_connectionsMenu->setIcon(
QIcon::fromTheme(QStringLiteral("network-connect"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/network-connect.svg"))));
m_ui->connectionsPushButton->setMenu(m_connectionsMenu);
// setup notifications menu
m_notificationsMenu = new QMenu(tr("New notifications"), this);
m_notificationsMenu->addAction(m_ui->actionShowNotifications);
m_notificationsMenu->addAction(m_ui->actionDismissNotifications);
m_ui->notificationsPushButton->setMenu(m_notificationsMenu);
// setup other widgets
m_ui->notificationsPushButton->setHidden(true);
updateTrafficText();
setTrafficPixmaps(true);
setLabelPixmaps();
// add actions from right-click menu if it is not available
#ifndef SYNCTHINGTRAY_UNIFY_TRAY_MENUS
if (!m_menu) {
#endif
m_internalErrorsButton = new QPushButton(m_cornerFrame);
m_internalErrorsButton->setToolTip(tr("Show internal errors"));
m_internalErrorsButton->setIcon(
QIcon::fromTheme(QStringLiteral("emblem-error"), QIcon(QStringLiteral(":/icons/hicolor/scalable/emblems/8/emblem-error.svg"))));
m_internalErrorsButton->setFlat(true);
m_internalErrorsButton->setVisible(false);
connect(m_internalErrorsButton, &QPushButton::clicked, this, &TrayWidget::showInternalErrorsDialog);
cornerFrameLayout->addWidget(m_internalErrorsButton);
if (!m_menu) {
connect(&m_connection, &SyncthingConnection::error, this, &TrayWidget::showInternalError);
}
#ifndef SYNCTHINGTRAY_UNIFY_TRAY_MENUS
}
#endif
#ifdef SYNCTHINGTRAY_UNIFY_TRAY_MENUS
auto *quitButton = new QPushButton(m_cornerFrame);
quitButton->setToolTip(tr("Quit Syncthing Tray"));
quitButton->setIcon(QIcon::fromTheme(QStringLiteral("window-close"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/window-close.svg"))));
quitButton->setFlat(true);
connect(quitButton, &QPushButton::clicked, this, &TrayWidget::quitTray);
cornerFrameLayout->addWidget(quitButton);
#endif
// connect signals and slots
connect(m_ui->statusPushButton, &QPushButton::clicked, this, &TrayWidget::changeStatus);
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);
connect(&m_connection, &SyncthingConnection::statusChanged, this, &TrayWidget::handleStatusChanged);
connect(&m_connection, &SyncthingConnection::trafficChanged, this, &TrayWidget::updateTraffic);
connect(&m_connection, &SyncthingConnection::dirStatisticsChanged, this, &TrayWidget::updateOverallStatistics);
connect(&m_connection, &SyncthingConnection::newNotification, this, &TrayWidget::handleNewNotification);
connect(m_ui->dirsTreeView, &DirView::openDir, this, &TrayWidget::openDir);
connect(m_ui->dirsTreeView, &DirView::scanDir, this, &TrayWidget::scanDir);
connect(m_ui->dirsTreeView, &DirView::pauseResumeDir, this, &TrayWidget::pauseResumeDir);
connect(m_ui->devsTreeView, &DevView::pauseResumeDev, this, &TrayWidget::pauseResumeDev);
connect(m_ui->downloadsTreeView, &DownloadView::openDir, this, &TrayWidget::openDir);
connect(m_ui->downloadsTreeView, &DownloadView::openItemDir, this, &TrayWidget::openItemDir);
connect(m_ui->recentChangesTreeView, &QTreeView::customContextMenuRequested, this, &TrayWidget::showRecentChangesContextMenu);
connect(scanAllButton, &QPushButton::clicked, &m_connection, &SyncthingConnection::rescanAllDirs);
connect(viewIdButton, &QPushButton::clicked, this, &TrayWidget::showOwnDeviceId);
connect(showLogButton, &QPushButton::clicked, this, &TrayWidget::showLog);
connect(m_ui->notificationsPushButton, &QPushButton::clicked, this, &TrayWidget::showNotifications);
connect(restartButton, &QPushButton::clicked, this, &TrayWidget::restartSyncthing);
connect(m_connectionsActionGroup, &QActionGroup::triggered, this, &TrayWidget::handleConnectionSelected);
connect(m_ui->actionShowNotifications, &QAction::triggered, this, &TrayWidget::showNotifications);
connect(m_ui->actionDismissNotifications, &QAction::triggered, this, &TrayWidget::dismissNotifications);
connect(m_ui->startStopPushButton, &QPushButton::clicked, this, &TrayWidget::toggleRunning);
if (const auto *const launcher = SyncthingLauncher::mainInstance()) {
connect(launcher, &SyncthingLauncher::runningChanged, this, &TrayWidget::handleLauncherStatusChanged);
}
#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD
if (const auto *const service = SyncthingService::mainInstance()) {
connect(service, &SyncthingService::systemdAvailableChanged, this, &TrayWidget::handleSystemdStatusChanged);
connect(service, &SyncthingService::stateChanged, this, &TrayWidget::handleSystemdStatusChanged);
}
#endif
}
TrayWidget::~TrayWidget()
{
auto i = std::find(s_instances.begin(), s_instances.end(), this);
if (i != s_instances.end()) {
s_instances.erase(i);
}
if (s_instances.empty()) {
delete s_settingsDlg;
delete s_aboutDlg;
QCoreApplication::quit();
} else if (s_instances.size() == 1) {
s_instances.front()->updateIconAndTooltip();
}
}
SettingsDialog *TrayWidget::settingsDialog()
{
if (!s_settingsDlg) {
s_settingsDlg = new SettingsDialog(s_instances.size() < 2 ? &m_connection : nullptr);
connect(s_settingsDlg, &SettingsDialog::wizardRequested, this, &TrayWidget::showWizard);
connect(s_settingsDlg, &SettingsDialog::applied, &TrayWidget::applySettingsOnAllInstances);
if (m_menu) {
connect(m_menu, &TrayMenu::positioningSettingsChanged, s_settingsDlg, &SettingsDialog::resetPositioningSettings);
}
// save settings to disk when applied
// note: QCoreApplication::aboutToQuit() does not work reliably but terminating only at the
// end of the session is a common use-case for the tray application. So workaround this
// by simply saving the settings immediately.
connect(s_settingsDlg, &SettingsDialog::applied, this, &TrayWidget::saveSettings);
}
return s_settingsDlg;
}
void TrayWidget::showSettingsDialog()
{
auto *const dlg = settingsDialog();
// show settings dialog centered or maximized if the relatively big windows would overflow
showDialog(dlg, centerWidgetAvoidingOverflow(dlg));
}
void TrayWidget::showLauncherSettings()
{
showSettingsDialog();
settingsDialog()->selectLauncherSettings();
}
void TrayWidget::showWizard()
{
if (!s_wizard) {
s_wizard = Wizard::instance();
connect(s_wizard, &Wizard::destroyed, this, [] { s_wizard = nullptr; });
connect(s_wizard, &Wizard::settingsDialogRequested, this, &TrayWidget::showSettingsDialog);
connect(s_wizard, &Wizard::openLauncherSettingsRequested, this, &TrayWidget::showLauncherSettings);
connect(s_wizard, &Wizard::openSyncthingRequested, this, &TrayWidget::showWebUI);
connect(s_wizard, &Wizard::settingsChanged, this, &TrayWidget::applySettingsChangesFromWizard);
}
showDialog(s_wizard, centerWidgetAvoidingOverflow(s_wizard));
}
void TrayWidget::applySettingsChangesFromWizard()
{
// reset possibly opened settings dialog to be consistent with new configuration
if (s_settingsDlg) {
s_settingsDlg->reset();
}
// consider the settings applied instantly if there's no tray widget instance
if (s_instances.empty()) {
if (s_wizard) {
s_wizard->handleConfigurationApplied(QString(), &m_connection);
}
return;
}
// apply settings on all tray widgets using the normal re-connect logic
// note: Ensure at least one tray widget has the primary config selected as this is the config changed
// by the wizard we need to keep track of.
const auto &primaryConnectionName = Settings::values().connection.primary.label;
TrayWidget *relevantInstance = nullptr;
for (auto *const instance : s_instances) {
auto index = instance->m_connectionsMenu->actions().indexOf(instance->m_connectionsActionGroup->checkedAction());
if (index == 0) {
instance->m_applyingSettingsForWizard = true;
relevantInstance = instance;
break;
}
}
for (auto *const instance : s_instances) {
if (!relevantInstance) {
instance->m_applyingSettingsForWizard = true;
instance->applySettings(primaryConnectionName);
relevantInstance = instance;
} else {
instance->applySettings();
}
}
}
void TrayWidget::saveSettings()
{
Settings::save();
if (const auto &error = Settings::values().error; !error.isEmpty()) {
QMessageBox::critical(nullptr, QCoreApplication::applicationName(), error);
}
}
void TrayWidget::showAboutDialog()
{
if (!s_aboutDlg) {
s_aboutDlg
= new AboutDialog(nullptr, QString(), aboutDialogAttribution(), QString(), {}, QStringLiteral(APP_URL), QString(), aboutDialogImage());
s_aboutDlg->setWindowTitle(tr("About") + QStringLiteral(" - " APP_NAME));
s_aboutDlg->setWindowIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/app/syncthingtray.svg")));
}
centerWidget(s_aboutDlg);
showDialog(s_aboutDlg);
}
void TrayWidget::showWebUI()
{
auto *const dlg = QtGui::showWebUI(m_connection.syncthingUrl(), m_selectedConnection, m_webViewDlg, this);
#ifndef SYNCTHINGWIDGETS_NO_WEBVIEW
if (!dlg) {
return;
}
if (!m_webViewDlg) {
m_webViewDlg = dlg;
connect(m_webViewDlg, &WebViewDialog::destroyed, this, &TrayWidget::handleWebViewDeleted);
}
showDialog(m_webViewDlg);
#else
Q_UNUSED(dlg)
#endif
}
void TrayWidget::showOwnDeviceId()
{
auto *const dlg = ownDeviceIdDialog(m_connection);
dlg->setAttribute(Qt::WA_DeleteOnClose, true);
showDialog(dlg, centerWidgetAvoidingOverflow(dlg));
}
void TrayWidget::showLog()
{
auto *const dlg = TextViewDialog::forLogEntries(m_connection);
dlg->setAttribute(Qt::WA_DeleteOnClose, true);
showDialog(dlg, centerWidgetAvoidingOverflow(dlg));
}
void TrayWidget::showNotifications()
{
auto *const dlg = TextViewDialog::forLogEntries(m_notifications, tr("New notifications"));
dlg->setAttribute(Qt::WA_DeleteOnClose, true);
showDialog(dlg, centerWidgetAvoidingOverflow(dlg));
m_notifications.clear();
dismissNotifications();
}
void TrayWidget::showUsingPositioningSettings()
{
if (m_menu) {
m_menu->showUsingPositioningSettings();
return;
}
if (const auto pos = Settings::values().appearance.positioning.positionToUse(); pos.has_value()) {
move(pos.value());
}
show();
}
void TrayWidget::showInternalError(
const QString &errorMessage, SyncthingErrorCategory category, int networkError, const QNetworkRequest &request, const QByteArray &response)
{
if (!InternalError::isRelevant(connection(), category, errorMessage, networkError)) {
return;
}
InternalErrorsDialog::addError(errorMessage, request.url(), response);
showInternalErrorsButton();
}
void TrayWidget::showInternalErrorsButton()
{
if (m_internalErrorsButton) {
m_internalErrorsButton->setVisible(true);
}
}
void TrayWidget::showInternalErrorsDialog()
{
auto *const errorViewDlg = InternalErrorsDialog::instance();
if (m_internalErrorsButton) {
connect(errorViewDlg, &InternalErrorsDialog::errorsCleared, m_internalErrorsButton, &QWidget::hide);
}
showDialog(errorViewDlg, centerWidgetAvoidingOverflow(errorViewDlg));
}
void TrayWidget::dismissNotifications()
{
m_connection.considerAllNotificationsRead();
m_ui->notificationsPushButton->setHidden(true);
if (m_menu && m_menu->icon()) {
m_menu->icon()->updateStatusIconAndText();
}
}
void TrayWidget::restartSyncthing()
{
if (QMessageBox::warning(
this, QCoreApplication::applicationName(), tr("Do you really want to restart Syncthing?"), QMessageBox::Yes, QMessageBox::No)
== QMessageBox::Yes) {
m_connection.restart();
}
}
void TrayWidget::quitTray()
{
QObject *parent;
if (m_menu) {
if (m_menu->icon()) {
parent = m_menu->icon();
} else {
parent = m_menu;
}
} else {
parent = this;
}
parent->deleteLater();
}
void TrayWidget::handleStatusChanged(SyncthingStatus status)
{
switch (status) {
case SyncthingStatus::Disconnected:
case SyncthingStatus::Reconnecting:
if (status == SyncthingStatus::Reconnecting || m_connection.isConnecting()) {
m_ui->statusPushButton->setText(tr("Connecting …"));
m_ui->statusPushButton->setToolTip(tr("Establishing connection to Syncthing …"));
m_ui->statusPushButton->setIcon(QIcon(QStringLiteral("refresh.fa")));
m_ui->statusPushButton->setEnabled(false);
} else {
m_ui->statusPushButton->setText(tr("Connect"));
m_ui->statusPushButton->setToolTip(tr("Not connected to Syncthing, click to connect"));
m_ui->statusPushButton->setIcon(QIcon(QStringLiteral("refresh.fa")));
m_ui->statusPushButton->setEnabled(true);
updateTraffic(); // ensure previous traffic statistics are no longer shown
if (m_applyingSettingsForWizard) {
concludeWizard(tr("Unable to establish connection to Syncthing."));
}
}
return;
case SyncthingStatus::Idle:
case SyncthingStatus::Scanning:
case SyncthingStatus::Synchronizing:
case SyncthingStatus::RemoteNotInSync:
m_ui->statusPushButton->setText(tr("Pause"));
m_ui->statusPushButton->setToolTip(tr("Syncthing is running, click to pause all devices"));
m_ui->statusPushButton->setIcon(QIcon(QStringLiteral("pause.fa")));
m_ui->statusPushButton->setEnabled(true);
break;
case SyncthingStatus::Paused:
m_ui->statusPushButton->setText(tr("Continue"));
m_ui->statusPushButton->setToolTip(tr("At least one device is paused, click to resume"));
m_ui->statusPushButton->setIcon(QIcon(QStringLiteral("play.fa")));
m_ui->statusPushButton->setEnabled(true);
break;
default:;
}
concludeWizard();
}
void TrayWidget::applySettings(const QString &connectionConfig)
{
// update connections menu
int connectionIndex = 0;
auto &settings = Settings::values();
auto &primaryConnectionSettings = settings.connection.primary;
auto &secondaryConnectionSettings = settings.connection.secondary;
const int connectionCount = static_cast<int>(1 + secondaryConnectionSettings.size());
const QList<QAction *> connectionActions = m_connectionsActionGroup->actions();
m_selectedConnection = nullptr;
bool specifiedConnectionConfigFound = false;
for (; connectionIndex < connectionCount; ++connectionIndex) {
SyncthingConnectionSettings &connectionSettings
= (connectionIndex == 0 ? primaryConnectionSettings : secondaryConnectionSettings[static_cast<size_t>(connectionIndex - 1)]);
QAction *action;
if (connectionIndex < connectionActions.size()) {
action = connectionActions.at(connectionIndex);
action->setText(connectionSettings.label);
if (action->isChecked() && !m_selectedConnection) {
m_selectedConnection = &connectionSettings;
}
} else {
action = m_connectionsMenu->addAction(connectionSettings.label);
action->setCheckable(true);
m_connectionsActionGroup->addAction(action);
}
if (!connectionConfig.isEmpty() && !connectionSettings.label.compare(connectionConfig, Qt::CaseInsensitive)) {
m_selectedConnection = &connectionSettings;
specifiedConnectionConfigFound = true;
action->setChecked(true);
}
}
for (; connectionIndex < connectionActions.size(); ++connectionIndex) {
delete connectionActions.at(connectionIndex);
}
if (!m_selectedConnection) {
m_selectedConnection = &primaryConnectionSettings;
m_connectionsMenu->actions().at(0)->setChecked(true);
}
m_ui->connectionsPushButton->setText(m_selectedConnection->label);
m_ui->connectionsPushButton->setHidden(secondaryConnectionSettings.empty());
const bool reconnectRequired = m_connection.applySettings(*m_selectedConnection);
// apply notification settings
settings.apply(m_notifier);
// apply systemd and launcher settings enforcing a reconnect if required and possible
#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD
const auto systemdStatus = applySystemdSettings(reconnectRequired);
const auto launcherStatus = applyLauncherSettings(reconnectRequired, systemdStatus.consideredForReconnect, systemdStatus.showStartStopButton);
const auto showStartStopButton = systemdStatus.showStartStopButton || launcherStatus.showStartStopButton;
const auto systemdOrLauncherRelevantForReconnect = systemdStatus.relevant || launcherStatus.relevant;
#else
const auto launcherStatus = applyLauncherSettings(reconnectRequired);
const auto showStartStopButton = launcherStatus.showStartStopButton;
const auto systemdOrLauncherRelevantForReconnect = launcherStatus.relevant;
#endif
m_ui->startStopPushButton->setVisible(showStartStopButton);
if (reconnectRequired && !systemdOrLauncherRelevantForReconnect) {
// simply enforce the reconnect for this connection if the systemd or launcher status are relevant for it
m_connection.reconnect();
}
#ifndef SYNCTHINGWIDGETS_NO_WEBVIEW
// web view
if (m_webViewDlg) {
m_webViewDlg->applySettings(*m_selectedConnection, false);
}
#endif
// update visual appearance
m_ui->trafficFormWidget->setVisible(settings.appearance.showTraffic);
m_ui->trafficHorizontalSpacer->changeSize(
0, 20, settings.appearance.showTraffic ? QSizePolicy::Expanding : QSizePolicy::Ignored, QSizePolicy::Minimum);
if (settings.appearance.showTraffic) {
updateTraffic();
}
m_ui->infoFrame->setFrameStyle(settings.appearance.frameStyle);
m_ui->buttonsFrame->setFrameStyle(settings.appearance.frameStyle);
if (QApplication::style() && !QApplication::style()->objectName().compare(QLatin1String("adwaita"), Qt::CaseInsensitive)) {
m_cornerFrame->setFrameStyle(QFrame::NoFrame);
} else {
m_cornerFrame->setFrameStyle(settings.appearance.frameStyle);
}
if (settings.appearance.tabPosition >= QTabWidget::North && settings.appearance.tabPosition <= QTabWidget::East) {
m_ui->tabWidget->setTabPosition(static_cast<QTabWidget::TabPosition>(settings.appearance.tabPosition));
}
IconManager::instance().applySettings(&settings.icons.status, settings.icons.distinguishTrayIcons ? &settings.icons.tray : nullptr);
if (m_tabTextsShown != settings.appearance.showTabTexts) {
const auto tabCount = m_ui->tabWidget->count();
if ((m_tabTextsShown = settings.appearance.showTabTexts)) {
for (decltype(m_ui->tabWidget->count()) i = 0; i != tabCount; ++i) {
m_ui->tabWidget->setTabText(i, m_tabTexts[i]);
m_ui->tabWidget->setTabToolTip(i, QString());
}
} else {
for (decltype(m_ui->tabWidget->count()) i = 0; i != tabCount; ++i) {
m_ui->tabWidget->setTabText(i, QString());
m_ui->tabWidget->setTabToolTip(i, m_tabTexts[i]);
}
}
}
if (m_menu) {
m_menu->setWindowType(settings.appearance.windowType);
}
// update status icon and text of tray icon because reconnect interval might have changed
if (m_menu && m_menu->icon()) {
m_menu->icon()->updateStatusIconAndText();
}
// show warning when explicitly specified connection configuration was not found
if (!specifiedConnectionConfigFound && !connectionConfig.isEmpty()) {
auto *const msgBox = new QMessageBox(QMessageBox::Warning, QCoreApplication::applicationName(),
tr("The specified connection configuration <em>%1</em> is not defined and hence ignored.").arg(connectionConfig));
msgBox->setAttribute(Qt::WA_DeleteOnClose);
msgBox->show();
}
// conclude wizard immediately if no re-connect was required anyways
if (!reconnectRequired) {
concludeWizard();
}
}
bool TrayWidget::event(QEvent *event)
{
const auto res = QWidget::event(event);
switch (event->type()) {
case QEvent::PaletteChange:
setBrightColorsOfModelsAccordingToPalette();
setLabelPixmaps();
setTrafficPixmaps(true);
break;
case QEvent::LanguageChange:
m_ui->retranslateUi(this);
applyLauncherSettings(false, true, false);
handleStatusChanged(m_connection.status());
if (m_menu && m_menu->icon()) {
m_menu->icon()->updateStatusIconAndText();
}
break;
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
case QEvent::DevicePixelRatioChange:
setLabelPixmaps();
setTrafficPixmaps(true);
IconManager::instance().update();
break;
#endif
default:;
}
return res;
}
void TrayWidget::applySettingsOnAllInstances()
{
auto &qtSettings = Settings::values().qt;
qtSettings.apply();
if (qtSettings.hasLocaleChanged()) {
QtUtilities::TranslationFiles::clearTranslationFiles();
LOAD_QT_TRANSLATIONS;
}
for (TrayWidget *instance : s_instances) {
instance->applySettings();
}
}
void TrayWidget::openDir(const SyncthingDir &dir)
{
const auto path = substituteTilde(dir.path, m_connection.tilde(), m_connection.pathSeparator());
if (QDir(path).exists()) {
openLocalFileOrDir(path);
} else {
QMessageBox::warning(this, QCoreApplication::applicationName(), tr("The folder <i>%1</i> does not exist on the local machine.").arg(path));
}
}
void TrayWidget::openItemDir(const SyncthingItemDownloadProgress &item)
{
const QDir containingDir(item.fileInfo.absoluteDir());
if (containingDir.exists()) {
openLocalFileOrDir(containingDir.path());
} else {
QMessageBox::warning(this, QCoreApplication::applicationName(),
tr("The containing folder <i>%1</i> does not exist on the local machine.").arg(item.fileInfo.filePath()));
}
}
void TrayWidget::scanDir(const SyncthingDir &dir)
{
m_connection.rescan(dir.id);
}
void TrayWidget::pauseResumeDev(const SyncthingDev &dev)
{
if (dev.paused) {
m_connection.resumeDevice(QStringList(dev.id));
} else {
m_connection.pauseDevice(QStringList(dev.id));
}
}
void TrayWidget::pauseResumeDir(const SyncthingDir &dir)
{
if (dir.paused) {
m_connection.resumeDirectories(QStringList(dir.id));
} else {
m_connection.pauseDirectories(QStringList(dir.id));
}
}
void TrayWidget::showRecentChangesContextMenu(const QPoint &position)
{
const auto *const selectionModel = m_ui->recentChangesTreeView->selectionModel();
if (!selectionModel || selectionModel->selectedRows().size() != 1) {
return;
}
const auto copyRole = [this](SyncthingRecentChangesModel::SyncthingRecentChangesModelRole role) {
return [this, role] {
const auto *const selectionModelToCopy = m_ui->recentChangesTreeView->selectionModel();
if (selectionModelToCopy && selectionModelToCopy->selectedRows().size() == 1) {
QGuiApplication::clipboard()->setText(m_recentChangesModel.data(selectionModelToCopy->selectedRows().at(0), role).toString());
}
};
};
QMenu menu(this);
connect(menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-copy.svg"))),
tr("Copy path")),
&QAction::triggered, this, copyRole(SyncthingRecentChangesModel::Path));
connect(menu.addAction(QIcon::fromTheme(QStringLiteral("network-server-symbolic"),
QIcon(QStringLiteral(":/icons/hicolor/scalable/places/network-workgroup.svg"))),
tr("Copy device ID")),
&QAction::triggered, this, copyRole(SyncthingRecentChangesModel::ModifiedBy));
connect(menu.addAction(QIcon::fromTheme(QStringLiteral("folder"), QIcon(QStringLiteral(":/icons/hicolor/scalable/places/folder.svg"))),
tr("Copy folder ID")),
&QAction::triggered, this, copyRole(SyncthingRecentChangesModel::DirectoryId));
showViewMenu(position, *m_ui->recentChangesTreeView, menu);
}
void TrayWidget::changeStatus()
{
switch (m_connection.status()) {
case SyncthingStatus::Disconnected:
m_connection.connect();
break;
case SyncthingStatus::Reconnecting:
break;
case SyncthingStatus::Idle:
case SyncthingStatus::Scanning:
case SyncthingStatus::Synchronizing:
case SyncthingStatus::RemoteNotInSync:
m_connection.pauseAllDevs();
break;
case SyncthingStatus::Paused:
m_connection.resumeAllDevs();
break;
default:;
}
}
void TrayWidget::updateTraffic()
{
if (updateTrafficText()) {
setTrafficPixmaps();
}
}
bool TrayWidget::updateTrafficText()
{
if (m_ui->trafficFormWidget->isHidden()) {
return false;
}
// update text and whether to use active/inactive icons
m_ui->inTrafficLabel->setText(trafficString(m_connection.totalIncomingTraffic(), m_connection.totalIncomingRate()));
m_ui->outTrafficLabel->setText(trafficString(m_connection.totalOutgoingTraffic(), m_connection.totalOutgoingRate()));
return true;
}
void TrayWidget::updateOverallStatistics()
{
const auto overallStats = m_connection.computeOverallDirStatistics();
m_ui->globalStatisticsLabel->setText(directoryStatusString(overallStats.global));
m_ui->localStatisticsLabel->setText(directoryStatusString(overallStats.local));
}
void TrayWidget::updateIconAndTooltip()
{
if (!m_menu) {
return;
}
if (auto *const trayIcon = m_menu->icon()) {
trayIcon->updateStatusIconAndText();
}
}
void TrayWidget::toggleRunning()
{
switch (m_startStopButtonTarget) {
#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD
case StartStopButtonTarget::Service:
if (auto *const service = SyncthingService::mainInstance()) {
service->toggleRunning();
}
break;
#endif
case StartStopButtonTarget::Launcher:
if (auto *const launcher = SyncthingLauncher::mainInstance()) {
if (launcher->isRunning()) {
launcher->terminate(&m_connection);
} else {
launcher->launch(Settings::values().launcher);
}
}
break;
default:;
}
}
Settings::Launcher::LauncherStatus TrayWidget::handleLauncherStatusChanged()
{
#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD
const auto systemdStatus = Settings::values().systemd.status(m_connection);
const auto launcherStatus = applyLauncherSettings(false, systemdStatus.consideredForReconnect, systemdStatus.showStartStopButton);
const auto showStartStopButton = systemdStatus.showStartStopButton || launcherStatus.showStartStopButton;
#else
const auto launcherStatus = applyLauncherSettings(false);
const auto showStartStopButton = launcherStatus.showStartStopButton;
#endif
m_ui->startStopPushButton->setVisible(showStartStopButton);
return launcherStatus;
}
Settings::Launcher::LauncherStatus TrayWidget::applyLauncherSettings(bool reconnectRequired, bool skipApplyingToConnection, bool skipStartStopButton)
{
// update connection
const auto &launcherSettings = Settings::values().launcher;
const auto launcherStatus = skipApplyingToConnection ? launcherSettings.status(m_connection)
: launcherSettings.apply(m_connection, m_selectedConnection, reconnectRequired);
if (skipStartStopButton || !launcherStatus.showStartStopButton) {
return launcherStatus;
}
// update start/stop button
m_startStopButtonTarget = StartStopButtonTarget::Launcher;
if (launcherStatus.running) {
m_ui->startStopPushButton->setText(tr("Stop"));
m_ui->startStopPushButton->setToolTip(tr("Stop Syncthing instance launched via tray icon"));
m_ui->startStopPushButton->setIcon(QIcon(QStringLiteral("stop.fa")));
} else {
m_ui->startStopPushButton->setText(tr("Start"));
m_ui->startStopPushButton->setToolTip(tr("Start Syncthing with the built-in launcher configured in the settings"));
m_ui->startStopPushButton->setIcon(QIcon(QStringLiteral("play.fa")));
}
return launcherStatus;
}
#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD
Settings::Systemd::ServiceStatus TrayWidget::handleSystemdStatusChanged()
{
const auto systemdStatus = applySystemdSettings();
if (systemdStatus.showStartStopButton) {
m_ui->startStopPushButton->setVisible(true);
return systemdStatus;
}
// update the start/stop button which might now control the internal launcher
const auto launcherStatus = applyLauncherSettings(false, true, false);
m_ui->startStopPushButton->setVisible(launcherStatus.showStartStopButton);
return systemdStatus;
}
Settings::Systemd::ServiceStatus TrayWidget::applySystemdSettings(bool reconnectRequired)
{
// update connection
const auto &systemdSettings = Settings::values().systemd;
const auto serviceStatus = systemdSettings.apply(m_connection, m_selectedConnection, reconnectRequired);
if (!serviceStatus.showStartStopButton) {
return serviceStatus;
}
// update start/stop button
m_startStopButtonTarget = StartStopButtonTarget::Service;
if (serviceStatus.running) {
m_ui->startStopPushButton->setText(tr("Stop"));
m_ui->startStopPushButton->setToolTip(
(serviceStatus.userService ? QStringLiteral("systemctl --user stop ") : QStringLiteral("systemctl stop "))
+ systemdSettings.syncthingUnit);
m_ui->startStopPushButton->setIcon(QIcon(QStringLiteral("stop.fa")));
} else {
m_ui->startStopPushButton->setText(tr("Start"));
m_ui->startStopPushButton->setToolTip(
(serviceStatus.userService ? QStringLiteral("systemctl --user start ") : QStringLiteral("systemctl start "))
+ systemdSettings.syncthingUnit);
m_ui->startStopPushButton->setIcon(QIcon(QStringLiteral("play.fa")));
}
return serviceStatus;
}
#endif
void TrayWidget::handleWebViewDeleted()
{
m_webViewDlg = nullptr;
}
void TrayWidget::handleNewNotification(DateTime when, const QString &msg)
{
m_notifications.emplace_back(QString::fromLocal8Bit(when.toString(DateTimeOutputFormat::DateAndTime, true).data()), msg);
m_ui->notificationsPushButton->setHidden(false);
}
void TrayWidget::handleConnectionSelected(QAction *connectionAction)
{
auto index = m_connectionsMenu->actions().indexOf(connectionAction);
if (index >= 0) {
m_selectedConnection
= (index == 0) ? &Settings::values().connection.primary : &Settings::values().connection.secondary[static_cast<size_t>(index - 1)];
m_ui->connectionsPushButton->setText(m_selectedConnection->label);
m_connection.reconnect(*m_selectedConnection);
#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD
handleSystemdStatusChanged();
#endif
#ifndef SYNCTHINGWIDGETS_NO_WEBVIEW
if (m_webViewDlg) {
m_webViewDlg->applySettings(*m_selectedConnection, false);
}
#endif
}
}
void TrayWidget::concludeWizard(const QString &errorMessage)
{
if (!m_applyingSettingsForWizard) {
return;
}
m_applyingSettingsForWizard = false;
if (s_wizard) {
s_wizard->handleConfigurationApplied(errorMessage, &m_connection);
}
}
void TrayWidget::showDialog(QWidget *dlg, bool maximized)
{
if (m_menu && m_menu->windowType() != TrayMenu::WindowType::NormalWindow) {
m_menu->close();
}
if (maximized) {
dlg->showMaximized();
} else {
dlg->show();
}
dlg->activateWindow();
}
void TrayWidget::setBrightColorsOfModelsAccordingToPalette()
{
auto &qtSettings = Settings::values().qt;
qtSettings.reevaluatePaletteAndDefaultIconTheme();
const auto brightColors = qtSettings.isPaletteDark();
m_dirModel.setBrightColors(brightColors);
m_devModel.setBrightColors(brightColors);
m_dlModel.setBrightColors(brightColors);
m_recentChangesModel.setBrightColors(brightColors);
}
void TrayWidget::setLabelPixmaps()
{
m_ui->globalTextLabel->setPixmap(QIcon(QStringLiteral("globe.fa")).pixmap(16));
m_ui->localTextLabel->setPixmap(QIcon(QStringLiteral("home.fa")).pixmap(16));
}
void TrayWidget::setTrafficPixmaps(bool recompute)
{
if (recompute) {
const auto size = QSize(16, 13);
const auto &palette = m_ui->trafficFormWidget->palette();
const auto colorBackground = palette.color(QPalette::Window);
const auto colorActive = palette.color(QPalette::WindowText);
const auto colorInactive = QColor((colorActive.red() + colorBackground.red()) / 2, (colorActive.green() + colorBackground.green()) / 2,
(colorActive.blue() + colorBackground.blue()) / 2);
const auto renderIcon
= [&size](QtForkAwesome::Icon icon, const QColor &color) { return QtForkAwesome::Renderer::global().pixmap(icon, size, color); };
m_trafficIcons.uploadIconActive = renderIcon(QtForkAwesome::Icon::CloudUpload, colorActive);
m_trafficIcons.uploadIconInactive = renderIcon(QtForkAwesome::Icon::CloudUpload, colorInactive);
m_trafficIcons.downloadIconActive = renderIcon(QtForkAwesome::Icon::CloudDownload, colorActive);
m_trafficIcons.downloadIconInactive = renderIcon(QtForkAwesome::Icon::CloudDownload, colorInactive);
}
m_ui->trafficInTextLabel->setPixmap(
m_connection.totalIncomingRate() > 0.0 ? m_trafficIcons.downloadIconActive : m_trafficIcons.downloadIconInactive);
m_ui->trafficOutTextLabel->setPixmap(
m_connection.totalOutgoingRate() > 0.0 ? m_trafficIcons.uploadIconActive : m_trafficIcons.uploadIconInactive);
}
} // namespace QtGui