diff --git a/syncthingwidgets/settings/autostartoptionpage.ui b/syncthingwidgets/settings/autostartoptionpage.ui index 2fb2baf..2654f12 100644 --- a/syncthingwidgets/settings/autostartoptionpage.ui +++ b/syncthingwidgets/settings/autostartoptionpage.ui @@ -12,14 +12,14 @@ Autostart - + + .. - 75 true @@ -28,6 +28,61 @@ + + + + + 0 + + + 0 + + + 0 + + + + + + 32 + 32 + + + + + 32 + 32 + + + + + + + + + 0 + 0 + + + + true + + + + + + + Delete existing entry + + + + :/icons/hicolor/scalable/actions/edit-delete.svg:/icons/hicolor/scalable/actions/edit-delete.svg + + + + + + @@ -63,6 +118,8 @@ - + + + diff --git a/syncthingwidgets/settings/settingsdialog.cpp b/syncthingwidgets/settings/settingsdialog.cpp index e42e182..17a5056 100644 --- a/syncthingwidgets/settings/settingsdialog.cpp +++ b/syncthingwidgets/settings/settingsdialog.cpp @@ -57,6 +57,8 @@ #include #elif defined(PLATFORM_WINDOWS) #include +#include +#include #elif defined(PLATFORM_MAC) #include #endif @@ -739,9 +741,16 @@ AutostartOptionPage::~AutostartOptionPage() QWidget *AutostartOptionPage::setupWidget() { auto *widget = AutostartOptionPageBase::setupWidget(); + auto *style = QApplication::style(); ui()->infoIconLabel->setPixmap( - QApplication::style()->standardIcon(QStyle::SP_MessageBoxInformation, nullptr, ui()->infoIconLabel).pixmap(ui()->infoIconLabel->size())); + 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.")); @@ -755,16 +764,143 @@ QWidget *AutostartOptionPage::setupWidget() #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; } +std::optional configuredAutostartPath() +{ +#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=\"?([^\"]*)\"?")); + const auto match = regex.match(data); + return match.hasCaptured(1) ? std::make_optional(match.captured(1)) : std::nullopt; +#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 1 || 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 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\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"); + 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. Always returns false on other platforms. - * - Does not check whether the startup entry is functional (eg. the specified path is still valid). + * - 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). */ bool isAutostartEnabled() { @@ -786,96 +922,32 @@ bool isAutostartEnabled() } /*! - * \brief Sets whether the application is launchedc on startup. + * \brief Sets whether the application is launched on startup. * \remarks - * - Only implemented under Linux/Windows. Does nothing on other platforms. - * - If a startup entry already exists and \a enabled is true, this function will ensure the path of the existing entry is valid. + * - 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. If the currently configured path cannot be + * determined it will always be overridden, though. * - If no startup entry could be detected via isAutostartEnabled() and \a enabled is false this function doesn't touch anything. */ bool setAutostartEnabled(bool enabled) { - if (!isAutostartEnabled() && !enabled) { + const auto configuredPath = configuredAutostartPath(); + if (!(configuredPath.has_value() ? !configuredPath.value().isEmpty() : isAutostartEnabled()) && !enabled) { return true; } - -#if defined(PLATFORM_LINUX) && !defined(Q_OS_ANDROID) - const auto configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation); - if (configPath.isEmpty()) { - return !enabled; + const auto supposedPath = supposedAutostartPath(); + if (enabled && configuredPath.has_value() && !configuredPath.value().isEmpty() && configuredPath.value() != supposedPath) { + return true; // don't touch existing entry } - if (enabled && !QDir().mkpath(configPath + QStringLiteral("/autostart"))) { - return false; - } - auto desktopFile = QFile(configPath + QStringLiteral("/autostart/" PROJECT_NAME ".desktop")); - if (enabled) { - if (!desktopFile.open(QFile::WriteOnly | QFile::Truncate)) { - return false; - } - desktopFile.write("[Desktop Entry]\n" - "Name=" APP_NAME "\n" - "Exec=\""); -#ifndef SYNCTHINGWIDGETS_AUTOSTART_EXEC_PATH -#define SYNCTHINGWIDGETS_AUTOSTART_EXEC_PATH QCoreApplication::applicationFilePath() -#endif - desktopFile.write(qEnvironmentVariable("APPIMAGE", SYNCTHINGWIDGETS_AUTOSTART_EXEC_PATH).toUtf8().data()); - desktopFile.write("\" qt-widgets-gui --single-instance\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"); - 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 (enabled) { - settings.setValue(QStringLiteral(PROJECT_NAME), QCoreApplication::applicationFilePath().replace(QChar('/'), QChar('\\'))); - } else { - settings.remove(QStringLiteral(PROJECT_NAME)); - } - settings.sync(); - return true; - -#elif defined(PLATFORM_MAC) - const auto libraryPath = QDir::home().filePath(QStringLiteral("Library")); - if (enabled && !QDir().mkpath(libraryPath + QStringLiteral("/LaunchAgents"))) { - return false; - } - auto launchdPlistFile = QFile(libraryPath + QStringLiteral("/LaunchAgents/" PROJECT_NAME ".plist")); - if (enabled) { - 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(QCoreApplication::applicationFilePath().toUtf8().data()); - launchdPlistFile.write("\n" - " \n" - " KeepAlive\n" - " \n" - " \n" - "\n"); - return launchdPlistFile.error() == QFile::NoError && launchdPlistFile.flush(); - - } else { - return !launchdPlistFile.exists() || launchdPlistFile.remove(); - } -#endif + 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; @@ -885,8 +957,31 @@ bool AutostartOptionPage::apply() void AutostartOptionPage::reset() { - if (hasBeenShown()) { + 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())); } } diff --git a/syncthingwidgets/settings/settingsdialog.h b/syncthingwidgets/settings/settingsdialog.h index 74fb5b7..7350e1d 100644 --- a/syncthingwidgets/settings/settingsdialog.h +++ b/syncthingwidgets/settings/settingsdialog.h @@ -15,6 +15,8 @@ #include #include +#include + QT_FORWARD_DECLARE_CLASS(QAction) QT_FORWARD_DECLARE_CLASS(QLabel) @@ -101,7 +103,14 @@ struct { } m_widgets[Data::StatusIconSettings::distinguishableColorCount]; END_DECLARE_OPTION_PAGE -DECLARE_UI_FILE_BASED_OPTION_PAGE_CUSTOM_SETUP(AutostartOptionPage) +BEGIN_DECLARE_UI_FILE_BASED_OPTION_PAGE(AutostartOptionPage) +private: +bool m_unsupported = false; +DECLARE_SETUP_WIDGETS +END_DECLARE_OPTION_PAGE +SYNCTHINGWIDGETS_EXPORT std::optional configuredAutostartPath(); +SYNCTHINGWIDGETS_EXPORT QString supposedAutostartPath(); +SYNCTHINGWIDGETS_EXPORT bool setAutostartPath(const QString &path); SYNCTHINGWIDGETS_EXPORT bool isAutostartEnabled(); SYNCTHINGWIDGETS_EXPORT bool setAutostartEnabled(bool enabled); diff --git a/tray/resources/icons/hicolor/scalable/actions/edit-delete.svg b/tray/resources/icons/hicolor/scalable/actions/edit-delete.svg new file mode 100644 index 0000000..9dfb2e0 --- /dev/null +++ b/tray/resources/icons/hicolor/scalable/actions/edit-delete.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/tray/resources/syncthingtrayicons.qrc b/tray/resources/syncthingtrayicons.qrc index c7424ba..c15df7b 100644 --- a/tray/resources/syncthingtrayicons.qrc +++ b/tray/resources/syncthingtrayicons.qrc @@ -17,5 +17,6 @@ icons/hicolor/scalable/actions/appointment-new.svg icons/hicolor/scalable/actions/download.svg icons/hicolor/scalable/actions/window-pin.svg + icons/hicolor/scalable/actions/edit-delete.svg