#include "./settingsdialog.h" #include "./settings.h" #include "./wizard.h" #include "../misc/syncthinglauncher.h" #include #include #include #include #include #include #ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD #include #include #include #endif #include "ui_appearanceoptionpage.h" #include "ui_autostartoptionpage.h" #include "ui_connectionoptionpage.h" #include "ui_iconsoptionpage.h" #include "ui_launcheroptionpage.h" #include "ui_notificationsoptionpage.h" #ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD #include "ui_systemdoptionpage.h" #endif #include "ui_builtinwebviewoptionpage.h" #include "ui_generalwebviewoptionpage.h" // use meta-data of syncthingtray application here #include "resources/../../tray/resources/config.h" #include #include #include #include #include #include #include #include #ifdef QT_UTILITIES_SUPPORT_DBUS_NOTIFICATIONS #include #endif #ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD #include #include #endif #include #include #include #include #include #if defined(PLATFORM_LINUX) && !defined(Q_OS_ANDROID) #include #elif defined(PLATFORM_WINDOWS) #include #include #include #elif defined(PLATFORM_MAC) #include #endif #include #include #include #include #include #include #include #include #include using namespace std; using namespace std::placeholders; using namespace Settings; using namespace Data; using namespace CppUtilities; using namespace QtUtilities; namespace QtGui { /// \brief Returns the tooltip text for the specified \a isMetered value. static QString meteredToolTip(std::optional isMetered) { return isMetered.has_value() ? (isMetered.value() ? QCoreApplication::translate("QtGui", "The network connection is currently considered metered.") : QCoreApplication::translate("QtGui", "The network connection is currently not considered metered.")) : QCoreApplication::translate("QtGui", "Unable to determine whether the network connection is metered; assuming an unmetered connection."); } /// \brief Configures the specified \a checkBox for the specified \a isMetered value. static void configureMeteredCheckbox(QCheckBox *checkBox, std::optional isMetered) { checkBox->setEnabled(isMetered.has_value()); checkBox->setToolTip(meteredToolTip(isMetered)); } // ConnectionOptionPage ConnectionOptionPage::ConnectionOptionPage(Data::SyncthingConnection *connection, QWidget *parentWidget) : ConnectionOptionPageBase(parentWidget) , m_connection(connection) , m_currentIndex(0) { } ConnectionOptionPage::~ConnectionOptionPage() { } void ConnectionOptionPage::hideConnectionStatus() { ui()->statusTextLabel->setHidden(true); ui()->statusLabel->setHidden(true); ui()->connectPushButton->setHidden(true); m_connection = nullptr; } QWidget *ConnectionOptionPage::setupWidget() { auto *const widget = ConnectionOptionPageBase::setupWidget(); m_statusComputionModel = new SyncthingStatusComputionModel(widget); ui()->certPathSelection->provideCustomFileMode(QFileDialog::ExistingFile); ui()->certPathSelection->lineEdit()->setPlaceholderText( QCoreApplication::translate("QtGui::ConnectionOptionPage", "Auto-detected for local instance")); ui()->instanceNoteIcon->setPixmap(QApplication::style()->standardIcon(QStyle::SP_MessageBoxInformation).pixmap(32, 32)); ui()->pollTrafficLabel->setToolTip(ui()->pollTrafficSpinBox->toolTip()); ui()->pollDevStatsLabel->setToolTip(ui()->pollDevStatsSpinBox->toolTip()); ui()->pollErrorsLabel->setToolTip(ui()->pollErrorsSpinBox->toolTip()); ui()->reconnectLabel->setToolTip(ui()->reconnectSpinBox->toolTip()); if (m_connection) { QObject::connect(m_connection, &SyncthingConnection::statusChanged, bind(&ConnectionOptionPage::updateConnectionStatus, this)); } else { hideConnectionStatus(); } ui()->statusComputionFlagsListView->setModel(m_statusComputionModel); QObject::connect(ui()->connectPushButton, &QPushButton::clicked, bind(&ConnectionOptionPage::applyAndReconnect, this)); QObject::connect(ui()->insertFromConfigFilePushButton, &QPushButton::clicked, bind(&ConnectionOptionPage::insertFromConfigFile, this, false)); QObject::connect( ui()->insertFromCustomConfigFilePushButton, &QPushButton::clicked, bind(&ConnectionOptionPage::insertFromConfigFile, this, true)); QObject::connect(ui()->selectionComboBox, static_cast(&QComboBox::currentIndexChanged), bind(&ConnectionOptionPage::showConnectionSettings, this, _1)); QObject::connect(ui()->selectionComboBox, static_cast(&QComboBox::editTextChanged), bind(&ConnectionOptionPage::saveCurrentConfigName, this, _1)); QObject::connect(ui()->downPushButton, &QPushButton::clicked, bind(&ConnectionOptionPage::moveSelectedConfigDown, this)); QObject::connect(ui()->upPushButton, &QPushButton::clicked, bind(&ConnectionOptionPage::moveSelectedConfigUp, this)); QObject::connect(ui()->addPushButton, &QPushButton::clicked, bind(&ConnectionOptionPage::addNewConfig, this)); QObject::connect(ui()->removePushButton, &QPushButton::clicked, bind(&ConnectionOptionPage::removeSelectedConfig, this)); QObject::connect(ui()->advancedCheckBox, &QCheckBox::toggled, bind(&ConnectionOptionPage::toggleAdvancedSettings, this, std::placeholders::_1)); const auto *const launcher = SyncthingLauncher::mainInstance(); configureMeteredCheckbox(ui()->pauseOnMeteredConnectionCheckBox, launcher ? launcher->isNetworkConnectionMetered() : std::nullopt); if (launcher) { QObject::connect(launcher, &SyncthingLauncher::networkConnectionMeteredChanged, bind(&configureMeteredCheckbox, ui()->pauseOnMeteredConnectionCheckBox, std::placeholders::_1)); } #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) ui()->timeoutSpinBox->setEnabled(false); #endif toggleAdvancedSettings(false); return widget; } void ConnectionOptionPage::insertFromConfigFile(bool forceFileSelection) { auto configFile(forceFileSelection ? QString() : SyncthingConfig::locateConfigFile()); if (configFile.isEmpty()) { // allow user to select config file manually if it could not be located configFile = QFileDialog::getOpenFileName( widget(), QCoreApplication::translate("QtGui::ConnectionOptionPage", "Select Syncthing config file") + QStringLiteral(" - " APP_NAME)); } if (configFile.isEmpty()) { return; } SyncthingConfig config; if (!config.restore(configFile)) { QMessageBox::critical(widget(), widget()->windowTitle() + QStringLiteral(" - " APP_NAME), QCoreApplication::translate("QtGui::ConnectionOptionPage", "Unable to parse the Syncthing config file.")); return; } if (!config.guiAddress.isEmpty()) { const auto portStart(config.guiAddress.indexOf(QChar(':'))); auto guiHost(config.guiAddress.mid(0, portStart)); const auto guiPort = portStart > 0 ? QtUtilities::midRef(config.guiAddress, portStart) : QtUtilities::StringView(); const QHostAddress guiAddress(guiHost); // assume local connection if address is eg. 0.0.0.0 auto localConnection = true; if (guiAddress == QHostAddress::AnyIPv4) { guiHost = QStringLiteral("127.0.0.1"); } else if (guiAddress == QHostAddress::AnyIPv6) { guiHost = QStringLiteral("[::1]"); } else if (!isLocal(guiHost, guiAddress)) { localConnection = false; } const QString guiProtocol((config.guiEnforcesSecureConnection || !localConnection) ? QStringLiteral("https://") : QStringLiteral("http://")); ui()->urlLineEdit->selectAll(); ui()->urlLineEdit->insert(guiProtocol % guiHost % guiPort); } if (!config.guiUser.isEmpty() || !config.guiPasswordHash.isEmpty()) { ui()->authCheckBox->setChecked(true); ui()->userNameLineEdit->selectAll(); ui()->userNameLineEdit->insert(config.guiUser); } else { ui()->authCheckBox->setChecked(false); } if (!config.guiApiKey.isEmpty()) { ui()->apiKeyLineEdit->selectAll(); ui()->apiKeyLineEdit->insert(config.guiApiKey); } } void ConnectionOptionPage::updateConnectionStatus() { if (m_connection) { ui()->statusLabel->setText(m_connection->statusText()); } } bool ConnectionOptionPage::showConnectionSettings(int index) { if (index == m_currentIndex) { return true; } if (!cacheCurrentSettings(false)) { ui()->selectionComboBox->setCurrentIndex(m_currentIndex); return false; } const SyncthingConnectionSettings &connectionSettings = (index == 0 ? m_primarySettings : m_secondarySettings[static_cast(index - 1)]); ui()->urlLineEdit->setText(connectionSettings.syncthingUrl); ui()->authCheckBox->setChecked(connectionSettings.authEnabled); ui()->userNameLineEdit->setText(connectionSettings.userName); ui()->passwordLineEdit->setText(connectionSettings.password); ui()->apiKeyLineEdit->setText(connectionSettings.apiKey); ui()->certPathSelection->lineEdit()->setText(connectionSettings.httpsCertPath); ui()->timeoutSpinBox->setValue(connectionSettings.requestTimeout); ui()->longPollingSpinBox->setValue(connectionSettings.longPollingTimeout); ui()->pollTrafficSpinBox->setValue(connectionSettings.trafficPollInterval); ui()->pollDevStatsSpinBox->setValue(connectionSettings.devStatsPollInterval); ui()->pollErrorsSpinBox->setValue(connectionSettings.errorsPollInterval); ui()->reconnectSpinBox->setValue(connectionSettings.reconnectInterval); ui()->autoConnectCheckBox->setChecked(connectionSettings.autoConnect); ui()->pauseOnMeteredConnectionCheckBox->setChecked(connectionSettings.pauseOnMeteredConnection); m_statusComputionModel->setStatusComputionFlags(connectionSettings.statusComputionFlags); setCurrentIndex(index); return true; } bool ConnectionOptionPage::cacheCurrentSettings(bool applying) { if (m_currentIndex < 0) { return true; } SyncthingConnectionSettings &connectionSettings = (m_currentIndex == 0 ? m_primarySettings : m_secondarySettings[static_cast(m_currentIndex - 1)]); connectionSettings.syncthingUrl = ui()->urlLineEdit->text(); connectionSettings.authEnabled = ui()->authCheckBox->isChecked(); connectionSettings.userName = ui()->userNameLineEdit->text(); connectionSettings.password = ui()->passwordLineEdit->text(); connectionSettings.apiKey = ui()->apiKeyLineEdit->text().toUtf8(); connectionSettings.expectedSslErrors.clear(); connectionSettings.httpsCertPath = ui()->certPathSelection->lineEdit()->text(); connectionSettings.requestTimeout = ui()->timeoutSpinBox->value(); connectionSettings.longPollingTimeout = ui()->longPollingSpinBox->value(); connectionSettings.trafficPollInterval = ui()->pollTrafficSpinBox->value(); connectionSettings.devStatsPollInterval = ui()->pollDevStatsSpinBox->value(); connectionSettings.errorsPollInterval = ui()->pollErrorsSpinBox->value(); connectionSettings.reconnectInterval = ui()->reconnectSpinBox->value(); connectionSettings.autoConnect = ui()->autoConnectCheckBox->isChecked(); connectionSettings.pauseOnMeteredConnection = ui()->pauseOnMeteredConnectionCheckBox->isChecked(); connectionSettings.statusComputionFlags = m_statusComputionModel->statusComputionFlags(); if (!connectionSettings.loadHttpsCert()) { const QString errorMessage = QCoreApplication::translate("QtGui::ConnectionOptionPage", "Unable to load specified certificate \"%1\".") .arg(connectionSettings.httpsCertPath); if (!applying) { QMessageBox::critical(widget(), QCoreApplication::applicationName(), errorMessage); } else { errors() << errorMessage; } return false; } return true; } void ConnectionOptionPage::saveCurrentConfigName(const QString &name) { const int index = ui()->selectionComboBox->currentIndex(); if (index == m_currentIndex && index >= 0) { (index == 0 ? m_primarySettings : m_secondarySettings[static_cast(index - 1)]).label = name; ui()->selectionComboBox->setItemText(index, name); } } void ConnectionOptionPage::addNewConfig() { m_secondarySettings.emplace_back(); m_secondarySettings.back().label = QCoreApplication::translate("QtGui::ConnectionOptionPage", "Instance %1").arg(ui()->selectionComboBox->count() + 1); ui()->selectionComboBox->addItem(m_secondarySettings.back().label); ui()->selectionComboBox->setCurrentIndex(ui()->selectionComboBox->count() - 1); ui()->removePushButton->setEnabled(true); } void ConnectionOptionPage::removeSelectedConfig() { if (m_secondarySettings.empty()) { return; } const int index = ui()->selectionComboBox->currentIndex(); if (index < 0 || static_cast(index) > m_secondarySettings.size()) { return; } if (index == 0) { m_primarySettings = std::move(m_secondarySettings.front()); m_secondarySettings.erase(m_secondarySettings.begin()); } else { m_secondarySettings.erase(m_secondarySettings.begin() + (index - 1)); } m_currentIndex = -1; ui()->selectionComboBox->removeItem(index); ui()->removePushButton->setEnabled(!m_secondarySettings.empty()); } void ConnectionOptionPage::moveSelectedConfigDown() { if (m_secondarySettings.empty()) { return; } const int index = ui()->selectionComboBox->currentIndex(); if (index < 0) { return; } if (index == 0) { swap(m_primarySettings, m_secondarySettings.front()); ui()->selectionComboBox->setItemText(0, m_primarySettings.label); ui()->selectionComboBox->setItemText(1, m_secondarySettings.front().label); setCurrentIndex(1); } else if (static_cast(index) < m_secondarySettings.size()) { SyncthingConnectionSettings ¤t = m_secondarySettings[static_cast(index) - 1]; SyncthingConnectionSettings &exchange = m_secondarySettings[static_cast(index)]; swap(current, exchange); ui()->selectionComboBox->setItemText(index, current.label); ui()->selectionComboBox->setItemText(index + 1, exchange.label); setCurrentIndex(index + 1); } ui()->selectionComboBox->setCurrentIndex(m_currentIndex); } void ConnectionOptionPage::moveSelectedConfigUp() { if (m_secondarySettings.empty()) { return; } const int index = ui()->selectionComboBox->currentIndex(); if (index <= 0) { return; } if (index == 1) { swap(m_primarySettings, m_secondarySettings.front()); ui()->selectionComboBox->setItemText(0, m_primarySettings.label); ui()->selectionComboBox->setItemText(1, m_secondarySettings.front().label); setCurrentIndex(0); } else if (static_cast(index) - 1 < m_secondarySettings.size()) { SyncthingConnectionSettings ¤t = m_secondarySettings[static_cast(index) - 1]; SyncthingConnectionSettings &exchange = m_secondarySettings[static_cast(index) - 2]; swap(current, exchange); ui()->selectionComboBox->setItemText(index, current.label); ui()->selectionComboBox->setItemText(index - 1, exchange.label); setCurrentIndex(index - 1); } ui()->selectionComboBox->setCurrentIndex(m_currentIndex); } void ConnectionOptionPage::setCurrentIndex(int currentIndex) { m_currentIndex = currentIndex; ui()->downPushButton->setEnabled(currentIndex >= 0 && static_cast(currentIndex) < m_secondarySettings.size()); ui()->upPushButton->setEnabled(currentIndex > 0 && static_cast(currentIndex) - 1 < m_secondarySettings.size()); } void ConnectionOptionPage::toggleAdvancedSettings(bool show) { if (!ui()) { return; } #if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) for (auto *const widget : std::initializer_list{ ui()->authLabel, ui()->userNameLabel, ui()->passwordLabel, ui()->timeoutLabel, ui()->longPollingLabel, ui()->pollLabel, ui()->pauseOnMeteredConnectionCheckBox }) { ui()->formLayout->setRowVisible(widget, show); } #else for (auto *const widget : std::initializer_list{ ui()->authLabel, ui()->authCheckBox, ui()->userNameLabel, ui()->userNameLineEdit, ui()->passwordLabel, ui()->passwordLineEdit, ui()->timeoutLabel, ui()->timeoutSpinBox, ui()->longPollingLabel, ui()->longPollingSpinBox, ui()->pollLabel, ui()->pollDevStatsLabel, ui()->pollDevStatsSpinBox, ui()->pollErrorsLabel, ui()->pollErrorsSpinBox, ui()->pollTrafficLabel, ui()->pollTrafficSpinBox, ui()->reconnectLabel, ui()->reconnectSpinBox, ui()->pauseOnMeteredConnectionCheckBox }) { widget->setVisible(show); } #endif } bool ConnectionOptionPage::apply() { if (!cacheCurrentSettings(true)) { return false; } values().connection.primary = m_primarySettings; values().connection.secondary = m_secondarySettings; return true; } void ConnectionOptionPage::reset() { m_primarySettings = values().connection.primary; m_secondarySettings = values().connection.secondary; m_currentIndex = -1; QStringList itemTexts; itemTexts.reserve(1 + static_cast(m_secondarySettings.size())); itemTexts << m_primarySettings.label; for (const SyncthingConnectionSettings &settings : m_secondarySettings) { itemTexts << settings.label; } ui()->selectionComboBox->clear(); ui()->selectionComboBox->addItems(itemTexts); ui()->selectionComboBox->setCurrentIndex(0); updateConnectionStatus(); } void ConnectionOptionPage::applyAndReconnect() { apply(); if (m_connection) { m_connection->reconnect((m_currentIndex == 0 ? m_primarySettings : m_secondarySettings[static_cast(m_currentIndex - 1)])); } } // NotificationsOptionPage NotificationsOptionPage::NotificationsOptionPage(GuiType guiType, QWidget *parentWidget) : NotificationsOptionPageBase(parentWidget) , m_guiType(guiType) { } NotificationsOptionPage::~NotificationsOptionPage() { } QWidget *NotificationsOptionPage::setupWidget() { auto *const widget = NotificationsOptionPageBase::setupWidget(); switch (m_guiType) { case GuiType::TrayWidget: break; case GuiType::Plasmoid: ui()->apiGroupBox->setHidden(true); break; } return widget; } bool NotificationsOptionPage::apply() { bool ok = true; auto &settings(values()); auto ¬ifyOn(settings.notifyOn); notifyOn.disconnect = ui()->notifyOnDisconnectCheckBox->isChecked(); notifyOn.internalErrors = ui()->notifyOnErrorsCheckBox->isChecked(); notifyOn.launcherErrors = ui()->notifyOnLauncherErrorsCheckBox->isChecked(); notifyOn.localSyncComplete = ui()->notifyOnLocalSyncCompleteCheckBox->isChecked(); notifyOn.remoteSyncComplete = ui()->notifyOnRemoteSyncCompleteCheckBox->isChecked(); notifyOn.syncthingErrors = ui()->showSyncthingNotificationsCheckBox->isChecked(); notifyOn.newDeviceConnects = ui()->notifyOnNewDevConnectsCheckBox->isChecked(); notifyOn.newDirectoryShared = ui()->notifyOnNewDirSharedCheckBox->isChecked(); #ifdef QT_UTILITIES_SUPPORT_DBUS_NOTIFICATIONS if ((settings.dbusNotifications = ui()->dbusRadioButton->isChecked()) && !DBusNotification::isAvailable()) { errors() << QCoreApplication::translate( "QtGui::NotificationsOptionPage", "Configured to use D-Bus notifications but D-Bus notification daemon seems unavailabe."); ok = false; } #endif values().ignoreInavailabilityAfterStart = static_cast(ui()->ignoreInavailabilityAfterStartSpinBox->value()); return ok; } void NotificationsOptionPage::reset() { const auto ¬ifyOn = values().notifyOn; ui()->notifyOnDisconnectCheckBox->setChecked(notifyOn.disconnect); ui()->notifyOnErrorsCheckBox->setChecked(notifyOn.internalErrors); ui()->notifyOnLauncherErrorsCheckBox->setChecked(notifyOn.launcherErrors); ui()->notifyOnLocalSyncCompleteCheckBox->setChecked(notifyOn.localSyncComplete); ui()->notifyOnRemoteSyncCompleteCheckBox->setChecked(notifyOn.remoteSyncComplete); ui()->showSyncthingNotificationsCheckBox->setChecked(notifyOn.syncthingErrors); ui()->notifyOnNewDevConnectsCheckBox->setChecked(notifyOn.newDeviceConnects); ui()->notifyOnNewDirSharedCheckBox->setChecked(notifyOn.newDirectoryShared); #ifdef QT_UTILITIES_SUPPORT_DBUS_NOTIFICATIONS (values().dbusNotifications ? ui()->dbusRadioButton : ui()->qtRadioButton)->setChecked(true); #else ui()->dbusRadioButton->setEnabled(false); ui()->qtRadioButton->setChecked(true); #endif ui()->ignoreInavailabilityAfterStartSpinBox->setValue(static_cast(values().ignoreInavailabilityAfterStart)); } // AppearanceOptionPage AppearanceOptionPage::AppearanceOptionPage(QWidget *parentWidget) : AppearanceOptionPageBase(parentWidget) { } AppearanceOptionPage::~AppearanceOptionPage() { } bool AppearanceOptionPage::apply() { auto &v = values(); auto &settings = v.appearance; settings.windowType = ui()->windowTypeComboBox->currentIndex(); settings.trayMenuSize.setWidth(ui()->widthSpinBox->value()); settings.trayMenuSize.setHeight(ui()->heightSpinBox->value()); settings.showTraffic = ui()->showTrafficCheckBox->isChecked(); settings.showTabTexts = ui()->showTabTextsCheckBox->isChecked(); v.icons.preferIconsFromTheme = ui()->preferIconsFromThemeCheckBox->isChecked(); int style; switch (ui()->frameShapeComboBox->currentIndex()) { case 0: style = QFrame::NoFrame; break; case 1: style = QFrame::Box; break; case 2: style = QFrame::Panel; break; default: style = QFrame::StyledPanel; } switch (ui()->frameShadowComboBox->currentIndex()) { case 0: style |= QFrame::Plain; break; case 1: style |= QFrame::Raised; break; default: style |= QFrame::Sunken; } settings.frameStyle = style; settings.tabPosition = ui()->tabPosComboBox->currentIndex(); settings.positioning.useCursorPosition = ui()->useCursorPosCheckBox->isChecked(); settings.positioning.useAssumedIconPosition = ui()->assumeIconPosCheckBox->isChecked(); settings.positioning.assumedIconPosition = QPoint(ui()->xPosSpinBox->value(), ui()->yPosSpinBox->value()); return true; } void AppearanceOptionPage::resetPositioningSettings() { const auto &v = values(); const auto &settings = v.appearance; ui()->widthSpinBox->setValue(settings.trayMenuSize.width()); ui()->heightSpinBox->setValue(settings.trayMenuSize.height()); ui()->xPosSpinBox->setValue(settings.positioning.assumedIconPosition.x()); ui()->yPosSpinBox->setValue(settings.positioning.assumedIconPosition.y()); } void AppearanceOptionPage::reset() { const auto &v = values(); const auto &settings = v.appearance; resetPositioningSettings(); ui()->windowTypeComboBox->setCurrentIndex(settings.windowType); ui()->showTrafficCheckBox->setChecked(settings.showTraffic); ui()->showTabTextsCheckBox->setChecked(settings.showTabTexts); ui()->preferIconsFromThemeCheckBox->setChecked(v.icons.preferIconsFromTheme); int index; switch (settings.frameStyle & QFrame::Shape_Mask) { case QFrame::NoFrame: index = 0; break; case QFrame::Box: index = 1; break; case QFrame::Panel: index = 2; break; default: index = 3; } ui()->frameShapeComboBox->setCurrentIndex(index); switch (settings.frameStyle & QFrame::Shadow_Mask) { case QFrame::Plain: index = 0; break; case QFrame::Raised: index = 1; break; default: index = 2; } ui()->frameShadowComboBox->setCurrentIndex(index); ui()->tabPosComboBox->setCurrentIndex(settings.tabPosition); ui()->useCursorPosCheckBox->setChecked(settings.positioning.useCursorPosition); ui()->assumeIconPosCheckBox->setChecked(settings.positioning.useAssumedIconPosition); } // IconsOptionPage IconsOptionPage::IconsOptionPage(Context context, QWidget *parentWidget) : IconsOptionPageBase(parentWidget) , m_context(context) { } IconsOptionPage::~IconsOptionPage() { } QWidget *IconsOptionPage::setupWidget() { auto *const widget = IconsOptionPageBase::setupWidget(); // set context-specific elements switch (m_context) { case Context::Combined: ui()->contextLabel->hide(); ui()->contextCheckBox->hide(); break; case Context::UI: widget->setWindowTitle(QCoreApplication::translate("QtGui::IconsOptionPageBase", "UI icons")); ui()->contextLabel->setText( QCoreApplication::translate("QtGui::IconsOptionPageBase", "These icon settings are used within Syncthing Tray's UI.")); ui()->contextCheckBox->hide(); break; case Context::System: widget->setWindowTitle(QCoreApplication::translate("QtGui::IconsOptionPageBase", "System icons")); ui()->contextLabel->setText(QCoreApplication::translate( "QtGui::IconsOptionPageBase", "These icon settings are used for the system tray icon and the notifications.")); ui()->contextCheckBox->setText(QCoreApplication::translate("QtGui::IconsOptionPageBase", "Use same settings as for UI icons")); break; } // allow changing stroke thickness QObject::connect(ui()->thickStrokeWidthCheckBox, &QCheckBox::toggled, widget, [this](bool thick) { m_settings.strokeWidth = thick ? StatusIconStrokeWidth::Thick : StatusIconStrokeWidth::Normal; }); // populate form for status icon colors auto *const gridLayout = ui()->gridLayout; auto *const statusIconsGroupBox = ui()->statusIconsGroupBox; int index = 0; for (auto &colorMapping : m_settings.colorMapping()) { // populate widgets array auto &widgetsForColor = m_widgets[index++] = { { new ColorButton(statusIconsGroupBox), new ColorButton(statusIconsGroupBox), new ColorButton(statusIconsGroupBox), }, new QLabel(statusIconsGroupBox), &colorMapping.setting, colorMapping.defaultEmblem, }; widgetsForColor.previewLabel->setMaximumSize(QSize(32, 32)); // add label for color name gridLayout->addWidget(new QLabel(colorMapping.colorName, statusIconsGroupBox), index, 0, Qt::AlignRight | Qt::AlignVCenter); // setup preview gridLayout->addWidget(widgetsForColor.previewLabel, index, 4, Qt::AlignCenter); const auto updatePreview = [this, &widgetsForColor] { widgetsForColor.previewLabel->setPixmap(renderSvgImage(makeSyncthingIcon( StatusIconColorSet{ widgetsForColor.colorButtons[0]->color(), widgetsForColor.colorButtons[1]->color(), widgetsForColor.colorButtons[2]->color(), }, widgetsForColor.statusEmblem, m_settings.strokeWidth), widgetsForColor.previewLabel->maximumSize())); }; for (const auto &colorButton : widgetsForColor.colorButtons) { QObject::connect(colorButton, &ColorButton::colorChanged, widget, updatePreview); } QObject::connect(ui()->thickStrokeWidthCheckBox, &QCheckBox::toggled, widget, updatePreview); // setup color buttons widgetsForColor.colorButtons[0]->setColor(colorMapping.setting.backgroundStart); widgetsForColor.colorButtons[1]->setColor(colorMapping.setting.backgroundEnd); widgetsForColor.colorButtons[2]->setColor(colorMapping.setting.foreground); gridLayout->addWidget(widgetsForColor.colorButtons[0], index, 1); gridLayout->addWidget(widgetsForColor.colorButtons[1], index, 2); gridLayout->addWidget(widgetsForColor.colorButtons[2], index, 3); if (index >= StatusIconSettings::distinguishableColorCount) { break; } } // setup presets menu auto *const presetsMenu = new QMenu(widget); presetsMenu->addAction(QCoreApplication::translate("QtGui::IconsOptionPageBase", "Colorful background with gradient (default)"), widget, [this] { m_settings = Data::StatusIconSettings(); update(); }); presetsMenu->addAction( QCoreApplication::translate("QtGui::IconsOptionPageBase", "Transparent background and dark foreground (for bright themes)"), widget, [this] { m_settings = Data::StatusIconSettings(Data::StatusIconSettings::BrightTheme{}); update(); }); presetsMenu->addAction( QCoreApplication::translate("QtGui::IconsOptionPageBase", "Transparent background and bright foreground (for dark themes)"), widget, [this] { m_settings = Data::StatusIconSettings(Data::StatusIconSettings::DarkTheme{}); update(); }); // setup additional buttons ui()->restoreDefaultsPushButton->setMenu(presetsMenu); QObject::connect(ui()->restorePreviousPushButton, &QPushButton::clicked, widget, [this] { reset(); }); // setup slider QObject::connect(ui()->renderingSizeSlider, &QSlider::valueChanged, widget, [this](int value) { m_settings.renderSize = QSize(value, value); auto *const label = ui()->renderingSizeLabel; if (const auto scaleFactor = label->devicePixelRatioF(); scaleFactor == 1.0) { label->setText(QString::number(value) + QStringLiteral(" px")); } else { label->setText(QCoreApplication::translate("QtGui::IconsOptionPageBase", "%1 px (scaled to %2 px)") .arg(QString::number(value), QString::number(static_cast(value) * scaleFactor, 'f', 0))); } }); return widget; } bool IconsOptionPage::apply() { for (auto &widgetsForColor : m_widgets) { *widgetsForColor.setting = StatusIconColorSet{ widgetsForColor.colorButtons[0]->color(), widgetsForColor.colorButtons[1]->color(), widgetsForColor.colorButtons[2]->color(), }; } auto &iconSettings = values().icons; switch (m_context) { case Context::Combined: case Context::UI: iconSettings.status = m_settings; break; case Context::System: iconSettings.tray = m_settings; iconSettings.distinguishTrayIcons = !ui()->contextCheckBox->isChecked(); break; } return true; } void IconsOptionPage::update() { ui()->renderingSizeSlider->setValue(std::max(m_settings.renderSize.width(), m_settings.renderSize.height())); ui()->thickStrokeWidthCheckBox->setChecked(m_settings.strokeWidth == StatusIconStrokeWidth::Thick); for (auto &widgetsForColor : m_widgets) { widgetsForColor.colorButtons[0]->setColor(widgetsForColor.setting->backgroundStart); widgetsForColor.colorButtons[1]->setColor(widgetsForColor.setting->backgroundEnd); widgetsForColor.colorButtons[2]->setColor(widgetsForColor.setting->foreground); } } void IconsOptionPage::reset() { const auto &iconSettings = values().icons; switch (m_context) { case Context::Combined: case Context::UI: m_settings = iconSettings.status; break; case Context::System: m_settings = iconSettings.tray; ui()->contextCheckBox->setChecked(!iconSettings.distinguishTrayIcons); break; } update(); } // AutostartOptionPage AutostartOptionPage::AutostartOptionPage(QWidget *parentWidget) : AutostartOptionPageBase(parentWidget) { } AutostartOptionPage::~AutostartOptionPage() { } QWidget *AutostartOptionPage::setupWidget() { auto *widget = AutostartOptionPageBase::setupWidget(); auto *style = QApplication::style(); ui()->infoIconLabel->setPixmap( style->standardIcon(QStyle::SP_MessageBoxInformation, nullptr, ui()->infoIconLabel).pixmap(ui()->infoIconLabel->size())); ui()->pathWarningIconLabel->setPixmap( style->standardIcon(QStyle::SP_MessageBoxWarning, nullptr, ui()->pathWarningIconLabel).pixmap(ui()->pathWarningIconLabel->size())); QObject::connect(ui()->deleteExistingEntryPushButton, &QPushButton::clicked, widget, [this] { setAutostartPath(QString()); reset(); }); #if defined(PLATFORM_LINUX) && !defined(PLATFORM_ANDROID) ui()->platformNoteLabel->setText(QCoreApplication::translate("QtGui::AutostartOptionPage", "This is achieved by adding a *.desktop file under ~/.config/autostart so the setting only affects the current user.")); #elif defined(PLATFORM_WINDOWS) ui()->platformNoteLabel->setText(QCoreApplication::translate("QtGui::AutostartOptionPage", "This is achieved by adding a registry key under HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run so the setting " "only affects the current user. Note that the startup entry is invalidated when moving syncthingtray.exe.")); #elif defined(PLATFORM_MAC) ui()->platformNoteLabel->setText(QCoreApplication::translate("QtGui::AutostartOptionPage", "This is achieved by adding a *.plist file under ~/Library/LaunchAgents so the setting only affects the current user.")); #else ui()->platformNoteLabel->setText( QCoreApplication::translate("QtGui::AutostartOptionPage", "This feature has not been implemented for your platform (yet).")); m_unsupported = true; ui()->pathWidget->setVisible(false); ui()->autostartCheckBox->setEnabled(false); #endif return widget; } /*! * \brief Returns the currently configured autostart path or an empty string if autostart is disabled; returns std::nullopt when the * path cannot be determined. * \remarks * - At this point the path cannot be determined on MacOS. * - Use isAutostartEnabled() if you just need to know whether autostart is generally enabled as this is a simpler check. This * function is also generally useful as a fallback. */ std::optional configuredAutostartPath() { if (qEnvironmentVariableIsSet(PROJECT_VARNAME_UPPER "_AUTOSTART_PATH_MOCK")) { auto mockedPath = qEnvironmentVariable(PROJECT_VARNAME_UPPER "_AUTOSTART_PATH_MOCK"); return mockedPath.isEmpty() ? std::nullopt : std::make_optional(mockedPath); } #if defined(PLATFORM_LINUX) && !defined(Q_OS_ANDROID) auto desktopFile = QFile(QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("autostart/" PROJECT_NAME ".desktop"))); // check whether the file can be opened and whether it is enabled but prevent reading large files if (!desktopFile.open(QFile::ReadOnly)) { return QString(); } if (desktopFile.size() > (5 * 1024)) { return std::nullopt; } const auto data = QString::fromUtf8(desktopFile.readAll()); if (data.contains(QLatin1String("Hidden=true"))) { return QString(); } static const auto regex = QRegularExpression(QStringLiteral("Exec=(\"([^\"\\n]*)\"|([^\\s\\n]*))")); const auto match = regex.match(data); auto captured = match.captured(2); if (captured.isNull()) { captured = match.captured(3); } return captured.isNull() ? std::nullopt : std::make_optional(captured); #elif defined(PLATFORM_WINDOWS) return QSettings(QStringLiteral("HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"), QSettings::NativeFormat) .value(QStringLiteral(PROJECT_NAME)) .toString(); #else return std::nullopt; #endif } /*! * \brief Returns the autostart path that will be configured by invoking setAutostartEnabled(true). * \remarks * - Only implemented under Linux/Windows/Mac. Always returns false on other platforms. * - Does not check whether the startup entry is functional (eg. the specified path is still valid and points to the * currently running instance of the application). */ QString supposedAutostartPath() { #if defined(PLATFORM_LINUX) && !defined(Q_OS_ANDROID) #ifndef SYNCTHINGWIDGETS_AUTOSTART_EXEC_PATH #define SYNCTHINGWIDGETS_AUTOSTART_EXEC_PATH QCoreApplication::applicationFilePath() #endif return qEnvironmentVariable("APPIMAGE", SYNCTHINGWIDGETS_AUTOSTART_EXEC_PATH); #elif defined(PLATFORM_WINDOWS) return QCoreApplication::applicationFilePath().replace(QChar('/'), QChar('\\')); #else return QCoreApplication::applicationFilePath(); #endif } /*! * \brief Sets the \a path of the application's autostart entry or removes the entry if \a path is empty. */ bool setAutostartPath(const QString &path) { if (qEnvironmentVariableIsSet(PROJECT_VARNAME_UPPER "_AUTOSTART_PATH_MOCK")) { return qputenv(PROJECT_VARNAME_UPPER "_AUTOSTART_PATH_MOCK", path.toLocal8Bit()); } #if defined(PLATFORM_LINUX) && !defined(Q_OS_ANDROID) const auto configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation); if (configPath.isEmpty()) { return false; } if (!path.isEmpty() && !QDir().mkpath(configPath + QStringLiteral("/autostart"))) { return false; } auto desktopFile = QFile(configPath + QStringLiteral("/autostart/" PROJECT_NAME ".desktop")); if (!path.isEmpty()) { if (!desktopFile.open(QFile::WriteOnly | QFile::Truncate)) { return false; } desktopFile.write("[Desktop Entry]\n" "Name=" APP_NAME "\n" "Exec=\""); desktopFile.write(path.toUtf8()); desktopFile.write("\" qt-widgets-gui --single-instance --wait\nComment=" APP_DESCRIPTION "\n" "Icon=" PROJECT_NAME "\n" "Type=Application\n" "Terminal=false\n" "X-GNOME-Autostart-Delay=0\n" "X-GNOME-Autostart-enabled=true\n" "X-LXQt-Need-Tray=true\n"); return desktopFile.error() == QFile::NoError && desktopFile.flush(); } else { return !desktopFile.exists() || desktopFile.remove(); } #elif defined(PLATFORM_WINDOWS) auto settings = QSettings(QStringLiteral("HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"), QSettings::NativeFormat); if (!path.isEmpty()) { settings.setValue(QStringLiteral(PROJECT_NAME), path); } else { settings.remove(QStringLiteral(PROJECT_NAME)); } settings.sync(); return true; #elif defined(PLATFORM_MAC) const auto libraryPath = QDir::home().filePath(QStringLiteral("Library")); if (!path.isEmpty() && !QDir().mkpath(libraryPath + QStringLiteral("/LaunchAgents"))) { return false; } auto launchdPlistFile = QFile(libraryPath + QStringLiteral("/LaunchAgents/" PROJECT_NAME ".plist")); if (!path.isEmpty()) { if (!launchdPlistFile.open(QFile::WriteOnly | QFile::Truncate)) { return false; } launchdPlistFile.write("\n" "\n" "\n" " \n" " Label\n" " " PROJECT_NAME "\n" " ProgramArguments\n" " \n" " "); launchdPlistFile.write(path.toUtf8()); launchdPlistFile.write("\n" " \n" " KeepAlive\n" " \n" " \n" "\n"); return launchdPlistFile.error() == QFile::NoError && launchdPlistFile.flush(); } else { return !launchdPlistFile.exists() || launchdPlistFile.remove(); } #endif } /*! * \brief Returns whether the application is launched on startup. * \remarks * - Only implemented under Linux/Windows/Mac. Always returns false on other platforms. * - Does not check whether the startup entry is functional (e.g. whether the specified path is still valid and points to the * currently running instance of the application). */ bool isAutostartEnabled() { #if defined(PLATFORM_LINUX) && !defined(Q_OS_ANDROID) auto desktopFile = QFile(QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("autostart/" PROJECT_NAME ".desktop"))); // check whether the file can be opened and whether it is enabled but prevent reading large files if (desktopFile.open(QFile::ReadOnly) && (desktopFile.size() > (5 * 1024) || !desktopFile.readAll().contains("Hidden=true"))) { return true; } return false; #elif defined(PLATFORM_WINDOWS) return QSettings(QStringLiteral("HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"), QSettings::NativeFormat) .contains(QStringLiteral(PROJECT_NAME)); #elif defined(PLATFORM_MAC) return QFileInfo(QDir::home(), QStringLiteral("Library/LaunchAgents/" PROJECT_NAME ".plist")).isReadable(); #else return false; #endif } /*! * \brief Sets whether the application is launched on startup. * \remarks * - Only implemented under Linux/Windows/Mac. Does nothing on other platforms. * - If a startup entry already exists and \a enabled is true, this function will not touch the existing entry - even if it points * to another application. Delete the existing entry first if it is no longer wanted or set \a force to true. * - Note that is might not be possible to determine the currently configured path. If the path cannot be determined an * existing autostart entry will always be overridden (despite \a force being false). * - If no startup entry could be detected via isAutostartEnabled() and \a enabled is false this function doesn't touch anything. */ bool setAutostartEnabled(bool enabled, bool force) { const auto configuredPath = configuredAutostartPath(); if (!(configuredPath.has_value() ? !configuredPath.value().isEmpty() : isAutostartEnabled()) && !enabled) { return true; } const auto supposedPath = supposedAutostartPath(); if (!force && enabled && configuredPath.has_value() && !configuredPath.value().isEmpty() && configuredPath.value() != supposedPath) { return true; // don't touch existing entry } return setAutostartPath(enabled ? supposedPath : QString()); } bool AutostartOptionPage::apply() { if (m_unsupported) { return true; // don't treat this as an error } if (!setAutostartEnabled(ui()->autostartCheckBox->isChecked())) { errors() << QCoreApplication::translate("QtGui::AutostartOptionPage", "unable to modify startup entry"); return false; } return true; } void AutostartOptionPage::reset() { if (!hasBeenShown() || m_unsupported) { return; } const auto configuredPath = configuredAutostartPath(); if (!configuredPath.has_value()) { // we can't determine the currently configured path ui()->pathWidget->setVisible(false); ui()->autostartCheckBox->setEnabled(true); ui()->autostartCheckBox->setChecked(isAutostartEnabled()); return; } const auto autostartEnabled = !configuredPath.value().isEmpty(); ui()->autostartCheckBox->setChecked(autostartEnabled); if (!autostartEnabled) { ui()->pathWidget->setVisible(false); ui()->autostartCheckBox->setEnabled(true); return; } const auto supposedPath = supposedAutostartPath(); const auto pathMismatch = configuredPath != supposedPath; ui()->pathWidget->setVisible(pathMismatch); ui()->autostartCheckBox->setEnabled(!pathMismatch); if (pathMismatch) { ui()->pathWarningLabel->setText(QCoreApplication::translate("QtGui::AutostartOptionPage", "There is already an autostart entry for \"%1\". " "It will not be overridden when applying changes unless you delete it first.") .arg(configuredPath.value())); } } // LauncherOptionPage LauncherOptionPage::LauncherOptionPage(QWidget *parentWidget) : QObject(parentWidget) , LauncherOptionPageBase(parentWidget) , m_process(nullptr) , m_launcher(SyncthingLauncher::mainInstance()) , m_kill(false) { } LauncherOptionPage::LauncherOptionPage(const QString &tool, const QString &toolName, const QString &windowTitle, QWidget *parentWidget) : QObject(parentWidget) , LauncherOptionPageBase(parentWidget) , m_process(&Launcher::toolProcess(tool)) , m_launcher(nullptr) , m_restoreArgsAction(nullptr) , m_kill(false) , m_tool(tool) , m_toolName(toolName) , m_windowTitle(windowTitle) { } LauncherOptionPage::~LauncherOptionPage() { } QWidget *LauncherOptionPage::setupWidget() { auto *const widget = LauncherOptionPageBase::setupWidget(); // adjust labels to use name of additional tool instead of "Syncthing" const auto isSyncthing = m_tool.isEmpty(); if (!isSyncthing) { widget->setWindowTitle(m_windowTitle.isEmpty() ? tr("%1-launcher").arg(m_tool) : m_windowTitle); ui()->enabledCheckBox->setText(tr("Launch %1 when starting the tray icon").arg(m_toolName.isEmpty() ? m_tool : m_toolName)); auto toolNameStartingSentence = m_toolName.isEmpty() ? m_tool : m_toolName; toolNameStartingSentence[0] = toolNameStartingSentence[0].toUpper(); ui()->syncthingPathLabel->setText(tr("%1 executable").arg(toolNameStartingSentence)); ui()->logLabel->setText(tr("%1 log (interleaved stdout/stderr)").arg(toolNameStartingSentence)); // hide "consider for reconnect" and "show start/stop button on tray" checkboxes for tools ui()->considerForReconnectCheckBox->setVisible(false); ui()->showButtonCheckBox->setVisible(false); ui()->stopOnMeteredCheckBox->setVisible(false); } // hide libsyncthing-controls by default (as the checkbox is unchecked by default) for (auto *const lstWidget : std::initializer_list{ ui()->configDirLabel, ui()->configDirPathSelection, ui()->dataDirLabel, ui()->dataDirPathSelection, ui()->logLevelLabel, ui()->logLevelComboBox }) { lstWidget->setVisible(false); } // add "restore to defaults" action for Syncthing arguments if (isSyncthing) { m_restoreArgsAction = new QAction(ui()->argumentsLineEdit); m_restoreArgsAction->setText(tr("Restore default")); m_restoreArgsAction->setIcon( QIcon::fromTheme(QStringLiteral("edit-undo"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/edit-paste.svg")))); connect(m_restoreArgsAction, &QAction::triggered, this, &LauncherOptionPage::restoreDefaultArguments); ui()->argumentsLineEdit->addCustomAction(m_restoreArgsAction); m_syncthingDownloadAction = new QAction(ui()->syncthingPathSelection); m_syncthingDownloadAction->setText(tr("Show Syncthing releases/downloads")); m_syncthingDownloadAction->setIcon( QIcon::fromTheme(QStringLiteral("download"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/download.svg")))); connect(m_syncthingDownloadAction, &QAction::triggered, [] { QDesktopServices::openUrl(QUrl(QStringLiteral("https://github.com/syncthing/syncthing/releases"))); }); ui()->syncthingPathSelection->lineEdit()->addCustomAction(m_syncthingDownloadAction); ui()->configDirPathSelection->provideCustomFileMode(QFileDialog::Directory); ui()->dataDirPathSelection->provideCustomFileMode(QFileDialog::Directory); } // setup other widgets ui()->syncthingPathSelection->provideCustomFileMode(QFileDialog::ExistingFile); ui()->logTextEdit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); const auto running = isRunning(); ui()->launchNowPushButton->setHidden(running); ui()->stopPushButton->setHidden(!running); ui()->useBuiltInVersionCheckBox->setVisible(isSyncthing && SyncthingLauncher::isLibSyncthingAvailable()); if (isSyncthing) { ui()->useBuiltInVersionCheckBox->setToolTip(SyncthingLauncher::libSyncthingVersionInfo()); } // setup process/launcher if (m_process) { connect(m_process, &SyncthingProcess::readyRead, this, &LauncherOptionPage::handleSyncthingReadyRead, Qt::QueuedConnection); connect(m_process, static_cast(&SyncthingProcess::finished), this, &LauncherOptionPage::handleSyncthingExited, Qt::QueuedConnection); connect(m_process, &SyncthingProcess::errorOccurred, this, &LauncherOptionPage::handleSyncthingError, Qt::QueuedConnection); configureMeteredCheckbox(ui()->stopOnMeteredCheckBox, std::nullopt); } else if (m_launcher) { connect(m_launcher, &SyncthingLauncher::runningChanged, this, &LauncherOptionPage::handleSyncthingLaunched); connect(m_launcher, &SyncthingLauncher::outputAvailable, this, &LauncherOptionPage::handleSyncthingOutputAvailable, Qt::QueuedConnection); connect(m_launcher, &SyncthingLauncher::exited, this, &LauncherOptionPage::handleSyncthingExited, Qt::QueuedConnection); connect(m_launcher, &SyncthingLauncher::errorOccurred, this, &LauncherOptionPage::handleSyncthingError, Qt::QueuedConnection); #ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING connect(ui()->logLevelComboBox, static_cast(&QComboBox::currentIndexChanged), this, &LauncherOptionPage::updateLibSyncthingLogLevel); #endif configureMeteredCheckbox(ui()->stopOnMeteredCheckBox, m_launcher->isNetworkConnectionMetered()); connect(m_launcher, &SyncthingLauncher::networkConnectionMeteredChanged, this, std::bind(&configureMeteredCheckbox, ui()->stopOnMeteredCheckBox, std::placeholders::_1)); m_launcher->setEmittingOutput(true); } connect(ui()->launchNowPushButton, &QPushButton::clicked, this, &LauncherOptionPage::launch); connect(ui()->stopPushButton, &QPushButton::clicked, this, &LauncherOptionPage::stop); return widget; } bool LauncherOptionPage::apply() { auto &settings = values().launcher; if (m_tool.isEmpty()) { settings.autostartEnabled = ui()->enabledCheckBox->isChecked(); settings.useLibSyncthing = ui()->useBuiltInVersionCheckBox->isChecked(); #ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING settings.libSyncthing.configDir = ui()->configDirPathSelection->lineEdit()->text(); settings.libSyncthing.dataDir = ui()->dataDirPathSelection->lineEdit()->text(); settings.libSyncthing.logLevel = static_cast(ui()->logLevelComboBox->currentIndex()); #endif settings.syncthingPath = ui()->syncthingPathSelection->lineEdit()->text(); settings.syncthingArgs = ui()->argumentsLineEdit->text(); settings.considerForReconnect = ui()->considerForReconnectCheckBox->isChecked(); settings.showButton = ui()->showButtonCheckBox->isChecked(); settings.stopOnMeteredConnection = ui()->stopOnMeteredCheckBox->isChecked(); if (m_launcher) { m_launcher->setStoppingOnMeteredConnection(settings.stopOnMeteredConnection); } } else { ToolParameter ¶ms = settings.tools[m_tool]; params.autostart = ui()->enabledCheckBox->isChecked(); params.path = ui()->syncthingPathSelection->lineEdit()->text(); params.args = ui()->argumentsLineEdit->text(); } return true; } void LauncherOptionPage::reset() { const auto &settings = values().launcher; if (m_tool.isEmpty()) { ui()->enabledCheckBox->setChecked(settings.autostartEnabled); ui()->useBuiltInVersionCheckBox->setChecked(settings.useLibSyncthing); ui()->useBuiltInVersionCheckBox->setVisible(settings.useLibSyncthing || SyncthingLauncher::isLibSyncthingAvailable()); #ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING ui()->configDirPathSelection->lineEdit()->setText(settings.libSyncthing.configDir); ui()->dataDirPathSelection->lineEdit()->setText(settings.libSyncthing.dataDir); ui()->logLevelComboBox->setCurrentIndex(static_cast(settings.libSyncthing.logLevel)); #endif ui()->syncthingPathSelection->lineEdit()->setText(settings.syncthingPath); ui()->argumentsLineEdit->setText(settings.syncthingArgs); ui()->considerForReconnectCheckBox->setChecked(settings.considerForReconnect); ui()->showButtonCheckBox->setChecked(settings.showButton); ui()->stopOnMeteredCheckBox->setChecked(settings.stopOnMeteredConnection); } else { const ToolParameter params = settings.tools.value(m_tool); ui()->useBuiltInVersionCheckBox->setChecked(false); ui()->useBuiltInVersionCheckBox->setVisible(false); ui()->enabledCheckBox->setChecked(params.autostart); ui()->syncthingPathSelection->lineEdit()->setText(params.path); ui()->argumentsLineEdit->setText(params.args); } } void LauncherOptionPage::handleSyncthingLaunched(bool running) { if (!running) { return; // Syncthing being stopped is handled elsewhere } ui()->launchNowPushButton->hide(); ui()->stopPushButton->show(); ui()->stopPushButton->setText(tr("Stop launched instance")); m_kill = false; } void LauncherOptionPage::handleSyncthingReadyRead() { handleSyncthingOutputAvailable(m_process->readAll()); } void LauncherOptionPage::handleSyncthingOutputAvailable(const QByteArray &output) { if (!hasBeenShown()) { return; } QTextCursor cursor(ui()->logTextEdit->textCursor()); cursor.movePosition(QTextCursor::End); cursor.insertText(QString::fromUtf8(output)); if (ui()->ensureCursorVisibleCheckBox->isChecked()) { ui()->logTextEdit->moveCursor(QTextCursor::End); ui()->logTextEdit->ensureCursorVisible(); } } void LauncherOptionPage::handleSyncthingExited(int exitCode, QProcess::ExitStatus exitStatus) { if (!hasBeenShown()) { return; } QTextCursor cursor(ui()->logTextEdit->textCursor()); cursor.movePosition(QTextCursor::End); cursor.insertBlock(); switch (exitStatus) { case QProcess::NormalExit: cursor.insertText(tr("%1 exited with exit code %2").arg(m_tool.isEmpty() ? QStringLiteral("Syncthing") : m_tool, QString::number(exitCode))); break; case QProcess::CrashExit: cursor.insertText(tr("%1 crashed with exit code %2").arg(m_tool.isEmpty() ? QStringLiteral("Syncthing") : m_tool, QString::number(exitCode))); break; } cursor.insertBlock(); if (ui()->ensureCursorVisibleCheckBox->isChecked()) { ui()->logTextEdit->moveCursor(QTextCursor::End); ui()->logTextEdit->ensureCursorVisible(); } ui()->stopPushButton->hide(); ui()->launchNowPushButton->show(); } void LauncherOptionPage::handleSyncthingError(QProcess::ProcessError error) { if (!hasBeenShown()) { return; } QTextCursor cursor(ui()->logTextEdit->textCursor()); cursor.movePosition(QTextCursor::End); cursor.insertBlock(); auto errorString = QString(); if (m_launcher) { errorString = m_launcher->errorString(); } else if (m_process) { errorString = m_process->errorString(); } if (errorString.isEmpty()) { switch (error) { case QProcess::FailedToStart: errorString = tr("failed to start (e.g. executable does not exist or not permission error)"); break; case QProcess::Crashed: errorString = tr("process crashed"); break; case QProcess::Timedout: errorString = tr("timeout error"); break; case QProcess::ReadError: errorString = tr("read error"); break; case QProcess::WriteError: errorString = tr("write error"); break; default: errorString = tr("unknown process error"); } } cursor.insertText(tr("An error occurred when running %1: %2").arg(m_tool.isEmpty() ? QStringLiteral("Syncthing") : m_tool, errorString)); cursor.insertBlock(); if ((m_launcher && !m_launcher->isRunning()) || (m_process && !m_process->isRunning())) { ui()->stopPushButton->hide(); ui()->launchNowPushButton->show(); } } bool LauncherOptionPage::isRunning() const { return (m_process && m_process->isRunning()) || (m_launcher && m_launcher->isRunning()); } void LauncherOptionPage::launch() { if (!hasBeenShown()) { return; } apply(); if (isRunning()) { return; } const auto &launcherSettings(values().launcher); if (m_tool.isEmpty()) { m_launcher->launch(launcherSettings); return; } const auto toolParams(launcherSettings.tools.value(m_tool)); m_process->startSyncthing(toolParams.path, SyncthingProcess::splitArguments(toolParams.args)); handleSyncthingLaunched(true); } #ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING void LauncherOptionPage::updateLibSyncthingLogLevel() { m_launcher->setLibSyncthingLogLevel(static_cast(ui()->logLevelComboBox->currentIndex())); } #endif void LauncherOptionPage::stop() { if (!hasBeenShown()) { return; } if (m_kill) { if (m_process) { m_process->killSyncthing(); } if (m_launcher) { m_launcher->kill(); } } else { ui()->stopPushButton->setText(tr("Kill launched instance")); m_kill = true; if (m_process) { m_process->stopSyncthing(); } if (m_launcher) { m_launcher->terminate(Launcher::connectionForLauncher(m_launcher)); } } } void LauncherOptionPage::restoreDefaultArguments() { static const ::Settings::Launcher defaults; ui()->argumentsLineEdit->setText(defaults.syncthingArgs); } // SystemdOptionPage #ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD SystemdOptionPage::SystemdOptionPage(QWidget *parentWidget) : SystemdOptionPageBase(parentWidget) , m_service(SyncthingService::mainInstance()) { } SystemdOptionPage::~SystemdOptionPage() { QObject::disconnect(m_unitChangedConn); QObject::disconnect(m_descChangedConn); QObject::disconnect(m_statusChangedConn); QObject::disconnect(m_enabledChangedConn); } QWidget *SystemdOptionPage::setupWidget() { auto *const widget = SystemdOptionPageBase::setupWidget(); auto *const refreshAction = new QAction(QCoreApplication::translate("QtGui::SystemdOptionPage", "Reload all unit files"), widget); refreshAction->setIcon( QIcon::fromTheme(QStringLiteral("view-refresh"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/view-refresh.svg")))); ui()->syncthingUnitLineEdit->addCustomAction(refreshAction); if (!m_service) { ui()->stopOnMeteredCheckBox->setHidden(true); return widget; } QObject::connect(refreshAction, &QAction::triggered, m_service, &SyncthingService::reloadAllUnitFiles); 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(ui()->stopOnMeteredCheckBox, &QCheckBox::checkStateChanged, m_service, [s = m_service](Qt::CheckState checkState) { s->setStoppingOnMeteredConnection(checkState == Qt::Checked); }); m_unitChangedConn = QObject::connect(ui()->systemUnitCheckBox, &QCheckBox::clicked, m_service, bind(&SystemdOptionPage::handleSystemUnitChanged, this)); m_descChangedConn = QObject::connect(m_service, &SyncthingService::descriptionChanged, bind(&SystemdOptionPage::handleDescriptionChanged, this, _1)); m_statusChangedConn = QObject::connect(m_service, &SyncthingService::stateChanged, bind(&SystemdOptionPage::handleStatusChanged, this, _1, _2, _3)); m_enabledChangedConn = QObject::connect(m_service, &SyncthingService::unitFileStateChanged, bind(&SystemdOptionPage::handleEnabledChanged, this, _1)); if (const auto *optionPageWidget = qobject_cast(widget)) { QObject::connect(optionPageWidget, &OptionPageWidget::paletteChanged, std::bind(&SystemdOptionPage::updateColors, this)); } configureMeteredCheckbox(ui()->stopOnMeteredCheckBox, m_service->isNetworkConnectionMetered()); QObject::connect(m_service, &SyncthingService::networkConnectionMeteredChanged, std::bind(&configureMeteredCheckbox, ui()->stopOnMeteredCheckBox, std::placeholders::_1)); return widget; } bool SystemdOptionPage::apply() { auto &settings = values(); auto &systemdSettings = settings.systemd; auto &launcherSettings = settings.launcher; systemdSettings.syncthingUnit = ui()->syncthingUnitLineEdit->text(); systemdSettings.systemUnit = ui()->systemUnitCheckBox->isChecked(); systemdSettings.showButton = ui()->showButtonCheckBox->isChecked(); systemdSettings.considerForReconnect = ui()->considerForReconnectCheckBox->isChecked(); systemdSettings.stopOnMeteredConnection = ui()->stopOnMeteredCheckBox->isChecked(); auto result = true; if (systemdSettings.showButton && launcherSettings.showButton) { errors().append(QCoreApplication::translate("QtGui::SystemdOptionPage", "It is not possible to show the start/stop button for the systemd service and the internal launcher at the same time. The systemd " "service precedes.")); result = false; } if (systemdSettings.considerForReconnect && launcherSettings.considerForReconnect) { errors().append(QCoreApplication::translate("QtGui::SystemdOptionPage", "It is not possible to consider the systemd service and the internal launcher for reconnects at the same time. The systemd service " "precedes.")); result = false; } return result; } void SystemdOptionPage::reset() { const auto &settings = values().systemd; ui()->syncthingUnitLineEdit->setText(settings.syncthingUnit); ui()->systemUnitCheckBox->setChecked(settings.systemUnit); ui()->showButtonCheckBox->setChecked(settings.showButton); ui()->considerForReconnectCheckBox->setChecked(settings.considerForReconnect); ui()->stopOnMeteredCheckBox->setChecked(settings.stopOnMeteredConnection); if (!m_service) { return; } handleDescriptionChanged(m_service->description()); handleStatusChanged(m_service->activeState(), m_service->subState(), m_service->activeSince()); handleEnabledChanged(m_service->unitFileState()); } void SystemdOptionPage::handleSystemUnitChanged() { m_service->setScope(ui()->systemUnitCheckBox->isChecked() ? SystemdScope::System : SystemdScope::User); } void SystemdOptionPage::handleDescriptionChanged(const QString &description) { ui()->descriptionValueLabel->setText(description.isEmpty() ? QCoreApplication::translate("QtGui::SystemdOptionPage", "specified unit is either inactive or doesn't exist") : description); } static 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, DateTime activeSince) { m_status.clear(); if (!activeState.isEmpty()) { m_status << activeState; } if (!subState.isEmpty()) { m_status << subState; } const bool isRunning = updateRunningColor(); QString timeStamp; if (isRunning && !activeSince.isNull()) { timeStamp = QLatin1Char('\n') % QCoreApplication::translate("QtGui::SystemdOptionPage", "since ") % QString::fromUtf8(activeSince.toString(DateTimeOutputFormat::DateAndTime).data()); } ui()->statusValueLabel->setText( m_status.isEmpty() ? QCoreApplication::translate("QtGui::SystemdOptionPage", "unknown") : m_status.join(QStringLiteral(" - ")) + timeStamp); ui()->startPushButton->setVisible(!isRunning); ui()->stopPushButton->setVisible(!m_status.isEmpty() && isRunning); } void SystemdOptionPage::handleEnabledChanged(const QString &unitFileState) { const auto isEnabled = updateEnabledColor(); ui()->unitFileStateValueLabel->setText( unitFileState.isEmpty() ? QCoreApplication::translate("QtGui::SystemdOptionPage", "unknown") : unitFileState); ui()->enablePushButton->setVisible(!isEnabled); ui()->disablePushButton->setVisible(!unitFileState.isEmpty() && isEnabled); } bool SystemdOptionPage::updateRunningColor() { const bool isRunning = m_service && m_service->isRunning(); const auto brightColors = isPaletteDark(widget()->palette()); setIndicatorColor(ui()->statusIndicator, m_status.isEmpty() ? Colors::gray(brightColors) : (isRunning ? Colors::green(brightColors) : Colors::red(brightColors))); return isRunning; } bool SystemdOptionPage::updateEnabledColor() { const auto isEnabled = m_service && m_service->isEnabled(); const auto brightColors = isPaletteDark(widget()->palette()); setIndicatorColor(ui()->enabledIndicator, isEnabled ? Colors::green(brightColors) : Colors::gray(brightColors)); return isEnabled; } void SystemdOptionPage::updateColors() { updateRunningColor(); updateEnabledColor(); } #endif // GeneralWebViewOptionPage GeneralWebViewOptionPage::GeneralWebViewOptionPage(QWidget *parentWidget) : GeneralWebViewOptionPageBase(parentWidget) { } GeneralWebViewOptionPage::~GeneralWebViewOptionPage() { } QWidget *GeneralWebViewOptionPage::setupWidget() { auto *const widget = GeneralWebViewOptionPageBase::setupWidget(); auto *const cfgToolButton = ui()->appModeCfgToolButton; #ifdef SYNCTHINGWIDGETS_NO_WEBVIEW ui()->builtinRadioButton->setEnabled(false); #endif const auto minHeight = cfgToolButton->height(); ui()->builtinRadioButton->setMinimumHeight(minHeight); ui()->browserRadioButton->setMinimumHeight(minHeight); ui()->appModeRadioButton->setMinimumHeight(minHeight); QObject::connect(cfgToolButton, &QToolButton::clicked, cfgToolButton, [this] { showCustomCommandPrompt(); }); return widget; } bool GeneralWebViewOptionPage::apply() { auto &webView = values().webView; if (ui()->builtinRadioButton->isChecked()) { webView.mode = ::Settings::WebView::Mode::Builtin; } else if (ui()->browserRadioButton->isChecked()) { webView.mode = ::Settings::WebView::Mode::Browser; } else if (ui()->appModeRadioButton->isChecked()) { webView.mode = ::Settings::WebView::Mode::Command; } webView.customCommand = m_customCommand; return true; } void GeneralWebViewOptionPage::reset() { const auto &webView = values().webView; switch (webView.mode) { case ::Settings::WebView::Mode::Builtin: #ifndef SYNCTHINGWIDGETS_NO_WEBVIEW ui()->builtinRadioButton->setChecked(true); break; #endif case ::Settings::WebView::Mode::Browser: ui()->browserRadioButton->setChecked(true); break; case ::Settings::WebView::Mode::Command: ui()->appModeRadioButton->setChecked(true); break; } m_customCommand = webView.customCommand; } void GeneralWebViewOptionPage::showCustomCommandPrompt() { auto dlg = QInputDialog(); dlg.setInputMode(QInputDialog::TextInput); dlg.setWindowTitle( QCoreApplication::translate("QtGui::GeneralWebViewOptionPage", "Custom command to launch Syncthing's UI - ") + QStringLiteral(APP_NAME)); dlg.setLabelText(QCoreApplication::translate("QtGui::GeneralWebViewOptionPage", "

Enter a custom command to launch Syncthing's UI. The expression %SYNCTHING_URL% will be replaced with the " "Syncthing-URL.

Leave the command empty to use the auto-detection.

")); dlg.setTextValue(m_customCommand); if (dlg.exec() == QDialog::Accepted) { m_customCommand = dlg.textValue(); } } // BuiltinWebViewOptionPage BuiltinWebViewOptionPage::BuiltinWebViewOptionPage(QWidget *parentWidget) : BuiltinWebViewOptionPageBase(parentWidget) { } BuiltinWebViewOptionPage::~BuiltinWebViewOptionPage() { } #ifdef SYNCTHINGWIDGETS_NO_WEBVIEW QWidget *BuiltinWebViewOptionPage::setupWidget() { auto *label = new QLabel; label->setWindowTitle(QCoreApplication::translate("QtGui::BuiltinWebViewOptionPage", "Built-in web view")); label->setAlignment(Qt::AlignCenter); label->setText(QCoreApplication::translate("QtGui::BuiltinWebViewOptionPage", "Syncthing Tray has not been built with vieb view support utilizing either Qt WebKit " "or Qt WebEngine.")); return label; } #endif bool BuiltinWebViewOptionPage::apply() { #ifndef SYNCTHINGWIDGETS_NO_WEBVIEW auto &webView = values().webView; webView.zoomFactor = ui()->zoomDoubleSpinBox->value(); webView.keepRunning = ui()->keepRunningCheckBox->isChecked(); #endif return true; } void BuiltinWebViewOptionPage::reset() { #ifndef SYNCTHINGWIDGETS_NO_WEBVIEW const auto &webView = values().webView; ui()->zoomDoubleSpinBox->setValue(webView.zoomFactor); ui()->keepRunningCheckBox->setChecked(webView.keepRunning); #endif } SettingsDialog::SettingsDialog(const QList &categories, QWidget *parent) : QtUtilities::SettingsDialog(parent) { categoryModel()->setCategories(categories); init(); } SettingsDialog::SettingsDialog(QWidget *parent) : QtUtilities::SettingsDialog(parent) { init(); } SettingsDialog::SettingsDialog(Data::SyncthingConnection *connection, QWidget *parent) : QtUtilities::SettingsDialog(parent) { setWindowFlags(windowFlags() | Qt::WindowMaximizeButtonHint); // setup categories QList categories; OptionCategory *category; category = new OptionCategory(this); translateCategory(category, [] { return tr("Tray"); }); category->assignPages({ m_connectionsOptionPage = new ConnectionOptionPage(connection), new NotificationsOptionPage, m_appearanceOptionPage = new AppearanceOptionPage, new IconsOptionPage(IconsOptionPage::Context::UI), new IconsOptionPage(IconsOptionPage::Context::System) }); category->setIcon(QIcon(QStringLiteral(":/icons/hicolor/scalable/app/syncthingtray.svg"))); categories << category; category = new OptionCategory(this); translateCategory(category, [] { return tr("Web view"); }); category->assignPages({ new GeneralWebViewOptionPage, new BuiltinWebViewOptionPage }); category->setIcon( QIcon::fromTheme(QStringLiteral("internet-web-browser"), QIcon(QStringLiteral(":/icons/hicolor/scalable/apps/internet-web-browser.svg")))); categories << category; category = new OptionCategory(this); translateCategory(category, [] { return tr("Startup"); }); category->assignPages({ new AutostartOptionPage, new LauncherOptionPage, new LauncherOptionPage(QStringLiteral("Process"), tr("additional tool"), tr("Extra launcher")) #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")))); m_launcherSettingsCategory = static_cast(categories.size()); m_launcherSettingsPageIndex = 1; categories << category; categories << values().qt.category(); categoryModel()->setCategories(categories); init(); } SettingsDialog::~SettingsDialog() { } void SettingsDialog::init() { resize(1100, 750); setWindowTitle(tr("Settings") + QStringLiteral(" - " APP_NAME)); setWindowIcon( QIcon::fromTheme(QStringLiteral("preferences-other"), QIcon(QStringLiteral(":/icons/hicolor/scalable/apps/preferences-other.svg")))); // add button for starting wizard auto *startWizardButton = new QPushButton(this); startWizardButton->setToolTip(tr("Start wizard")); startWizardButton->setIcon( QIcon::fromTheme(QStringLiteral("quickwizard"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/tools-wizard.svg")))); startWizardButton->setFlat(true); startWizardButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); connect(startWizardButton, &QPushButton::clicked, this, &SettingsDialog::wizardRequested); addHeadingWidget(startWizardButton); // some settings could be applied without restarting the application, good idea? //connect(this, &Dialogs::SettingsDialog::applied, bind(&Dialogs::QtSettings::apply, &Settings::qtSettings())); } void SettingsDialog::hideConnectionStatus() { if (m_connectionsOptionPage) { m_connectionsOptionPage->hideConnectionStatus(); } } void SettingsDialog::resetPositioningSettings() { if (m_appearanceOptionPage && m_appearanceOptionPage->hasBeenShown()) { m_appearanceOptionPage->resetPositioningSettings(); } } void SettingsDialog::selectLauncherSettings() { if (m_launcherSettingsCategory >= 0 && m_launcherSettingsPageIndex >= 0) { selectPage(m_launcherSettingsCategory, m_launcherSettingsPageIndex); } } } // namespace QtGui INSTANTIATE_UI_FILE_BASED_OPTION_PAGE_NS(QtGui, ConnectionOptionPage) 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, IconsOptionPage) 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 INSTANTIATE_UI_FILE_BASED_OPTION_PAGE_NS(QtGui, GeneralWebViewOptionPage) #ifndef SYNCTHINGWIDGETS_NO_WEBVIEW INSTANTIATE_UI_FILE_BASED_OPTION_PAGE_NS(QtGui, BuiltinWebViewOptionPage) #endif