diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 6c2bc3d8da353..efbdeda720bb5 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -24,6 +24,7 @@ configure_file(${CMAKE_SOURCE_DIR}/theme.qrc.in ${CMAKE_SOURCE_DIR}/theme.qrc) set(theme_dir ${CMAKE_SOURCE_DIR}/theme) set(client_UI_SRCS + advancedsettings.ui accountsettings.ui conflictdialog.ui invalidfilenamedialog.ui @@ -32,6 +33,7 @@ set(client_UI_SRCS folderwizardsourcepage.ui folderwizardtargetpage.ui generalsettings.ui + infosettings.ui legalnotice.ui ignorelisteditor.ui ignorelisttablewidget.ui @@ -58,6 +60,8 @@ qt_add_resources(client_UI_SRCS ../../resources.qrc ${CMAKE_SOURCE_DIR}/theme.qr set(client_SRCS accountmanager.h accountmanager.cpp + advancedsettings.h + advancedsettings.cpp accountsettings.h accountsettings.cpp accountsetupfromcommandlinejob.h @@ -104,6 +108,8 @@ set(client_SRCS folderwizard.cpp generalsettings.h generalsettings.cpp + infosettings.h + infosettings.cpp legalnotice.h legalnotice.cpp ignorelisteditor.h @@ -136,6 +142,10 @@ set(client_SRCS selectivesyncdialog.cpp settingsdialog.h settingsdialog.cpp + settingspanelstyle.h + settingspanelstyle.cpp + settingsswitch.h + settingsswitch.cpp sharemanager.h sharemanager.cpp profilepagewidget.h diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index f3a272f2104f9..c765d99cc505d 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -40,6 +40,7 @@ #include #include +#include #include #include #include @@ -57,6 +58,7 @@ #include #include #include +#include using namespace Qt::StringLiterals; @@ -185,22 +187,43 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) { _ui->setupUi(this); + _encryptionPanel = new QFrame(this); + _encryptionPanel->setObjectName(QLatin1String("encryptionPanel")); + _encryptionPanel->setFrameShape(QFrame::NoFrame); + _encryptionPanel->setAttribute(Qt::WA_StyledBackground, true); + auto *encryptionPanelLayout = new QVBoxLayout(_encryptionPanel); + encryptionPanelLayout->setContentsMargins(0, 0, 0, 0); + encryptionPanelLayout->setSpacing(0); + _ui->accountStatusLayout->removeWidget(_ui->encryptionMessage); + encryptionPanelLayout->addWidget(_ui->encryptionMessage); + + auto *connectionSettingsButton = new QPushButton(tr("Connection settings"), _ui->accountStatus); + connectionSettingsButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed); + _ui->gridLayout_2->addWidget(connectionSettingsButton, 0, 2, Qt::AlignRight | Qt::AlignVCenter); + connect(connectionSettingsButton, &QPushButton::clicked, this, &AccountSettings::showConnectionSettingsDialog); + + _ui->verticalLayout_2->removeWidget(_ui->accountStatusPanel); + _ui->verticalLayout_2->removeWidget(_ui->fileProviderPanel); + _ui->verticalLayout_2->removeWidget(_ui->syncFoldersPanel); + _ui->verticalLayout_2->removeWidget(_ui->connectionSettingsPanel); + _ui->connectionSettingsPanel->hide(); + _ui->verticalLayout_2->insertWidget(0, _ui->fileProviderPanel); + _ui->verticalLayout_2->insertWidget(1, _ui->syncFoldersPanel); + _ui->verticalLayout_2->insertWidget(2, _encryptionPanel); + _ui->verticalLayout_2->insertWidget(3, _ui->accountStatusPanel); + _model->setAccountState(_accountState); _model->setParent(this); const auto delegate = new FolderStatusDelegate; delegate->setParent(this); - setStyleSheet("QWidget#syncFoldersPanelContents, QWidget#connectionSettingsPanelContents, QWidget#fileProviderPanelContents { background: palette(" BACKGROUND_PALETTE "); }"_L1); + setStyleSheet("QWidget#syncFoldersPanelContents, QWidget#fileProviderPanelContents { background: palette(" BACKGROUND_PALETTE "); }"_L1); _ui->syncFoldersPanelContents->setAutoFillBackground(true); _ui->syncFoldersPanelContents->setAttribute(Qt::WA_StyledBackground, true); _ui->syncFoldersPanelContents->setContentsMargins(0, 0, 0, 0); _ui->fileProviderPanelContents->setAutoFillBackground(true); _ui->fileProviderPanelContents->setAttribute(Qt::WA_StyledBackground, true); _ui->fileProviderPanelContents->setContentsMargins(0, 0, 0, 0); - _ui->connectionSettingsPanelContents->setAutoFillBackground(true); - _ui->connectionSettingsPanelContents->setAttribute(Qt::WA_StyledBackground, true); - _ui->connectionSettingsPanelContents->setContentsMargins(0, 0, 0, 0); - // Connect styleChanged events to our widgets, so they can adapt (Dark-/Light-Mode switching) connect(this, &AccountSettings::styleChanged, delegate, &FolderStatusDelegate::slotStyleChanged); @@ -241,17 +264,6 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) _ui->fileProviderPanel->setVisible(false); #endif - const auto connectionSettingsPanelContents = _ui->connectionSettingsPanelContents; - const auto connectionSettingsLayout = new QVBoxLayout(connectionSettingsPanelContents); - const auto networkSettings = new NetworkSettings(_accountState->account(), connectionSettingsPanelContents); - if (const auto networkSettingsLayout = networkSettings->layout()) { - networkSettingsLayout->setContentsMargins(0, 0, 0, 0); - } - connectionSettingsLayout->setContentsMargins(0, 0, 0, 0); - connectionSettingsLayout->setSpacing(0); - connectionSettingsLayout->addWidget(networkSettings, 1); - connectionSettingsPanelContents->setLayout(connectionSettingsLayout); - const auto mouseCursorChanger = new MouseCursorChanger(this); mouseCursorChanger->folderList = _ui->_folderList; mouseCursorChanger->model = _model; @@ -318,6 +330,7 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) _ui->encryptionMessageLabel->setOpenExternalLinks(true); _ui->encryptionMessageButtonsLayout->addStretch(); setEncryptionMessageIcon({}); + setEncryptionPanelVisible(_ui->encryptionMessage->isVisible()); _ui->connectLabel->setText(tr("No account configured.")); @@ -355,7 +368,7 @@ void AccountSettings::slotE2eEncryptionMnemonicReady() _ui->encryptionMessageLabel->setText(tr("Encryption is set-up. Remember to Encrypt a folder to end-to-end encrypt any new files added to it.")); setEncryptionMessageIcon(Theme::createColorAwareIcon(QStringLiteral(":/client/theme/lock.svg"))); - _ui->encryptionMessage->show(); + setEncryptionPanelVisible(true); } void AccountSettings::slotE2eEncryptionGenerateKeys() @@ -1286,6 +1299,30 @@ void AccountSettings::showConnectionLabel(const QString &message, QStringList er _ui->accountStatus->setVisible(!message.isEmpty()); } +void AccountSettings::showConnectionSettingsDialog() +{ + auto *dialog = new QDialog(this); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->setWindowTitle(tr("Connection settings")); + + auto *layout = new QVBoxLayout(dialog); + layout->setContentsMargins(12, 12, 12, 12); + layout->setSpacing(12); + + auto *networkSettings = new NetworkSettings(_accountState->account(), dialog); + if (auto *networkSettingsLayout = networkSettings->layout()) { + networkSettingsLayout->setContentsMargins(0, 0, 0, 0); + } + layout->addWidget(networkSettings); + + auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, dialog); + connect(buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::reject); + layout->addWidget(buttonBox); + + dialog->resize(networkSettings->sizeHint()); + dialog->open(); +} + void AccountSettings::slotEnableCurrentFolder(bool terminate) { const auto alias = selectedFolderAlias(); @@ -1504,7 +1541,7 @@ void AccountSettings::checkClientSideEncryptionState() << "Client Side Encryption" << accountsState()->account()->capabilities().clientSideEncryptionAvailable(); if (_accountState->account()->capabilities().clientSideEncryptionAvailable()) { - _ui->encryptionMessage->show(); + setEncryptionPanelVisible(true); } } @@ -1867,12 +1904,20 @@ void AccountSettings::setupE2eEncryptionMessage() #endif _ui->encryptionMessageLabel->setText(encryptionMessage); setEncryptionMessageIcon(Theme::createColorAwareIcon(QStringLiteral(":/client/theme/info.svg"))); - _ui->encryptionMessage->hide(); + setEncryptionPanelVisible(false); auto *const actionSetupE2e = addActionToEncryptionMessage(tr("Set up encryption"), e2EeUiActionSetupEncryptionId); connect(actionSetupE2e, &QAction::triggered, this, &AccountSettings::slotE2eEncryptionGenerateKeys); } +void AccountSettings::setEncryptionPanelVisible(bool visible) +{ + _ui->encryptionMessage->setVisible(visible); + if (_encryptionPanel) { + _encryptionPanel->setVisible(visible); + } +} + void AccountSettings::setEncryptionMessageIcon(const QIcon &icon) { if (icon.isNull()) { diff --git a/src/gui/accountsettings.h b/src/gui/accountsettings.h index 1b875845b4891..e029ba50a4f84 100644 --- a/src/gui/accountsettings.h +++ b/src/gui/accountsettings.h @@ -29,6 +29,7 @@ class QListWidgetItem; class QLabel; class QPushButton; class QIcon; +class QFrame; namespace OCC { @@ -130,6 +131,7 @@ private slots: void forgetEncryptionOnDeviceForAccount(const OCC::AccountPtr &account) const; void migrateCertificateForAccount(const OCC::AccountPtr &account); void showConnectionLabel(const QString &message, QStringList errors = QStringList()); + void showConnectionSettingsDialog(); void openIgnoredFilesDialog(const QString & absFolderPath); void customizeStyle(); @@ -137,6 +139,7 @@ private slots: void forgetE2eEncryption(); void checkClientSideEncryptionState(); void removeActionFromEncryptionMessage(const QString &actionId); + void setEncryptionPanelVisible(bool visible); private: bool event(QEvent *) override; @@ -160,6 +163,7 @@ private slots: QAction *_addAccountAction = nullptr; bool _menuShown = false; + QFrame *_encryptionPanel = nullptr; QHash _folderConnections; QHash _encryptionMessageButtons; diff --git a/src/gui/advancedsettings.cpp b/src/gui/advancedsettings.cpp new file mode 100644 index 0000000000000..a7922239ac3e1 --- /dev/null +++ b/src/gui/advancedsettings.cpp @@ -0,0 +1,437 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "advancedsettings.h" +#include "ui_advancedsettings.h" + +#include "accountmanager.h" +#include "application.h" +#include "capabilities.h" +#include "common/utility.h" +#include "configfile.h" +#include "folderman.h" +#include "folder.h" +#include "ignorelisteditor.h" +#include "logger.h" +#include "owncloudgui.h" +#include "settingspanelstyle.h" +#include "theme.h" +#include "common/syncjournaldb.h" + +#ifdef BUILD_FILE_PROVIDER_MODULE +#include "macOS/fileproviderutils.h" +#endif + +#ifdef Q_OS_MACOS +#include "common/utility_mac_sandbox.h" +#endif + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +Q_LOGGING_CATEGORY(lcAdvancedSettings, "com.nextcloud.settings.advanced") + +namespace { +struct ZipEntry { + QString localFilename; + QString zipFilename; +}; + +ZipEntry fileInfoToZipEntry(const QFileInfo &info) +{ + return { + info.absoluteFilePath(), + info.fileName() + }; +} + +ZipEntry fileInfoToLogZipEntry(const QFileInfo &info) +{ + auto entry = fileInfoToZipEntry(info); + entry.zipFilename.prepend(QStringLiteral("logs/")); + return entry; +} + +QVector syncFolderToDatabaseZipEntry(OCC::Folder *f) +{ + QVector result; + + const auto journalPath = f->journalDb()->databaseFilePath(); + const auto journalInfo = QFileInfo(journalPath); + const auto walJournalInfo = QFileInfo(journalPath + "-wal"); + const auto shmJournalInfo = QFileInfo(journalPath + "-shm"); + + result += fileInfoToZipEntry(journalInfo); + if (walJournalInfo.exists()) { + result += fileInfoToZipEntry(walJournalInfo); + } + if (shmJournalInfo.exists()) { + result += fileInfoToZipEntry(shmJournalInfo); + } + + return result; +} + +QVector createDebugArchiveFileList() +{ + auto list = QVector(); + OCC::ConfigFile cfg; + + list.append(fileInfoToZipEntry(QFileInfo(cfg.configFile()))); + + const auto logger = OCC::Logger::instance(); + + if (!logger->logDir().isEmpty()) { + QDir dir(logger->logDir()); + const auto infoList = dir.entryInfoList(QDir::Files); + std::transform(std::cbegin(infoList), std::cend(infoList), + std::back_inserter(list), + fileInfoToLogZipEntry); + } else if (!logger->logFile().isEmpty()) { + list.append(fileInfoToZipEntry(QFileInfo(logger->logFile()))); + } + + const auto folders = OCC::FolderMan::instance()->map().values(); + std::for_each(std::cbegin(folders), std::cend(folders), + [&list] (auto &folderIt) { + const auto &newEntries = syncFolderToDatabaseZipEntry(folderIt); + std::copy(std::cbegin(newEntries), std::cend(newEntries), std::back_inserter(list)); + }); + + return list; +} + +bool createDebugArchive(const QString &filename) +{ + const auto entries = createDebugArchiveFileList(); + + const auto tempDir = QDir::temp(); + const auto tempFilePath = tempDir.filePath(QStringLiteral("nextcloud-debug-archive-temp.zip")); + + KZip zip(tempFilePath); + + if (!zip.open(QIODevice::WriteOnly)) { + qWarning() << "Failed to open debug archive for writing:" + << tempFilePath + << "because of error:" + << zip.errorString(); + + QMessageBox::critical( + nullptr, + QObject::tr("Failed to create debug archive"), + QObject::tr("Could not create debug archive in selected location!"), + QMessageBox::Ok + ); + + return false; + } + + for (const auto &entry : entries) { + zip.addLocalFile(entry.localFilename, entry.zipFilename); + } + +#ifdef BUILD_FILE_PROVIDER_MODULE + qDebug() << "Trying to add file provider domain database and log files..."; + const auto fileProviderDomainsSupportDirectory = OCC::Mac::FileProviderUtils::fileProviderDomainsSupportDirectory(); + + if (fileProviderDomainsSupportDirectory.exists()) { + QDirIterator it(fileProviderDomainsSupportDirectory.path(), QStringList() << "*.jsonl" << "*.realm", QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); + + while (it.hasNext()) { + const auto filePath = it.next(); + const auto relativePath = fileProviderDomainsSupportDirectory.relativeFilePath(filePath); + const auto zipPath = QStringLiteral("File Provider Domains/%1").arg(relativePath); + + zip.addLocalFile(filePath, zipPath); + qDebug() << "Added file from" << filePath; + } + } else { + qWarning() << "file provider domain container log directory not found at" << fileProviderDomainsSupportDirectory.path(); + } +#endif + + const auto clientParameters = QCoreApplication::arguments().join(' ').toUtf8(); + zip.prepareWriting("_client_parameters.txt", {}, {}, clientParameters.size()); + zip.writeData(clientParameters, clientParameters.size()); + zip.finishWriting(clientParameters.size()); + + const auto buildInfo = QString(OCC::Theme::instance()->aboutInfo() + "\n\n" + OCC::Theme::instance()->aboutDetails()).toUtf8(); + zip.prepareWriting("_client_buildinfo.txt", {}, {}, buildInfo.size()); + zip.writeData(buildInfo, buildInfo.size()); + zip.finishWriting(buildInfo.size()); + + zip.close(); + + QFile tempFile(tempFilePath); + if (!tempFile.exists()) { + qWarning() << "Temporary debug archive file does not exist:" << tempFilePath; + QMessageBox::critical( + nullptr, + QObject::tr("Failed to create debug archive"), + QObject::tr("Could not create debug archive in temporary location!"), + QMessageBox::Ok + ); + return false; + } + + if (QFile::exists(filename) && !QFile::remove(filename)) { + qWarning() << "Failed to remove existing file at destination:" << filename; + tempFile.remove(); + QMessageBox::critical( + nullptr, + QObject::tr("Failed to create debug archive"), + QObject::tr("Could not remove existing file at destination!"), + QMessageBox::Ok + ); + return false; + } + + if (!tempFile.rename(filename)) { + qWarning() << "Failed to move debug archive from" << tempFilePath << "to" << filename; + tempFile.remove(); + QMessageBox::critical( + nullptr, + QObject::tr("Failed to create debug archive"), + QObject::tr("Could not move debug archive to selected location!"), + QMessageBox::Ok + ); + return false; + } + + return true; +} + +} // namespace + +namespace OCC { + +AdvancedSettings::AdvancedSettings(QWidget *parent) + : QWidget(parent) + , _ui(new Ui::AdvancedSettings) +{ + _ui->setupUi(this); + + auto *advancedActionsLabel = new QLabel(tr("Advanced"), this); + advancedActionsLabel->setObjectName(QLatin1String("advancedActionsLabel")); + _ui->advancedActionsLayout->insertWidget(0, advancedActionsLabel); + + connect(_ui->newFolderLimitCheckBox, &QAbstractButton::toggled, this, &AdvancedSettings::saveMiscSettings); + connect(_ui->newFolderLimitSpinBox, static_cast(&QSpinBox::valueChanged), this, &AdvancedSettings::saveMiscSettings); + connect(_ui->existingFolderLimitCheckBox, &QAbstractButton::toggled, this, &AdvancedSettings::saveMiscSettings); + connect(_ui->stopExistingFolderNowBigSyncCheckBox, &QAbstractButton::toggled, this, &AdvancedSettings::saveMiscSettings); + connect(_ui->newExternalStorage, &QAbstractButton::toggled, this, &AdvancedSettings::saveMiscSettings); + connect(_ui->moveFilesToTrashCheckBox, &QAbstractButton::toggled, this, &AdvancedSettings::saveMiscSettings); + connect(_ui->showInExplorerNavigationPaneCheckBox, &QAbstractButton::toggled, this, &AdvancedSettings::slotShowInExplorerNavigationPane); + connect(_ui->remotePollIntervalSpinBox, static_cast(&QSpinBox::valueChanged), this, &AdvancedSettings::slotRemotePollIntervalChanged); + connect(_ui->ignoredFilesButton, &QAbstractButton::clicked, this, &AdvancedSettings::slotIgnoreFilesEditor); + connect(_ui->debugArchiveButton, &QAbstractButton::clicked, this, &AdvancedSettings::slotCreateDebugArchive); + +#ifdef Q_OS_MACOS + QString txt = _ui->showInExplorerNavigationPaneLabel->text(); + txt.replace(QString::fromLatin1("Explorer"), QString::fromLatin1("Finder")); + _ui->showInExplorerNavigationPaneLabel->setText(txt); +#endif + +#ifdef Q_OS_WIN + if (QOperatingSystemVersion::current() < QOperatingSystemVersion::Windows10) { + _ui->showInExplorerNavigationPaneCheckBox->setVisible(false); + _ui->showInExplorerNavigationPaneLabel->setVisible(false); + _ui->showInExplorerNavigationPaneRowWidget->setVisible(false); + _ui->showInExplorerNavigationPaneSeparator->setVisible(false); + } +#else + _ui->showInExplorerNavigationPaneCheckBox->setVisible(false); + _ui->showInExplorerNavigationPaneLabel->setVisible(false); + _ui->showInExplorerNavigationPaneRowWidget->setVisible(false); + _ui->showInExplorerNavigationPaneSeparator->setVisible(false); +#endif + + loadMiscSettings(); + + // accountAdded means the wizard was finished and the wizard might change some options. + connect(AccountManager::instance(), &AccountManager::accountAdded, this, &AdvancedSettings::loadMiscSettings); + connect(AccountManager::instance(), &AccountManager::accountAdded, this, &AdvancedSettings::updatePollIntervalVisibility); + connect(AccountManager::instance(), &AccountManager::accountRemoved, this, &AdvancedSettings::updatePollIntervalVisibility); + connect(AccountManager::instance(), &AccountManager::capabilitiesChanged, this, &AdvancedSettings::updatePollIntervalVisibility); + + customizeStyle(); +} + +AdvancedSettings::~AdvancedSettings() +{ + delete _ui; +} + +QSize AdvancedSettings::sizeHint() const +{ + return { + ownCloudGui::settingsDialogSize().width(), + QWidget::sizeHint().height() + }; +} + +void AdvancedSettings::loadMiscSettings() +{ + QScopedValueRollback scope(_currentlyLoading, true); + ConfigFile cfgFile; + + _ui->newExternalStorage->setChecked(cfgFile.confirmExternalStorage()); + _ui->moveFilesToTrashCheckBox->setChecked(cfgFile.moveToTrash()); + _ui->showInExplorerNavigationPaneCheckBox->setChecked(cfgFile.showInExplorerNavigationPane()); + + auto newFolderLimit = cfgFile.newBigFolderSizeLimit(); + _ui->newFolderLimitCheckBox->setChecked(newFolderLimit.first); + _ui->newFolderLimitSpinBox->setValue(newFolderLimit.second); + _ui->existingFolderLimitCheckBox->setEnabled(_ui->newFolderLimitCheckBox->isChecked()); + _ui->existingFolderLimitLabel->setEnabled(_ui->newFolderLimitCheckBox->isChecked()); + _ui->existingFolderLimitCheckBox->setChecked(_ui->newFolderLimitCheckBox->isChecked() && cfgFile.notifyExistingFoldersOverLimit()); + _ui->stopExistingFolderNowBigSyncCheckBox->setEnabled(_ui->existingFolderLimitCheckBox->isChecked()); + _ui->stopExistingFolderNowBigSyncLabel->setEnabled(_ui->existingFolderLimitCheckBox->isChecked()); + _ui->stopExistingFolderNowBigSyncCheckBox->setChecked(_ui->existingFolderLimitCheckBox->isChecked() && cfgFile.stopSyncingExistingFoldersOverLimit()); + + const auto interval = cfgFile.remotePollInterval(); + _ui->remotePollIntervalSpinBox->setValue(static_cast(interval.count() / 1000)); + updatePollIntervalVisibility(); +} + +void AdvancedSettings::saveMiscSettings() +{ + if (_currentlyLoading) { + return; + } + + ConfigFile cfgFile; + + const auto newFolderLimitEnabled = _ui->newFolderLimitCheckBox->isChecked(); + const auto existingFolderLimitEnabled = newFolderLimitEnabled && _ui->existingFolderLimitCheckBox->isChecked(); + const auto stopSyncingExistingFoldersOverLimit = existingFolderLimitEnabled && _ui->stopExistingFolderNowBigSyncCheckBox->isChecked(); + + cfgFile.setMoveToTrash(_ui->moveFilesToTrashCheckBox->isChecked()); + cfgFile.setNewBigFolderSizeLimit(newFolderLimitEnabled, _ui->newFolderLimitSpinBox->value()); + cfgFile.setConfirmExternalStorage(_ui->newExternalStorage->isChecked()); + cfgFile.setNotifyExistingFoldersOverLimit(existingFolderLimitEnabled); + cfgFile.setStopSyncingExistingFoldersOverLimit(stopSyncingExistingFoldersOverLimit); + + _ui->existingFolderLimitCheckBox->setEnabled(newFolderLimitEnabled); + _ui->existingFolderLimitLabel->setEnabled(newFolderLimitEnabled); + _ui->stopExistingFolderNowBigSyncCheckBox->setEnabled(existingFolderLimitEnabled); + _ui->stopExistingFolderNowBigSyncLabel->setEnabled(existingFolderLimitEnabled); +} + +void AdvancedSettings::slotShowInExplorerNavigationPane(bool checked) +{ + ConfigFile cfgFile; + cfgFile.setShowInExplorerNavigationPane(checked); + +#ifdef Q_OS_WIN + FolderMan::instance()->navigationPaneHelper().setShowInExplorerNavigationPane(checked); +#endif +} + +void AdvancedSettings::slotIgnoreFilesEditor() +{ + if (_ignoreEditor.isNull()) { + _ignoreEditor = new IgnoreListEditor(this); + _ignoreEditor->setAttribute(Qt::WA_DeleteOnClose, true); + _ignoreEditor->open(); + } else { + ownCloudGui::raiseDialog(_ignoreEditor); + } +} + +void AdvancedSettings::slotCreateDebugArchive() +{ + const auto destination = QFileDialog::getSaveFileUrl( + this, + tr("Create Debug Archive"), + QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation), + tr("Zip Archives") + " (*.zip)" + ); + + if (!destination.isLocalFile() || destination.toLocalFile().isEmpty()) { + return; + } + +#ifdef Q_OS_MACOS + auto scopedAccess = Utility::MacSandboxSecurityScopedAccess::create(destination); + + if (!scopedAccess->isValid()) { + QMessageBox::critical( + this, + tr("Failed to Access File"), + tr("Could not access the selected location. Please try again or choose a different location.") + ); + return; + } +#endif + + if (createDebugArchive(destination.toLocalFile())) { + QMessageBox::information( + this, + tr("Debug Archive Created"), + tr("Redact information deemed sensitive before sharing! Debug archive created at %1").arg(destination.toLocalFile()) + ); + } +} + +void AdvancedSettings::slotRemotePollIntervalChanged(int seconds) +{ + if (_currentlyLoading) { + return; + } + + ConfigFile cfgFile; + std::chrono::milliseconds interval(seconds * 1000); + cfgFile.setRemotePollInterval(interval); +} + +void AdvancedSettings::updatePollIntervalVisibility() +{ + const auto accounts = AccountManager::instance()->accounts(); + const auto pushAvailable = std::any_of(accounts.cbegin(), accounts.cend(), [](const AccountStatePtr &accountState) -> bool { + if (!accountState) { + return false; + } + const auto accountPtr = accountState->account(); + if (!accountPtr) { + return false; + } + return accountPtr->capabilities().availablePushNotifications().testFlag(PushNotificationType::Files); + }); + + _ui->horizontalLayoutWidget_remotePollInterval->setVisible(!pushAvailable); + _ui->remotePollIntervalSeparator->setVisible(!pushAvailable); +} + +void AdvancedSettings::slotStyleChanged() +{ + customizeStyle(); +} + +void AdvancedSettings::customizeStyle() +{ + SettingsPanelStyle::apply(this); +} + +} // namespace OCC diff --git a/src/gui/advancedsettings.h b/src/gui/advancedsettings.h new file mode 100644 index 0000000000000..49825a50a2baf --- /dev/null +++ b/src/gui/advancedsettings.h @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef ADVANCEDSETTINGS_H +#define ADVANCEDSETTINGS_H + +#include +#include + +namespace OCC { + +class IgnoreListEditor; + +namespace Ui { + class AdvancedSettings; +} + +class AdvancedSettings : public QWidget +{ + Q_OBJECT + +public: + explicit AdvancedSettings(QWidget *parent = nullptr); + ~AdvancedSettings() override; + [[nodiscard]] QSize sizeHint() const override; + +public slots: + void slotStyleChanged(); + +private slots: + void saveMiscSettings(); + void slotShowInExplorerNavigationPane(bool checked); + void slotIgnoreFilesEditor(); + void slotCreateDebugArchive(); + void loadMiscSettings(); + void slotRemotePollIntervalChanged(int seconds); + void updatePollIntervalVisibility(); + +private: + void customizeStyle(); + + Ui::AdvancedSettings *_ui; + QPointer _ignoreEditor; + bool _currentlyLoading = false; +}; + +} // namespace OCC + +#endif // ADVANCEDSETTINGS_H diff --git a/src/gui/advancedsettings.ui b/src/gui/advancedsettings.ui new file mode 100644 index 0000000000000..441a277dfdb9b --- /dev/null +++ b/src/gui/advancedsettings.ui @@ -0,0 +1,433 @@ + + + + OCC::AdvancedSettings + + + + 0 + 0 + 667 + 360 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + 12 + + + + + + + + + 0 + + + + + + + Ask for confirmation before synchronizing new folders larger than + + + newFolderLimitCheckBox + + + + + + + 999999 + + + 99 + + + + + + + MB + + + + + + + Qt::Orientation::Horizontal + + + + + + + true + + + + + + + + + QFrame::Shape::HLine + + + QFrame::Shadow::Plain + + + + + + + + + Notify when synchronised folders grow larger than specified limit + + + existingFolderLimitCheckBox + + + + + + + Qt::Orientation::Horizontal + + + + + + + + + + + + QFrame::Shape::HLine + + + QFrame::Shadow::Plain + + + + + + + + + Automatically disable synchronisation of folders that overcome limit + + + stopExistingFolderNowBigSyncCheckBox + + + + + + + Qt::Orientation::Horizontal + + + + + + + + + + + + QFrame::Shape::HLine + + + QFrame::Shadow::Plain + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Server poll interval + + + + + + + 5 + + + 999999 + + + + + + + seconds (if <a href="https://github.com/nextcloud/notify_push">Client Push</a> is unavailable) + + + Qt::TextFormat::RichText + + + true + + + Qt::TextInteractionFlag::LinksAccessibleByKeyboard|Qt::TextInteractionFlag::LinksAccessibleByMouse + + + + + + + Qt::Orientation::Horizontal + + + + + + + + + + + + + + + + + 0 + + + + + + + Ask for confirmation before synchronizing external storages + + + newExternalStorage + + + + + + + Qt::Orientation::Horizontal + + + + + + + + + + + + QFrame::Shape::HLine + + + QFrame::Shadow::Plain + + + + + + + + + Move removed files to trash + + + moveFilesToTrashCheckBox + + + + + + + Qt::Orientation::Horizontal + + + + + + + + + + + + QFrame::Shape::HLine + + + QFrame::Shadow::Plain + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Show sync folders in &Explorer's navigation pane + + + showInExplorerNavigationPaneCheckBox + + + + + + + Qt::Orientation::Horizontal + + + + + + + + + + + + + + + + + + + + + + Qt::Orientation::Horizontal + + + + + + + Edit &Ignored Files + + + + + + + Create Debug Archive + + + + + + + + + + Qt::Orientation::Vertical + + + QSizePolicy::Policy::MinimumExpanding + + + + 20 + 0 + + + + + + + + + OCC::SettingsSwitch + QAbstractButton +
settingsswitch.h
+
+
+ + newFolderLimitCheckBox + newFolderLimitSpinBox + existingFolderLimitCheckBox + stopExistingFolderNowBigSyncCheckBox + remotePollIntervalSpinBox + newExternalStorage + moveFilesToTrashCheckBox + showInExplorerNavigationPaneCheckBox + ignoredFilesButton + debugArchiveButton + + + + + newFolderLimitCheckBox + toggled(bool) + newFolderLimitSpinBox + setEnabled(bool) + + + 620 + 30 + + + 520 + 30 + + + + +
diff --git a/src/gui/generalsettings.cpp b/src/gui/generalsettings.cpp index b98c7072a9a01..7ac9317bc6a45 100644 --- a/src/gui/generalsettings.cpp +++ b/src/gui/generalsettings.cpp @@ -7,232 +7,17 @@ #include "generalsettings.h" #include "ui_generalsettings.h" -#include "theme.h" -#include "configfile.h" -#include "application.h" -#include "owncloudsetupwizard.h" #include "accountmanager.h" -#include "guiutility.h" -#include "capabilities.h" - -#if defined(BUILD_UPDATER) -#include "updater/updater.h" -#include "updater/ocupdater.h" -#ifdef Q_OS_MACOS -// FIXME We should unify those, but Sparkle does everything behind the scene transparently -#include "updater/sparkleupdater.h" -#endif -#endif - -#ifdef BUILD_FILE_PROVIDER_MODULE -#include "macOS/fileproviderutils.h" -#include "macOS/fileprovider.h" -#include "macOS/fileprovidersettingscontroller.h" -#endif - -#include "ignorelisteditor.h" #include "common/utility.h" -#include "logger.h" - -#include "legalnotice.h" +#include "configfile.h" +#include "owncloudgui.h" +#include "settingspanelstyle.h" +#include "theme.h" -#include +#include #include -#include -#include -#include #include -#include - -#include -#include - -Q_LOGGING_CATEGORY(lcGeneralSettings, "com.nextcloud.settings.general") - -#ifdef Q_OS_MACOS -#include "common/utility_mac_sandbox.h" -#endif - -namespace { -struct ZipEntry { - QString localFilename; - QString zipFilename; -}; - -ZipEntry fileInfoToZipEntry(const QFileInfo &info) -{ - return { - info.absoluteFilePath(), - info.fileName() - }; -} - -ZipEntry fileInfoToLogZipEntry(const QFileInfo &info) -{ - auto entry = fileInfoToZipEntry(info); - entry.zipFilename.prepend(QStringLiteral("logs/")); - return entry; -} - -QVector syncFolderToDatabaseZipEntry(OCC::Folder *f) -{ - QVector result; - - const auto journalPath = f->journalDb()->databaseFilePath(); - const auto journalInfo = QFileInfo(journalPath); - const auto walJournalInfo = QFileInfo(journalPath + "-wal"); - const auto shmJournalInfo = QFileInfo(journalPath + "-shm"); - - result += fileInfoToZipEntry(journalInfo); - if (walJournalInfo.exists()) { - result += fileInfoToZipEntry(walJournalInfo); - } - if (shmJournalInfo.exists()) { - result += fileInfoToZipEntry(shmJournalInfo); - } - - return result; -} - -QVector createDebugArchiveFileList() -{ - auto list = QVector(); - OCC::ConfigFile cfg; - - list.append(fileInfoToZipEntry(QFileInfo(cfg.configFile()))); - - const auto logger = OCC::Logger::instance(); - - if (!logger->logDir().isEmpty()) { - QDir dir(logger->logDir()); - const auto infoList = dir.entryInfoList(QDir::Files); - std::transform(std::cbegin(infoList), std::cend(infoList), - std::back_inserter(list), - fileInfoToLogZipEntry); - } else if (!logger->logFile().isEmpty()) { - list.append(fileInfoToZipEntry(QFileInfo(logger->logFile()))); - } - - const auto folders = OCC::FolderMan::instance()->map().values(); - std::for_each(std::cbegin(folders), std::cend(folders), - [&list] (auto &folderIt) { - const auto &newEntries = syncFolderToDatabaseZipEntry(folderIt); - std::copy(std::cbegin(newEntries), std::cend(newEntries), std::back_inserter(list)); - }); - - return list; -} - -bool createDebugArchive(const QString &filename) -{ - const auto entries = createDebugArchiveFileList(); - - // Create the ZIP archive in a temporary directory first - const auto tempDir = QDir::temp(); - const auto tempFilePath = tempDir.filePath(QStringLiteral("nextcloud-debug-archive-temp.zip")); - - KZip zip(tempFilePath); - - if (!zip.open(QIODevice::WriteOnly)) { - qWarning() << "Failed to open debug archive for writing:" - << tempFilePath - << "because of error:" - << zip.errorString(); - - QMessageBox::critical( - nullptr, - QObject::tr("Failed to create debug archive"), - QObject::tr("Could not create debug archive in selected location!"), - QMessageBox::Ok - ); - - return false; - } - - for (const auto &entry : entries) { - zip.addLocalFile(entry.localFilename, entry.zipFilename); - } - -#ifdef BUILD_FILE_PROVIDER_MODULE - qDebug() << "Trying to add file provider domain database and log files..."; - const auto fileProviderDomainsSupportDirectory = OCC::Mac::FileProviderUtils::fileProviderDomainsSupportDirectory(); - - if (fileProviderDomainsSupportDirectory.exists()) { - // Recursively add all files from the container log directory - QDirIterator it(fileProviderDomainsSupportDirectory.path(), QStringList() << "*.jsonl" << "*.realm", QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); - - while (it.hasNext()) { - const auto filePath = it.next(); - - // Calculate relative path from the base container log directory - const auto relativePath = fileProviderDomainsSupportDirectory.relativeFilePath(filePath); - const auto zipPath = QStringLiteral("File Provider Domains/%1").arg(relativePath); - - zip.addLocalFile(filePath, zipPath); - qDebug() << "Added file from" << filePath; - } - } else { - qWarning() << "file provider domain container log directory not found at" << fileProviderDomainsSupportDirectory.path(); - } -#endif - - const auto clientParameters = QCoreApplication::arguments().join(' ').toUtf8(); - zip.prepareWriting("_client_parameters.txt", {}, {}, clientParameters.size()); - zip.writeData(clientParameters, clientParameters.size()); - zip.finishWriting(clientParameters.size()); - - const auto buildInfo = QString(OCC::Theme::instance()->aboutInfo() + "\n\n" + OCC::Theme::instance()->aboutDetails()).toUtf8(); - zip.prepareWriting("_client_buildinfo.txt", {}, {}, buildInfo.size()); - zip.writeData(buildInfo, buildInfo.size()); - zip.finishWriting(buildInfo.size()); - - zip.close(); - - // Now move the temporary ZIP file to the desired destination - QFile tempFile(tempFilePath); - if (!tempFile.exists()) { - qWarning() << "Temporary debug archive file does not exist:" << tempFilePath; - QMessageBox::critical( - nullptr, - QObject::tr("Failed to create debug archive"), - QObject::tr("Could not create debug archive in temporary location!"), - QMessageBox::Ok - ); - return false; - } - - // Remove destination file if it already exists - if (QFile::exists(filename)) { - if (!QFile::remove(filename)) { - qWarning() << "Failed to remove existing file at destination:" << filename; - tempFile.remove(); - QMessageBox::critical( - nullptr, - QObject::tr("Failed to create debug archive"), - QObject::tr("Could not remove existing file at destination!"), - QMessageBox::Ok - ); - return false; - } - } - - // Move the temporary file to the final destination - if (!tempFile.rename(filename)) { - qWarning() << "Failed to move debug archive from" << tempFilePath << "to" << filename; - tempFile.remove(); - QMessageBox::critical( - nullptr, - QObject::tr("Failed to create debug archive"), - QObject::tr("Could not move debug archive to selected location!"), - QMessageBox::Ok - ); - return false; - } - - return true; -} - -} +#include namespace OCC { @@ -242,8 +27,6 @@ GeneralSettings::GeneralSettings(QWidget *parent) { _ui->setupUi(this); - updatePollIntervalVisibility(); - connect(_ui->serverNotificationsCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::slotToggleOptionalServerNotifications); _ui->serverNotificationsCheckBox->setToolTip(tr("Server notifications that require attention.")); @@ -259,16 +42,7 @@ GeneralSettings::GeneralSettings(QWidget *parent) connect(_ui->quotaWarningNotificationsCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::slotToggleQuotaWarningNotifications); _ui->quotaWarningNotificationsCheckBox->setToolTip(tr("Show notification when quota usage exceeds 80%.")); - connect(_ui->showInExplorerNavigationPaneCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::slotShowInExplorerNavigationPane); - - // Rename 'Explorer' appropriately on non-Windows -#ifdef Q_OS_MACOS - QString txt = _ui->showInExplorerNavigationPaneCheckBox->text(); - txt.replace(QString::fromLatin1("Explorer"), QString::fromLatin1("Finder")); - _ui->showInExplorerNavigationPaneCheckBox->setText(txt); -#endif - - if(const auto hasSystemAutoStart = Utility::hasSystemLaunchOnStartup(Theme::instance()->appName())) { + if (const auto hasSystemAutoStart = Utility::hasSystemLaunchOnStartup(Theme::instance()->appName())) { _ui->autostartCheckBox->setChecked(hasSystemAutoStart); _ui->autostartCheckBox->setDisabled(hasSystemAutoStart); _ui->autostartCheckBox->setToolTip(tr("You cannot disable autostart because system-wide autostart is enabled.")); @@ -277,63 +51,19 @@ GeneralSettings::GeneralSettings(QWidget *parent) _ui->autostartCheckBox->setChecked(Utility::hasLaunchOnStartup(Theme::instance()->appName())); } - // setup about section - _ui->infoAndUpdatesLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); - _ui->infoAndUpdatesLabel->setText(Theme::instance()->about()); - _ui->infoAndUpdatesLabel->setOpenExternalLinks(true); - - // About legal notice - connect(_ui->legalNoticeButton, &QPushButton::clicked, this, &GeneralSettings::slotShowLegalNotice); - - connect(_ui->usageDocumentationButton, &QPushButton::clicked, this, []() { - Utility::openBrowser(QUrl(Theme::instance()->helpUrl())); - }); - loadMiscSettings(); - // misc connect(_ui->monoIconsCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings); - connect(_ui->newFolderLimitCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings); - connect(_ui->newFolderLimitSpinBox, static_cast(&QSpinBox::valueChanged), this, &GeneralSettings::saveMiscSettings); - connect(_ui->existingFolderLimitCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings); - connect(_ui->stopExistingFolderNowBigSyncCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings); - connect(_ui->newExternalStorage, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings); - connect(_ui->moveFilesToTrashCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings); - connect(_ui->remotePollIntervalSpinBox, &QSpinBox::valueChanged, this, &GeneralSettings::slotRemotePollIntervalChanged); - - // Hide on non-Windows, or WindowsVersion < 10. - // The condition should match the default value of ConfigFile::showInExplorerNavigationPane. -#ifdef Q_OS_WIN - if (QOperatingSystemVersion::current() < QOperatingSystemVersion::Windows10) - _ui->showInExplorerNavigationPaneCheckBox->setVisible(false); -#else - // Hide on non-Windows - _ui->showInExplorerNavigationPaneCheckBox->setVisible(false); -#endif - - /* Set the left contents margin of the layout to zero to make the checkboxes - * align properly vertically , fixes bug #3758 - */ - int m0 = 0; - int m1 = 0; - int m2 = 0; - int m3 = 0; - _ui->horizontalLayout_3->getContentsMargins(&m0, &m1, &m2, &m3); - _ui->horizontalLayout_3->setContentsMargins(0, m1, m2, m3); - - // OEM themes are not obliged to ship mono icons, so there - // is no point in offering an option - _ui->monoIconsCheckBox->setVisible(Theme::instance()->monoIconsAvailable()); - - connect(_ui->ignoredFilesButton, &QAbstractButton::clicked, this, &GeneralSettings::slotIgnoreFilesEditor); - connect(_ui->debugArchiveButton, &QAbstractButton::clicked, this, &GeneralSettings::slotCreateDebugArchive); // accountAdded means the wizard was finished and the wizard might change some options. connect(AccountManager::instance(), &AccountManager::accountAdded, this, &GeneralSettings::loadMiscSettings); -#if defined(BUILD_UPDATER) - loadUpdateChannelsList(); -#endif + // OEM themes are not obliged to ship mono icons, so there is no point in offering an option. + const auto monoIconsAvailable = Theme::instance()->monoIconsAvailable(); + _ui->monoIconsCheckBox->setVisible(monoIconsAvailable); + _ui->monoIconsLabel->setVisible(monoIconsAvailable); + _ui->monoIconsRowWidget->setVisible(monoIconsAvailable); + _ui->startupSeparator->setVisible(monoIconsAvailable); customizeStyle(); } @@ -358,294 +88,26 @@ void GeneralSettings::loadMiscSettings() _ui->monoIconsCheckBox->setChecked(cfgFile.monoIcons()); _ui->serverNotificationsCheckBox->setChecked(cfgFile.optionalServerNotifications()); + _ui->chatNotificationsLabel->setEnabled(cfgFile.optionalServerNotifications()); _ui->chatNotificationsCheckBox->setEnabled(cfgFile.optionalServerNotifications()); _ui->chatNotificationsCheckBox->setChecked(cfgFile.showChatNotifications()); + _ui->callNotificationsLabel->setEnabled(cfgFile.optionalServerNotifications()); _ui->callNotificationsCheckBox->setEnabled(cfgFile.optionalServerNotifications()); _ui->callNotificationsCheckBox->setChecked(cfgFile.showCallNotifications()); + _ui->quotaWarningNotificationsLabel->setEnabled(cfgFile.optionalServerNotifications()); _ui->quotaWarningNotificationsCheckBox->setEnabled(cfgFile.optionalServerNotifications()); _ui->quotaWarningNotificationsCheckBox->setChecked(cfgFile.showQuotaWarningNotifications()); - _ui->showInExplorerNavigationPaneCheckBox->setChecked(cfgFile.showInExplorerNavigationPane()); - _ui->newExternalStorage->setChecked(cfgFile.confirmExternalStorage()); - _ui->monoIconsCheckBox->setChecked(cfgFile.monoIcons()); - _ui->moveFilesToTrashCheckBox->setChecked(cfgFile.moveToTrash()); - - auto newFolderLimit = cfgFile.newBigFolderSizeLimit(); - _ui->newFolderLimitCheckBox->setChecked(newFolderLimit.first); - _ui->newFolderLimitSpinBox->setValue(newFolderLimit.second); - _ui->existingFolderLimitCheckBox->setEnabled(_ui->newFolderLimitCheckBox->isChecked()); - _ui->existingFolderLimitCheckBox->setChecked(_ui->newFolderLimitCheckBox->isChecked() && cfgFile.notifyExistingFoldersOverLimit()); - _ui->stopExistingFolderNowBigSyncCheckBox->setEnabled(_ui->existingFolderLimitCheckBox->isChecked()); - _ui->stopExistingFolderNowBigSyncCheckBox->setChecked(_ui->existingFolderLimitCheckBox->isChecked() && cfgFile.stopSyncingExistingFoldersOverLimit()); - _ui->newExternalStorage->setChecked(cfgFile.confirmExternalStorage()); - _ui->monoIconsCheckBox->setChecked(cfgFile.monoIcons()); - - const auto interval = cfgFile.remotePollInterval(); - _ui->remotePollIntervalSpinBox->setValue(static_cast(interval.count() / 1000)); - updatePollIntervalVisibility(); -} - -#if defined(BUILD_UPDATER) -void GeneralSettings::loadUpdateChannelsList() { - ConfigFile cfgFile; - if (cfgFile.serverHasValidSubscription()) { - _ui->updateChannel->hide(); - _ui->updateChannelLabel->hide(); - _ui->restoreUpdateChannelButton->hide(); - return; - } - - const auto validUpdateChannels = cfgFile.validUpdateChannels(); - const auto currentUpdateChannel = cfgFile.currentUpdateChannel(); - if (_currentUpdateChannelList.isEmpty() || _currentUpdateChannelList != validUpdateChannels){ - _currentUpdateChannelList = validUpdateChannels; - _ui->updateChannel->clear(); - _ui->updateChannel->addItems(_currentUpdateChannelList); - const auto currentUpdateChannelIndex = _currentUpdateChannelList.indexOf(currentUpdateChannel); - _ui->updateChannel->setCurrentIndex(currentUpdateChannelIndex != -1 ? currentUpdateChannelIndex : 0); - connect(_ui->updateChannel, &QComboBox::currentTextChanged, this, &GeneralSettings::slotUpdateChannelChanged); - } - - const auto defaultUpdateChannel = cfgFile.defaultUpdateChannel(); - _ui->restoreUpdateChannelButton->setText(tr("Restore to &%1").arg(updateChannelToLocalized(defaultUpdateChannel))); - _ui->restoreUpdateChannelButton->setEnabled(currentUpdateChannel != defaultUpdateChannel); - connect(_ui->restoreUpdateChannelButton, &QPushButton::clicked, this, &GeneralSettings::slotRestoreUpdateChannel); } -void GeneralSettings::slotUpdateInfo() -{ - ConfigFile config; - const auto updater = Updater::instance(); - if (config.skipUpdateCheck() || !updater) { - // updater disabled on compile - _ui->updatesContainer->setVisible(false); - return; - } - - if (updater) { - connect(_ui->updateButton, - &QAbstractButton::clicked, - this, - &GeneralSettings::slotUpdateCheckNow, - Qt::UniqueConnection); - connect(_ui->autoCheckForUpdatesCheckBox, &QAbstractButton::toggled, this, - &GeneralSettings::slotToggleAutoUpdateCheck, Qt::UniqueConnection); - _ui->autoCheckForUpdatesCheckBox->setChecked(config.autoUpdateCheck()); - } - - // Note: the sparkle-updater is not an OCUpdater - const auto ocupdater = qobject_cast(updater); - if (ocupdater) { - connect(ocupdater, &OCUpdater::downloadStateChanged, this, &GeneralSettings::slotUpdateInfo, Qt::UniqueConnection); - connect(_ui->restartButton, &QAbstractButton::clicked, ocupdater, &OCUpdater::slotStartInstaller, Qt::UniqueConnection); - - auto status = ocupdater->statusString(OCUpdater::UpdateStatusStringFormat::Html); - if (config.serverHasValidSubscription()) { - auto currentChannel = updateChannelToLocalized(config.currentUpdateChannel()); - if (currentChannel.isEmpty()) { - currentChannel = config.currentUpdateChannel(); - } - status.append(QStringLiteral("
%1") - .arg(tr("Connected to an enterprise system. Update channel (%1) cannot be changed.") - .arg(currentChannel))); - } - Theme::replaceLinkColorStringBackgroundAware(status); - - _ui->updateStateLabel->setOpenExternalLinks(false); - connect(_ui->updateStateLabel, &QLabel::linkActivated, this, [](const QString &link) { - Utility::openBrowser(QUrl(link)); - }); - _ui->updateStateLabel->setText(status); - _ui->restartButton->setVisible(ocupdater->downloadState() == OCUpdater::DownloadComplete); - _ui->updateButton->setEnabled(ocupdater->downloadState() != OCUpdater::CheckingServer && - ocupdater->downloadState() != OCUpdater::Downloading && - ocupdater->downloadState() != OCUpdater::DownloadComplete); - } -#if defined(Q_OS_MACOS) && defined(HAVE_SPARKLE) - else if (const auto sparkleUpdater = qobject_cast(updater)) { - connect(sparkleUpdater, &SparkleUpdater::statusChanged, this, &GeneralSettings::slotUpdateInfo, Qt::UniqueConnection); - auto status = sparkleUpdater->statusString(); - if (config.serverHasValidSubscription()) { - const auto currentChannel = config.currentUpdateChannel(); - if (Qt::mightBeRichText(status)) { - status.append(QStringLiteral("
")); - } else { - status.append(QStringLiteral("\n")); - } - status.append(tr("Connected to an enterprise system. Update channel (%1) cannot be changed.") - .arg(currentChannel)); - } - _ui->updateStateLabel->setText(status); - _ui->restartButton->setVisible(false); - - const auto updaterState = sparkleUpdater->state(); - const auto enableUpdateButton = updaterState == SparkleUpdater::State::Idle || - updaterState == SparkleUpdater::State::Unknown; - _ui->updateButton->setEnabled(enableUpdateButton); - } -#endif -} - -void GeneralSettings::setAndCheckNewUpdateChannel(const QString &newChannel) { - ConfigFile().setUpdateChannel(newChannel); - if (auto updater = qobject_cast(Updater::instance())) { - updater->setUpdateUrl(Updater::updateUrl()); - updater->checkForUpdate(); - } -#if defined(Q_OS_MACOS) && defined(HAVE_SPARKLE) - else if (auto updater = qobject_cast(Updater::instance())) { - updater->setUpdateUrl(Updater::updateUrl()); - updater->checkForUpdate(); - } -#endif -} - -QString GeneralSettings::updateChannelToLocalized(const QString &channel) const -{ - if (channel == QStringLiteral("stable")) { - return tr("stable"); - } - - if (channel == QStringLiteral("beta")) { - return tr("beta"); - } - - if (channel == QStringLiteral("daily")) { - return tr("daily"); - } - - if (channel == QStringLiteral("enterprise")) { - return tr("enterprise"); - } - - return QString{}; -} - -void GeneralSettings::slotUpdateChannelChanged() -{ - const auto updateChannelFromLocalized = [](const int index) { - switch(index) { - case 1: - return QStringLiteral("beta"); - break; - case 2: - return QStringLiteral("daily"); - break; - case 3: - return QStringLiteral("enterprise"); - break; - default: - return QStringLiteral("stable"); - } - }; - - ConfigFile configFile; - const auto newChannel = updateChannelFromLocalized(_ui->updateChannel->currentIndex()); - const auto currentUpdateChannel = configFile.currentUpdateChannel(); - if (newChannel == currentUpdateChannel) { - return; - } - - if (newChannel == configFile.defaultUpdateChannel()) { - restoreUpdateChannel(); - return; - } - - _ui->restoreUpdateChannelButton->setEnabled(true); - - const auto nonEnterpriseOptions = tr("- beta: contains versions with new features that may not be tested thoroughly\n" - "- daily: contains versions created daily only for testing and development\n" - "\n" - "Downgrading versions is not possible immediately: changing from beta to stable means waiting for the new stable version.", - "list of available update channels to non enterprise users and downgrading warning"); - const auto enterpriseOptions = tr("- enterprise: contains stable versions for customers.\n" - "\n" - "Downgrading versions is not possible immediately: changing from stable to enterprise means waiting for the new enterprise version.", - "list of available update channels to enterprise users and downgrading warning"); - - auto msgBox = new QMessageBox( - QMessageBox::Warning, - tr("Changing update channel?"), - tr("The channel determines which upgrades will be offered to install:\n" - "- stable: contains tested versions considered reliable\n", - "starts list of available update channels, stable is always available") - .append(configFile.validUpdateChannels().contains("enterprise") ? enterpriseOptions : nonEnterpriseOptions), - QMessageBox::NoButton, - this); - const auto acceptButton = msgBox->addButton(tr("Change update channel"), QMessageBox::AcceptRole); - msgBox->addButton(tr("Cancel"), QMessageBox::RejectRole); - connect(msgBox, &QMessageBox::finished, msgBox, [this, newChannel, currentUpdateChannel, msgBox, acceptButton] { - msgBox->deleteLater(); - if (msgBox->clickedButton() == acceptButton) { - setAndCheckNewUpdateChannel(newChannel); - } else { - _ui->updateChannel->setCurrentText(updateChannelToLocalized(currentUpdateChannel)); - } - }); - msgBox->open(); -} - -void GeneralSettings::slotUpdateCheckNow() -{ -#if defined(Q_OS_MACOS) && defined(HAVE_SPARKLE) - auto *updater = qobject_cast(Updater::instance()); -#else - auto *updater = qobject_cast(Updater::instance()); -#endif - if (ConfigFile().skipUpdateCheck()) { - updater = nullptr; // don't show update info if updates are disabled - } - - if (updater) { - _ui->updateButton->setEnabled(false); - - updater->checkForUpdate(); - } -} - -void GeneralSettings::slotToggleAutoUpdateCheck() -{ - ConfigFile cfgFile; - bool isChecked = _ui->autoCheckForUpdatesCheckBox->isChecked(); - cfgFile.setAutoUpdateCheck(isChecked, QString()); -} - -void GeneralSettings::restoreUpdateChannel() -{ - const auto defaultUpdateChannel = ConfigFile().defaultUpdateChannel(); - _ui->restoreUpdateChannelButton->setEnabled(false); - _ui->updateChannel->setCurrentText(updateChannelToLocalized(defaultUpdateChannel)); - setAndCheckNewUpdateChannel(defaultUpdateChannel); -} - -void GeneralSettings::slotRestoreUpdateChannel() -{ - restoreUpdateChannel(); -} -#endif // defined(BUILD_UPDATER) - void GeneralSettings::saveMiscSettings() { if (_currentlyLoading) { return; } - ConfigFile cfgFile; - const auto useMonoIcons = _ui->monoIconsCheckBox->isChecked(); - const auto newFolderLimitEnabled = _ui->newFolderLimitCheckBox->isChecked(); - const auto existingFolderLimitEnabled = newFolderLimitEnabled && _ui->existingFolderLimitCheckBox->isChecked(); - const auto stopSyncingExistingFoldersOverLimit = existingFolderLimitEnabled && _ui->stopExistingFolderNowBigSyncCheckBox->isChecked(); Theme::instance()->setSystrayUseMonoIcons(useMonoIcons); - - cfgFile.setMonoIcons(useMonoIcons); - cfgFile.setMoveToTrash(_ui->moveFilesToTrashCheckBox->isChecked()); - cfgFile.setNewBigFolderSizeLimit(newFolderLimitEnabled, _ui->newFolderLimitSpinBox->value()); - cfgFile.setConfirmExternalStorage(_ui->newExternalStorage->isChecked()); - cfgFile.setNotifyExistingFoldersOverLimit(existingFolderLimitEnabled); - cfgFile.setStopSyncingExistingFoldersOverLimit(stopSyncingExistingFoldersOverLimit); - - _ui->existingFolderLimitCheckBox->setEnabled(newFolderLimitEnabled); - _ui->stopExistingFolderNowBigSyncCheckBox->setEnabled(existingFolderLimitEnabled); + ConfigFile().setMonoIcons(useMonoIcons); } void GeneralSettings::slotToggleLaunchOnStartup(bool enable) @@ -658,8 +120,7 @@ void GeneralSettings::slotToggleLaunchOnStartup(bool enable) Utility::setLaunchOnStartup(theme->appName(), theme->appNameGUI(), enable); const auto actualState = Utility::hasLaunchOnStartup(theme->appName()); - ConfigFile configFile; - configFile.setLaunchOnSystemStartup(actualState); + ConfigFile().setLaunchOnSystemStartup(actualState); if (actualState != enable) { const QSignalBlocker blocker(_ui->autostartCheckBox); @@ -680,97 +141,28 @@ void GeneralSettings::slotToggleLaunchOnStartup(bool enable) void GeneralSettings::slotToggleOptionalServerNotifications(bool enable) { - ConfigFile cfgFile; - cfgFile.setOptionalServerNotifications(enable); + ConfigFile().setOptionalServerNotifications(enable); + _ui->chatNotificationsLabel->setEnabled(enable); _ui->chatNotificationsCheckBox->setEnabled(enable); + _ui->callNotificationsLabel->setEnabled(enable); _ui->callNotificationsCheckBox->setEnabled(enable); + _ui->quotaWarningNotificationsLabel->setEnabled(enable); _ui->quotaWarningNotificationsCheckBox->setEnabled(enable); } void GeneralSettings::slotToggleChatNotifications(bool enable) { - ConfigFile cfgFile; - cfgFile.setShowChatNotifications(enable); + ConfigFile().setShowChatNotifications(enable); } void GeneralSettings::slotToggleCallNotifications(bool enable) { - ConfigFile cfgFile; - cfgFile.setShowCallNotifications(enable); + ConfigFile().setShowCallNotifications(enable); } void GeneralSettings::slotToggleQuotaWarningNotifications(bool enable) { - ConfigFile cfgFile; - cfgFile.setShowQuotaWarningNotifications(enable); -} - -void GeneralSettings::slotShowInExplorerNavigationPane(bool checked) -{ - ConfigFile cfgFile; - cfgFile.setShowInExplorerNavigationPane(checked); - -#ifdef Q_OS_WIN - // Now update the registry with the change. - FolderMan::instance()->navigationPaneHelper().setShowInExplorerNavigationPane(checked); -#endif -} - -void GeneralSettings::slotIgnoreFilesEditor() -{ - if (_ignoreEditor.isNull()) { - ConfigFile cfgFile; - _ignoreEditor = new IgnoreListEditor(this); - _ignoreEditor->setAttribute(Qt::WA_DeleteOnClose, true); - _ignoreEditor->open(); - } else { - ownCloudGui::raiseDialog(_ignoreEditor); - } -} - -void GeneralSettings::slotCreateDebugArchive() -{ - const auto destination = QFileDialog::getSaveFileUrl( - this, - tr("Create Debug Archive"), - QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation), - tr("Zip Archives") + " (*.zip)" - ); - - if (!destination.isLocalFile() || destination.toLocalFile().isEmpty()) { - return; - } - -#ifdef Q_OS_MACOS - // On macOS with app sandbox, we need to explicitly access the security-scoped resource - // that was selected by the user via the file dialog. This is required even though we have - // the com.apple.security.files.user-selected.read-write entitlement. - auto scopedAccess = Utility::MacSandboxSecurityScopedAccess::create(destination); - - if (!scopedAccess->isValid()) { - QMessageBox::critical( - this, - tr("Failed to Access File"), - tr("Could not access the selected location. Please try again or choose a different location.") - ); - return; - } -#endif - - if (createDebugArchive(destination.toLocalFile())) { - QMessageBox::information( - this, - tr("Debug Archive Created"), - tr("Redact information deemed sensitive before sharing! Debug archive created at %1").arg(destination.toLocalFile()) - ); - } -} - -void GeneralSettings::slotShowLegalNotice() -{ - auto notice = new LegalNotice(); - notice->exec(); - delete notice; + ConfigFile().setShowQuotaWarningNotifications(enable); } void GeneralSettings::slotStyleChanged() @@ -780,48 +172,7 @@ void GeneralSettings::slotStyleChanged() void GeneralSettings::customizeStyle() { - // setup about section - const auto aboutText = []() { - auto aboutText = Theme::instance()->about(); - Theme::replaceLinkColorStringBackgroundAware(aboutText); - return aboutText; - }(); - _ui->infoAndUpdatesLabel->setText(aboutText); - -#if defined(BUILD_UPDATER) - // updater info - slotUpdateInfo(); -#else - _ui->updatesContainer->setVisible(false); -#endif -} - -void GeneralSettings::slotRemotePollIntervalChanged(int seconds) -{ - if (_currentlyLoading) { - return; - } - - ConfigFile cfgFile; - std::chrono::milliseconds interval(seconds * 1000); - cfgFile.setRemotePollInterval(interval); -} - -void GeneralSettings::updatePollIntervalVisibility() -{ - const auto accounts = AccountManager::instance()->accounts(); - const auto pushAvailable = std::any_of(accounts.cbegin(), accounts.cend(), [](const AccountStatePtr &accountState) -> bool { - if (!accountState) { - return false; - } - const auto accountPtr = accountState->account(); - if (!accountPtr) { - return false; - } - return accountPtr->capabilities().availablePushNotifications().testFlag(PushNotificationType::Files); - }); - - _ui->horizontalLayoutWidget_remotePollInterval->setVisible(!pushAvailable); + SettingsPanelStyle::apply(this); } } // namespace OCC diff --git a/src/gui/generalsettings.h b/src/gui/generalsettings.h index b62a7b3c07476..9b69b94e59d99 100644 --- a/src/gui/generalsettings.h +++ b/src/gui/generalsettings.h @@ -7,15 +7,9 @@ #ifndef MIRALL_GENERALSETTINGS_H #define MIRALL_GENERALSETTINGS_H -#include "config.h" - #include -#include namespace OCC { -class IgnoreListEditor; -class SyncLogDialog; -class AccountState; namespace Ui { class GeneralSettings; @@ -36,12 +30,6 @@ class GeneralSettings : public QWidget public slots: void slotStyleChanged(); -#if defined(BUILD_UPDATER) - void loadUpdateChannelsList(); - [[nodiscard]] QString updateChannelToLocalized(const QString &channel) const; - void setAndCheckNewUpdateChannel(const QString &newChannel); - void restoreUpdateChannel(); -#endif private slots: void saveMiscSettings(); @@ -50,30 +38,15 @@ private slots: void slotToggleChatNotifications(bool); void slotToggleCallNotifications(bool); void slotToggleQuotaWarningNotifications(bool); - void slotShowInExplorerNavigationPane(bool); - void slotIgnoreFilesEditor(); - void slotCreateDebugArchive(); void loadMiscSettings(); - void slotShowLegalNotice(); - void slotRemotePollIntervalChanged(int seconds); - void updatePollIntervalVisibility(); -#if defined(BUILD_UPDATER) - void slotUpdateInfo(); - void slotUpdateChannelChanged(); - void slotUpdateCheckNow(); - void slotToggleAutoUpdateCheck(); - void slotRestoreUpdateChannel(); -#endif private: void customizeStyle(); Ui::GeneralSettings *_ui; - QPointer _ignoreEditor; bool _currentlyLoading = false; - QStringList _currentUpdateChannelList; }; - } // namespace OCC + #endif // MIRALL_GENERALSETTINGS_H diff --git a/src/gui/generalsettings.ui b/src/gui/generalsettings.ui index b9dfc94a275ca..83eba393b3add 100644 --- a/src/gui/generalsettings.ui +++ b/src/gui/generalsettings.ui @@ -7,13 +7,13 @@ 0 0 667 - 796 + 300 Form - + 0 @@ -29,514 +29,240 @@ 12 - + - - - - - - true - - - - General settings - - - - - - - &Launch on System Startup - - - - - - - Show Call Notifications - - - - - - - For System Tray - - - Use &Monochrome Icons - - - - - - - Show Chat Notifications - - - - - - - Show Server &Notifications - - - - - - - Show &Quota Warning Notifications - - - - - - - - - - - - - - - - - true - - - - Advanced - - - - - - - 0 - - - - - - - Ask for confirmation before synchronizing new folders larger than - - - true - - - - - - - Qt::Orientation::Horizontal - - - QSizePolicy::Policy::Fixed - - - - 10 - 20 - - - - - - - - 999999 - - - 99 - - - - - - - MB - - - - - - - - - - - Qt::Orientation::Horizontal - - - QSizePolicy::Policy::Fixed - - - - 20 - 20 - - - - - - - - Notify when synchronised folders grow larger than specified limit - - - - - - - - - - - Qt::Orientation::Horizontal - - - QSizePolicy::Policy::Fixed - - - - 40 - 20 - - - - - - - - Automatically disable synchronisation of folders that overcome limit - - - - - - - + + + 0 + - + - + - Ask for confirmation before synchronizing external storages + &Launch on System Startup + + + autostartCheckBox - - - - - - - Move removed files to trash + + + Qt::Orientation::Horizontal - + - - - - - - - Show sync folders in &Explorer's navigation pane - - + - - - - - - Server poll interval - - - - - - - 5 - - - 999999 - - - 1 - - - + + + QFrame::Shape::HLine + + + QFrame::Shadow::Plain + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + - + - seconds (if <a href="https://github.com/nextcloud/notify_push">Client Push</a> is unavailable) + Use &Monochrome Icons - - Qt::TextFormat::RichText - - - true - - - Qt::TextInteractionFlag::LinksAccessibleByKeyboard|Qt::TextInteractionFlag::LinksAccessibleByMouse + + monoIconsCheckBox - + Qt::Orientation::Horizontal - - - 40 - 20 - - + + + + For System Tray + + + + + + + + + + + + + + 0 + - + - + - Edit &Ignored Files + Show Server &Notifications - - - - - - Create Debug Archive + + serverNotificationsCheckBox - + Qt::Orientation::Horizontal - - - 555 - 20 - - + + + - - - - - - - - - - - - - true - + + + QFrame::Shape::HLine - - Info + + QFrame::Shadow::Plain - - 10 - - - - - 0 - 0 - - - - Desktop client x.x.x - - + + + + + Show Chat Notifications + + + chatNotificationsCheckBox + + + + + + + Qt::Orientation::Horizontal + + + + + + + - - - - 0 - - - 1 - - - - - - - - 0 - 0 - - - - Update channel - - - - - - - - 0 - 0 - - - - - - - - - - - true - - - true - - - - - - - - 0 - 0 - - - - &Restart && Update - - - - - - - - - - - - 0 - 0 - - - - &Automatically check for updates - - - true - - - - - - - - 0 - 0 - - - - Check Now - - - - - - - Qt::Orientation::Horizontal - - - - 40 - 20 - - - - - - - + + + QFrame::Shape::HLine + + + QFrame::Shadow::Plain + - + - + - Usage Documentation + Show Call Notifications + + + callNotificationsCheckBox - - - Legal Notice + + + Qt::Orientation::Horizontal - + - - - false - + + + + + + + + QFrame::Shape::HLine + + + QFrame::Shadow::Plain + + + + + + + - Restore &Default + Show &Quota Warning Notifications + + + quotaWarningNotificationsCheckBox - + Qt::Orientation::Horizontal - - - 40 - 20 - - + + + - + Qt::Orientation::Vertical + + QSizePolicy::Policy::MinimumExpanding + 20 @@ -547,31 +273,21 @@ + + + OCC::SettingsSwitch + QAbstractButton +
settingsswitch.h
+
+
autostartCheckBox + monoIconsCheckBox serverNotificationsCheckBox - ignoredFilesButton - newFolderLimitCheckBox - newFolderLimitSpinBox - restartButton + chatNotificationsCheckBox + callNotificationsCheckBox + quotaWarningNotificationsCheckBox - - - newFolderLimitCheckBox - toggled(bool) - newFolderLimitSpinBox - setEnabled(bool) - - - 247 - 188 - - - 497 - 190 - - - - + diff --git a/src/gui/infosettings.cpp b/src/gui/infosettings.cpp new file mode 100644 index 0000000000000..16a6a31e7166c --- /dev/null +++ b/src/gui/infosettings.cpp @@ -0,0 +1,342 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "infosettings.h" +#include "ui_infosettings.h" + +#include "configfile.h" +#include "guiutility.h" +#include "legalnotice.h" +#include "owncloudgui.h" +#include "settingspanelstyle.h" +#include "theme.h" + +#if defined(BUILD_UPDATER) +#include "updater/updater.h" +#include "updater/ocupdater.h" +#ifdef Q_OS_MACOS +// FIXME We should unify those, but Sparkle does everything behind the scene transparently +#include "updater/sparkleupdater.h" +#endif +#endif + +#include +#include +#include +#include +#include +#include +#include + +namespace OCC { + +namespace { +QString aboutTextForInfoPanel() +{ + auto aboutText = Theme::instance()->about(); + Theme::replaceLinkColorStringBackgroundAware(aboutText); + aboutText.replace(QRegularExpression(QStringLiteral(R"( \(([^()]*)\)$)")), QStringLiteral("
(\\1)")); + return aboutText; +} +} + +InfoSettings::InfoSettings(QWidget *parent) + : QWidget(parent) + , _ui(new Ui::InfoSettings) +{ + _ui->setupUi(this); + + _ui->infoAndUpdatesLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); + _ui->infoAndUpdatesLabel->setText(aboutTextForInfoPanel()); + _ui->infoAndUpdatesLabel->setOpenExternalLinks(true); + + connect(_ui->legalNoticeButton, &QPushButton::clicked, this, &InfoSettings::slotShowLegalNotice); + + connect(_ui->usageDocumentationButton, &QPushButton::clicked, this, []() { + Utility::openBrowser(QUrl(Theme::instance()->helpUrl())); + }); + +#if defined(BUILD_UPDATER) + loadUpdateChannelsList(); +#endif + + customizeStyle(); +} + +InfoSettings::~InfoSettings() +{ + delete _ui; +} + +QSize InfoSettings::sizeHint() const +{ + return { + ownCloudGui::settingsDialogSize().width(), + QWidget::sizeHint().height() + }; +} + +#if defined(BUILD_UPDATER) +void InfoSettings::loadUpdateChannelsList() { + ConfigFile cfgFile; + if (cfgFile.serverHasValidSubscription()) { + _ui->updateChannel->hide(); + _ui->updateChannelLabel->hide(); + _ui->restoreUpdateChannelButton->hide(); + return; + } + + const auto validUpdateChannels = cfgFile.validUpdateChannels(); + const auto currentUpdateChannel = cfgFile.currentUpdateChannel(); + if (_currentUpdateChannelList.isEmpty() || _currentUpdateChannelList != validUpdateChannels){ + _currentUpdateChannelList = validUpdateChannels; + _ui->updateChannel->clear(); + _ui->updateChannel->addItems(_currentUpdateChannelList); + const auto currentUpdateChannelIndex = _currentUpdateChannelList.indexOf(currentUpdateChannel); + _ui->updateChannel->setCurrentIndex(currentUpdateChannelIndex != -1 ? currentUpdateChannelIndex : 0); + connect(_ui->updateChannel, &QComboBox::currentTextChanged, this, &InfoSettings::slotUpdateChannelChanged); + } + + const auto defaultUpdateChannel = cfgFile.defaultUpdateChannel(); + _ui->restoreUpdateChannelButton->setText(tr("Restore to &%1").arg(updateChannelToLocalized(defaultUpdateChannel))); + _ui->restoreUpdateChannelButton->setEnabled(currentUpdateChannel != defaultUpdateChannel); + connect(_ui->restoreUpdateChannelButton, &QPushButton::clicked, this, &InfoSettings::slotRestoreUpdateChannel); +} + +void InfoSettings::slotUpdateInfo() +{ + ConfigFile config; + const auto updater = Updater::instance(); + if (config.skipUpdateCheck() || !updater) { + _ui->updatesContainer->setVisible(false); + _ui->updatesGroupBox->setVisible(false); + return; + } + + _ui->updatesGroupBox->setVisible(true); + _ui->updatesContainer->setVisible(true); + + if (updater) { + connect(_ui->updateButton, + &QAbstractButton::clicked, + this, + &InfoSettings::slotUpdateCheckNow, + Qt::UniqueConnection); + connect(_ui->autoCheckForUpdatesCheckBox, &QAbstractButton::toggled, this, + &InfoSettings::slotToggleAutoUpdateCheck, Qt::UniqueConnection); + _ui->autoCheckForUpdatesCheckBox->setChecked(config.autoUpdateCheck()); + } + + const auto ocupdater = qobject_cast(updater); + if (ocupdater) { + connect(ocupdater, &OCUpdater::downloadStateChanged, this, &InfoSettings::slotUpdateInfo, Qt::UniqueConnection); + connect(_ui->restartButton, &QAbstractButton::clicked, ocupdater, &OCUpdater::slotStartInstaller, Qt::UniqueConnection); + + auto status = ocupdater->statusString(OCUpdater::UpdateStatusStringFormat::Html); + if (config.serverHasValidSubscription()) { + auto currentChannel = updateChannelToLocalized(config.currentUpdateChannel()); + if (currentChannel.isEmpty()) { + currentChannel = config.currentUpdateChannel(); + } + status.append(QStringLiteral("
%1") + .arg(tr("Connected to an enterprise system. Update channel (%1) cannot be changed.") + .arg(currentChannel))); + } + Theme::replaceLinkColorStringBackgroundAware(status); + + _ui->updateStateLabel->setOpenExternalLinks(false); + connect(_ui->updateStateLabel, &QLabel::linkActivated, this, [](const QString &link) { + Utility::openBrowser(QUrl(link)); + }); + _ui->updateStateLabel->setText(status); + _ui->restartButton->setVisible(ocupdater->downloadState() == OCUpdater::DownloadComplete); + _ui->updateButton->setEnabled(ocupdater->downloadState() != OCUpdater::CheckingServer && + ocupdater->downloadState() != OCUpdater::Downloading && + ocupdater->downloadState() != OCUpdater::DownloadComplete); + } +#if defined(Q_OS_MACOS) && defined(HAVE_SPARKLE) + else if (const auto sparkleUpdater = qobject_cast(updater)) { + connect(sparkleUpdater, &SparkleUpdater::statusChanged, this, &InfoSettings::slotUpdateInfo, Qt::UniqueConnection); + auto status = sparkleUpdater->statusString(); + if (config.serverHasValidSubscription()) { + const auto currentChannel = config.currentUpdateChannel(); + if (Qt::mightBeRichText(status)) { + status.append(QStringLiteral("
")); + } else { + status.append(QStringLiteral("\n")); + } + status.append(tr("Connected to an enterprise system. Update channel (%1) cannot be changed.") + .arg(currentChannel)); + } + _ui->updateStateLabel->setText(status); + _ui->restartButton->setVisible(false); + + const auto updaterState = sparkleUpdater->state(); + const auto enableUpdateButton = updaterState == SparkleUpdater::State::Idle || + updaterState == SparkleUpdater::State::Unknown; + _ui->updateButton->setEnabled(enableUpdateButton); + } +#endif +} + +void InfoSettings::setAndCheckNewUpdateChannel(const QString &newChannel) { + ConfigFile().setUpdateChannel(newChannel); + if (auto updater = qobject_cast(Updater::instance())) { + updater->setUpdateUrl(Updater::updateUrl()); + updater->checkForUpdate(); + } +#if defined(Q_OS_MACOS) && defined(HAVE_SPARKLE) + else if (auto updater = qobject_cast(Updater::instance())) { + updater->setUpdateUrl(Updater::updateUrl()); + updater->checkForUpdate(); + } +#endif +} + +QString InfoSettings::updateChannelToLocalized(const QString &channel) const +{ + if (channel == QStringLiteral("stable")) { + return tr("stable"); + } + + if (channel == QStringLiteral("beta")) { + return tr("beta"); + } + + if (channel == QStringLiteral("daily")) { + return tr("daily"); + } + + if (channel == QStringLiteral("enterprise")) { + return tr("enterprise"); + } + + return QString{}; +} + +void InfoSettings::slotUpdateChannelChanged() +{ + const auto updateChannelFromLocalized = [](const int index) { + switch(index) { + case 1: + return QStringLiteral("beta"); + case 2: + return QStringLiteral("daily"); + case 3: + return QStringLiteral("enterprise"); + default: + return QStringLiteral("stable"); + } + }; + + ConfigFile configFile; + const auto newChannel = updateChannelFromLocalized(_ui->updateChannel->currentIndex()); + const auto currentUpdateChannel = configFile.currentUpdateChannel(); + if (newChannel == currentUpdateChannel) { + return; + } + + if (newChannel == configFile.defaultUpdateChannel()) { + restoreUpdateChannel(); + return; + } + + _ui->restoreUpdateChannelButton->setEnabled(true); + + const auto nonEnterpriseOptions = tr("- beta: contains versions with new features that may not be tested thoroughly\n" + "- daily: contains versions created daily only for testing and development\n" + "\n" + "Downgrading versions is not possible immediately: changing from beta to stable means waiting for the new stable version.", + "list of available update channels to non enterprise users and downgrading warning"); + const auto enterpriseOptions = tr("- enterprise: contains stable versions for customers.\n" + "\n" + "Downgrading versions is not possible immediately: changing from stable to enterprise means waiting for the new enterprise version.", + "list of available update channels to enterprise users and downgrading warning"); + + auto msgBox = new QMessageBox( + QMessageBox::Warning, + tr("Changing update channel?"), + tr("The channel determines which upgrades will be offered to install:\n" + "- stable: contains tested versions considered reliable\n", + "starts list of available update channels, stable is always available") + .append(configFile.validUpdateChannels().contains("enterprise") ? enterpriseOptions : nonEnterpriseOptions), + QMessageBox::NoButton, + this); + const auto acceptButton = msgBox->addButton(tr("Change update channel"), QMessageBox::AcceptRole); + msgBox->addButton(tr("Cancel"), QMessageBox::RejectRole); + connect(msgBox, &QMessageBox::finished, msgBox, [this, newChannel, currentUpdateChannel, msgBox, acceptButton] { + msgBox->deleteLater(); + if (msgBox->clickedButton() == acceptButton) { + setAndCheckNewUpdateChannel(newChannel); + } else { + _ui->updateChannel->setCurrentText(updateChannelToLocalized(currentUpdateChannel)); + } + }); + msgBox->open(); +} + +void InfoSettings::slotUpdateCheckNow() +{ +#if defined(Q_OS_MACOS) && defined(HAVE_SPARKLE) + auto *updater = qobject_cast(Updater::instance()); +#else + auto *updater = qobject_cast(Updater::instance()); +#endif + if (ConfigFile().skipUpdateCheck()) { + updater = nullptr; + } + + if (updater) { + _ui->updateButton->setEnabled(false); + updater->checkForUpdate(); + } +} + +void InfoSettings::slotToggleAutoUpdateCheck() +{ + ConfigFile().setAutoUpdateCheck(_ui->autoCheckForUpdatesCheckBox->isChecked(), QString()); +} + +void InfoSettings::restoreUpdateChannel() +{ + const auto defaultUpdateChannel = ConfigFile().defaultUpdateChannel(); + _ui->restoreUpdateChannelButton->setEnabled(false); + _ui->updateChannel->setCurrentText(updateChannelToLocalized(defaultUpdateChannel)); + setAndCheckNewUpdateChannel(defaultUpdateChannel); +} + +void InfoSettings::slotRestoreUpdateChannel() +{ + restoreUpdateChannel(); +} +#endif // defined(BUILD_UPDATER) + +void InfoSettings::slotShowLegalNotice() +{ + auto notice = new LegalNotice(); + notice->exec(); + delete notice; +} + +void InfoSettings::slotStyleChanged() +{ + customizeStyle(); +} + +void InfoSettings::customizeStyle() +{ + SettingsPanelStyle::apply(this); + + _ui->infoAndUpdatesLabel->setText(aboutTextForInfoPanel()); + +#if defined(BUILD_UPDATER) + slotUpdateInfo(); +#else + _ui->updatesContainer->setVisible(false); + _ui->updatesGroupBox->setVisible(false); +#endif +} + +} // namespace OCC diff --git a/src/gui/infosettings.h b/src/gui/infosettings.h new file mode 100644 index 0000000000000..4b6ffaadb7363 --- /dev/null +++ b/src/gui/infosettings.h @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef INFOSETTINGS_H +#define INFOSETTINGS_H + +#include "config.h" + +#include +#include + +namespace OCC { + +namespace Ui { + class InfoSettings; +} + +class InfoSettings : public QWidget +{ + Q_OBJECT + +public: + explicit InfoSettings(QWidget *parent = nullptr); + ~InfoSettings() override; + [[nodiscard]] QSize sizeHint() const override; + +public slots: + void slotStyleChanged(); +#if defined(BUILD_UPDATER) + void loadUpdateChannelsList(); + [[nodiscard]] QString updateChannelToLocalized(const QString &channel) const; + void setAndCheckNewUpdateChannel(const QString &newChannel); + void restoreUpdateChannel(); +#endif + +private slots: + void slotShowLegalNotice(); +#if defined(BUILD_UPDATER) + void slotUpdateInfo(); + void slotUpdateChannelChanged(); + void slotUpdateCheckNow(); + void slotToggleAutoUpdateCheck(); + void slotRestoreUpdateChannel(); +#endif + +private: + void customizeStyle(); + + Ui::InfoSettings *_ui; + QStringList _currentUpdateChannelList; +}; + +} // namespace OCC + +#endif // INFOSETTINGS_H diff --git a/src/gui/infosettings.ui b/src/gui/infosettings.ui new file mode 100644 index 0000000000000..5fe4985f54411 --- /dev/null +++ b/src/gui/infosettings.ui @@ -0,0 +1,284 @@ + + + + OCC::InfoSettings + + + + 0 + 0 + 667 + 280 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + 12 + + + + + + + + + + + + 1 + 0 + + + + Desktop client x.x.x + + + true + + + + + + + Qt::Orientation::Horizontal + + + + 24 + 20 + + + + + + + + Usage Documentation + + + + + + + Legal Notice + + + + + + + + + + + + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + 1 + 0 + + + + + + + true + + + true + + + + + + + Qt::Orientation::Horizontal + + + + + + + + 0 + 0 + + + + Update channel + + + + + + + + 0 + 0 + + + + + + + + false + + + Restore &Default + + + + + + + + + QFrame::Shape::HLine + + + QFrame::Shadow::Plain + + + + + + + + + &Automatically check for updates + + + autoCheckForUpdatesCheckBox + + + + + + + Qt::Orientation::Horizontal + + + + + + + + 0 + 0 + + + + &Restart && Update + + + + + + + + 0 + 0 + + + + Check Now + + + + + + + true + + + + + + + + + + + + + + + Qt::Orientation::Vertical + + + QSizePolicy::Policy::MinimumExpanding + + + + 20 + 0 + + + + + + + + + OCC::SettingsSwitch + QAbstractButton +
settingsswitch.h
+
+
+ + usageDocumentationButton + legalNoticeButton + updateChannel + restoreUpdateChannelButton + restartButton + updateButton + autoCheckForUpdatesCheckBox + + + +
diff --git a/src/gui/networksettings.cpp b/src/gui/networksettings.cpp index e4edda10b4e71..d67d8b38b2169 100644 --- a/src/gui/networksettings.cpp +++ b/src/gui/networksettings.cpp @@ -46,7 +46,6 @@ NetworkSettings::NetworkSettings(const AccountPtr &account, QWidget *parent) _ui->downloadBox->setBackgroundRole(BACKGROUND_ROLE); _ui->uploadBox->setAutoFillBackground(true); _ui->uploadBox->setBackgroundRole(BACKGROUND_ROLE); - _ui->manualSettings->setVisible(_ui->manualProxyRadioButton->isChecked()); _ui->proxyGroupBox->setVisible(!Theme::instance()->doNotUseProxy()); diff --git a/src/gui/networksettings.ui b/src/gui/networksettings.ui index 3f75db910f510..93d832b12115b 100644 --- a/src/gui/networksettings.ui +++ b/src/gui/networksettings.ui @@ -169,14 +169,32 @@
- - - false - - - Proxy server requires authentication - - + + + + + Proxy server requires authentication + + + authRequiredcheckBox + + + + + + + Qt::Orientation::Horizontal + + + + + + + false + + + + @@ -438,6 +456,13 @@ + + + OCC::SettingsSwitch + QAbstractButton +
settingsswitch.h
+
+
diff --git a/src/gui/settingsdialog.cpp b/src/gui/settingsdialog.cpp index dd44d0cb11022..73be7c514d918 100644 --- a/src/gui/settingsdialog.cpp +++ b/src/gui/settingsdialog.cpp @@ -6,9 +6,11 @@ #include "settingsdialog.h" +#include "advancedsettings.h" #include "folderman.h" #include "theme.h" #include "generalsettings.h" +#include "infosettings.h" #include "networksettings.h" #include "accountsettings.h" #include "configfile.h" @@ -30,11 +32,15 @@ #include #include #include +#include +#include #include #include #include #include #include +#include +#include #include #include #include @@ -100,6 +106,7 @@ constexpr auto TOOLBAR_CSS = QLatin1String( const float buttonSizeRatio = 1.618f; // golden ratio constexpr auto settingsDialogDefaultWidth = 950; constexpr auto settingsDialogDefaultHeight = 500; +const auto settingsNavigationIconTextSpacing = QLatin1String(" "); /** display name with two lines that is displayed in the settings * If width is bigger than 0, the string will be ellided so it does not exceed that width @@ -180,33 +187,21 @@ SettingsDialog::SettingsDialog(ownCloudGui *gui, QWidget *parent) _actionGroup->setExclusive(true); connect(_actionGroup, &QActionGroup::triggered, this, &SettingsDialog::slotSwitchPage); - QAction *generalAction = createColorAwareAction(QLatin1String(":/client/theme/settings.svg"), tr("General")); - _actionGroup->addAction(generalAction); - _toolBar->addAction(generalAction); - auto *accountSpacer = new QWidget(this); - accountSpacer->setFixedHeight(16); - _toolBar->addWidget(accountSpacer); - _toolBar->addSeparator(); - auto *generalSettings = new GeneralSettings; - _stack->addWidget(generalSettings); _stack->setStyleSheet(QStringLiteral("QStackedWidget { background: transparent; }")); - // Connect styleChanged events to our widgets, so they can adapt (Dark-/Light-Mode switching) - connect(this, &SettingsDialog::styleChanged, generalSettings, &GeneralSettings::slotStyleChanged); - -#if defined(BUILD_UPDATER) - connect(AccountManager::instance(), &AccountManager::accountAdded, generalSettings, &GeneralSettings::loadUpdateChannelsList); - connect(AccountManager::instance(), &AccountManager::accountRemoved, generalSettings, &GeneralSettings::loadUpdateChannelsList); - connect(AccountManager::instance(), &AccountManager::capabilitiesChanged, generalSettings, &GeneralSettings::loadUpdateChannelsList); -#endif - - _actionGroupWidgets.insert(generalAction, generalSettings); - const auto accountsList = AccountManager::instance()->accounts(); for (const auto &account : accountsList) { accountAdded(account.data()); } + auto *accountSpacer = new QWidget(this); + accountSpacer->setFixedHeight(16); + _firstNonAccountAction = _toolBar->addWidget(accountSpacer); + + addSettingsPage(QLatin1String(":/client/theme/settings.svg"), tr("General"), new GeneralSettings(this)); + addSettingsPage(QLatin1String(":/client/theme/advanced.svg"), tr("Advanced"), new AdvancedSettings(this)); + addSettingsPage(QLatin1String(":/client/theme/info.svg"), tr("Info"), new InfoSettings(this), true); + QTimer::singleShot(1, this, &SettingsDialog::showFirstPage); auto *showLogWindow = new QAction(this); @@ -302,9 +297,12 @@ void SettingsDialog::showFirstPage() _initialAccount = nullptr; return; } - QList actions = _toolBar->actions(); - if (!actions.empty()) { - actions.first()->trigger(); + const QList actions = _toolBar->actions(); + for (auto *action : actions) { + if (_actionGroupWidgets.contains(action)) { + action->trigger(); + return; + } } } @@ -343,7 +341,11 @@ void SettingsDialog::accountAdded(AccountState *s) accountAction->setIconText(shortDisplayNameForSettings(s->account().data(), static_cast(height * buttonSizeRatio))); } - _toolBar->addAction(accountAction); + if (_firstNonAccountAction) { + _toolBar->insertAction(_firstNonAccountAction, accountAction); + } else { + _toolBar->addAction(accountAction); + } auto accountSettings = new AccountSettings(s, this); QString objectName = QLatin1String("accountSettings_"); objectName += s->account()->displayName(); @@ -448,6 +450,36 @@ void SettingsDialog::accountRemoved(AccountState *s) } } +void SettingsDialog::addSettingsPage(const QString &iconPath, const QString &title, QWidget *settingsPage, bool updateChannelAware) +{ + auto *settingsAction = createColorAwareAction(iconPath, title); + _actionGroup->addAction(settingsAction); + _toolBar->addAction(settingsAction); + + QString objectName = QLatin1String("settingsPage_"); + objectName += title; + settingsPage->setObjectName(objectName); + _stack->addWidget(settingsPage); + + if (auto *generalSettingsPage = qobject_cast(settingsPage)) { + connect(this, &SettingsDialog::styleChanged, generalSettingsPage, &GeneralSettings::slotStyleChanged); + } else if (auto *advancedSettingsPage = qobject_cast(settingsPage)) { + connect(this, &SettingsDialog::styleChanged, advancedSettingsPage, &AdvancedSettings::slotStyleChanged); + } else if (auto *infoSettingsPage = qobject_cast(settingsPage)) { + connect(this, &SettingsDialog::styleChanged, infoSettingsPage, &InfoSettings::slotStyleChanged); + +#if defined(BUILD_UPDATER) + if (updateChannelAware) { + connect(AccountManager::instance(), &AccountManager::accountAdded, infoSettingsPage, &InfoSettings::loadUpdateChannelsList); + connect(AccountManager::instance(), &AccountManager::accountRemoved, infoSettingsPage, &InfoSettings::loadUpdateChannelsList); + connect(AccountManager::instance(), &AccountManager::capabilitiesChanged, infoSettingsPage, &InfoSettings::loadUpdateChannelsList); + } +#endif + } + + _actionGroupWidgets.insert(settingsAction, settingsPage); +} + void SettingsDialog::customizeStyle() { if (_updatingStyle) { @@ -457,6 +489,14 @@ void SettingsDialog::customizeStyle() const QScopedValueRollback updatingStyle(_updatingStyle, true); _toolBar->setStyleSheet(TOOLBAR_CSS); + auto separatorColor = palette().color(QPalette::Mid); + separatorColor.setAlpha(48); + const auto separatorCss = QStringLiteral("rgba(%1, %2, %3, %4)") + .arg(separatorColor.red()) + .arg(separatorColor.green()) + .arg(separatorColor.blue()) + .arg(separatorColor.alpha()); + setStyleSheet(QStringLiteral( "#Settings { background: palette(window); border-radius: 0; }" @@ -467,17 +507,43 @@ void SettingsDialog::customizeStyle() "#settings_content, #settings_content_scroll { background: palette(window); border-radius: 12px; }" /* Panels */ - "#generalGroupBox, #advancedGroupBox, #aboutAndUpdatesGroupBox," - "#accountStatusPanel, #connectionSettingsPanel, #fileProviderPanel, #syncFoldersPanel {" + "#generalGroupBox, #notificationsGroupBox, #advancedGroupBox, #syncBehaviorGroupBox," + "#advancedActionsGroupBox, #aboutAndUpdatesGroupBox, #updatesGroupBox {" " background: palette(" BACKGROUND_PALETTE ");" - " border-radius: 10px;" + " border: none;" + " border-radius: 12px;" + " margin: 0px;" + " padding: 0px;" + " }" + "#accountStatusPanel, #encryptionPanel, #fileProviderPanel, #syncFoldersPanel {" + " background: palette(" BACKGROUND_PALETTE ");" + " border: none;" + " border-radius: 12px;" " margin: 0px;" " padding: 6px;" " }" - "#generalGroupBoxTitle, #advancedGroupBoxTitle, #aboutAndUpdatesGroupBoxTitle {" - " margin-bottom: 6px;" + "#generalGroupBox QLabel, #notificationsGroupBox QLabel, #advancedGroupBox QLabel," + "#syncBehaviorGroupBox QLabel, #advancedActionsGroupBox QLabel," + "#aboutAndUpdatesGroupBox QLabel, #updatesGroupBox QLabel {" + " margin: 0px;" + " padding: 0px;" + " }" + "#advancedGroupBox QSpinBox, #updatesGroupBox QComboBox {" + " min-height: 18px;" + " max-height: 20px;" + " }" + "#startupSeparator, #serverNotificationsSeparator, #chatNotificationsSeparator," + "#callNotificationsSeparator, #existingFolderLimitSeparator," + "#stopExistingFolderNowBigSyncSeparator, #remotePollIntervalSeparator," + "#moveFilesToTrashSeparator, #showInExplorerNavigationPaneSeparator," + "#updateControlsSeparator {" + " color: %1;" + " background: %1;" + " border: none;" + " min-height: 1px;" + " max-height: 1px;" " }" - )); + ).arg(separatorCss)); const auto &allActions = _actionGroup->actions(); for (const auto a : allActions) { @@ -508,7 +574,35 @@ class ToolButtonAction : public QWidgetAction return nullptr; } - auto *btn = new QToolButton(parent); + class SettingsNavigationButton : public QToolButton + { + public: + using QToolButton::QToolButton; + + protected: + void paintEvent(QPaintEvent *event) override + { + Q_UNUSED(event) + + QStyleOptionToolButton option; + initStyleOption(&option); + + QPainter painter(this); + style()->drawComplexControl(QStyle::CC_ToolButton, &option, &painter, this); + } + + private: + void initStyleOption(QStyleOptionToolButton *option) const + { + QToolButton::initStyleOption(option); + if (!option->text.isEmpty()) { + option->text.prepend(settingsNavigationIconTextSpacing); + option->text.replace(QLatin1Char('\n'), QLatin1Char('\n') + settingsNavigationIconTextSpacing); + } + } + }; + + auto *btn = new SettingsNavigationButton(parent); QString objectName = QLatin1String("settingsdialog_toolbutton_"); objectName += text(); btn->setObjectName(objectName); diff --git a/src/gui/settingsdialog.h b/src/gui/settingsdialog.h index e79a7e4840990..bdbcd70bb9ea1 100644 --- a/src/gui/settingsdialog.h +++ b/src/gui/settingsdialog.h @@ -71,6 +71,7 @@ private slots: void customizeStyle(); void requestStyleUpdate(); void updateAccountAvatar(const Account *account); + void addSettingsPage(const QString &iconPath, const QString &title, QWidget *settingsPage, bool updateChannelAware = false); QAction *createColorAwareAction(const QString &iconName, const QString &fileName); QAction *createActionWithIcon(const QIcon &icon, const QString &text, const QString &iconPath = QString()); @@ -85,6 +86,7 @@ private slots: QToolBar *_toolBar; QStackedWidget *_stack = nullptr; + QAction *_firstNonAccountAction = nullptr; #if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) QWidget *_windowDragHandle = nullptr; diff --git a/src/gui/settingspanelstyle.cpp b/src/gui/settingspanelstyle.cpp new file mode 100644 index 0000000000000..ee5cb83beb59c --- /dev/null +++ b/src/gui/settingspanelstyle.cpp @@ -0,0 +1,115 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "settingspanelstyle.h" + +#include "settingsswitch.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +constexpr auto rowLeftMargin = 12; +constexpr auto rowTopMargin = 12; +constexpr auto rowRightMargin = 12; +constexpr auto rowBottomMargin = 12; +constexpr auto rowSpacing = 8; +constexpr auto compactControlHeight = 20; + +bool isSeparator(const QWidget *widget) +{ + return widget && widget->objectName().endsWith(QLatin1String("Separator")); +} + +void applyPanelLayout(QLayout *layout) +{ + if (!layout) { + return; + } + + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); +} + +void applyRowLayout(QLayout *layout) +{ + if (!layout) { + return; + } + + layout->setContentsMargins(rowLeftMargin, rowTopMargin, rowRightMargin, rowBottomMargin); + layout->setSpacing(rowSpacing); +} + +void applyLayout(QLayout *layout) +{ + if (dynamic_cast(layout)) { + applyRowLayout(layout); + } else if (dynamic_cast(layout)) { + applyPanelLayout(layout); + } +} + +void applySeparator(QFrame *separator) +{ + separator->setFrameShape(QFrame::NoFrame); + separator->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + separator->setFixedHeight(1); +} + +void applyCompactSpinBox(QSpinBox *spinBox) +{ + spinBox->setMinimumHeight(compactControlHeight); + spinBox->setMaximumHeight(compactControlHeight); + spinBox->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed); +} +} // namespace + +namespace OCC::SettingsPanelStyle { + +void apply(QWidget *root) +{ + if (!root) { + return; + } + + const auto panels = root->findChildren(); + for (auto *panel : panels) { + applyLayout(panel->layout()); + + const auto layouts = panel->findChildren(); + for (auto *layout : layouts) { + if (layout != panel->layout()) { + applyLayout(layout); + } + } + + const auto frames = panel->findChildren(); + for (auto *frame : frames) { + if (isSeparator(frame)) { + applySeparator(frame); + } + } + + const auto spinBoxes = panel->findChildren(); + for (auto *spinBox : spinBoxes) { + applyCompactSpinBox(spinBox); + } + + const auto switches = panel->findChildren(); + for (auto *settingsSwitch : switches) { + settingsSwitch->setFixedSize(settingsSwitch->sizeHint()); + } + } +} + +} // namespace OCC::SettingsPanelStyle diff --git a/src/gui/settingspanelstyle.h b/src/gui/settingspanelstyle.h new file mode 100644 index 0000000000000..cd6559282b6d9 --- /dev/null +++ b/src/gui/settingspanelstyle.h @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef SETTINGSPANELSTYLE_H +#define SETTINGSPANELSTYLE_H + +class QWidget; + +namespace OCC::SettingsPanelStyle { + +void apply(QWidget *root); + +} // namespace OCC::SettingsPanelStyle + +#endif // SETTINGSPANELSTYLE_H diff --git a/src/gui/settingsswitch.cpp b/src/gui/settingsswitch.cpp new file mode 100644 index 0000000000000..d1c4f231f26f4 --- /dev/null +++ b/src/gui/settingsswitch.cpp @@ -0,0 +1,93 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "settingsswitch.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace OCC { + +SettingsSwitch::SettingsSwitch(QWidget *parent) + : QAbstractButton(parent) +{ + setCheckable(true); + setCursor(Qt::PointingHandCursor); + setFocusPolicy(Qt::StrongFocus); + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); +} + +QSize SettingsSwitch::sizeHint() const +{ + return {36, 20}; +} + +QSize SettingsSwitch::minimumSizeHint() const +{ + return sizeHint(); +} + +void SettingsSwitch::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event) + + constexpr qreal trackWidth = 36.0; + constexpr qreal trackHeight = 20.0; + constexpr qreal margin = 2.0; + constexpr qreal knobSize = trackHeight - 2.0 * margin; + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + const auto x = (width() - trackWidth) / 2.0; + const auto y = (height() - trackHeight) / 2.0; + const QRectF trackRect{x, y, trackWidth, trackHeight}; + +#ifdef Q_OS_MACOS + auto trackColor = isChecked() ? QColor(52, 120, 246) : QColor(209, 209, 214); +#else + auto trackColor = isChecked() ? palette().highlight().color() : palette().color(QPalette::Button); +#endif + auto knobColor = QColor(255, 255, 255); + auto knobShadowColor = QColor(0, 0, 0, 38); + if (!isEnabled()) { + trackColor.setAlphaF(isChecked() ? 0.45 : 0.35); + knobColor.setAlphaF(0.8); + knobShadowColor.setAlphaF(0.08); + } + + painter.setPen(Qt::NoPen); + painter.setBrush(trackColor); + painter.drawRoundedRect(trackRect, trackHeight / 2.0, trackHeight / 2.0); + + const auto knobX = isChecked() + ? trackRect.right() - margin - knobSize + : trackRect.left() + margin; + const QRectF knobRect{knobX, trackRect.top() + margin, knobSize, knobSize}; + + painter.setBrush(knobShadowColor); + painter.setPen(Qt::NoPen); + painter.drawEllipse(knobRect.translated(0.0, 0.6)); + + painter.setBrush(knobColor); + painter.setPen(Qt::NoPen); + painter.drawEllipse(knobRect); + + if (hasFocus()) { + auto focusColor = palette().highlight().color(); + focusColor.setAlphaF(0.35); + QPen focusPen(focusColor, 2.0); + painter.setPen(focusPen); + painter.setBrush(Qt::NoBrush); + painter.drawRoundedRect(trackRect.adjusted(-2.0, -2.0, 2.0, 2.0), trackHeight / 2.0 + 2.0, trackHeight / 2.0 + 2.0); + } +} + +} // namespace OCC diff --git a/src/gui/settingsswitch.h b/src/gui/settingsswitch.h new file mode 100644 index 0000000000000..e8e6a115e0e38 --- /dev/null +++ b/src/gui/settingsswitch.h @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef SETTINGSSWITCH_H +#define SETTINGSSWITCH_H + +#include +#include + +class QPaintEvent; + +namespace OCC { + +class SettingsSwitch : public QAbstractButton +{ + Q_OBJECT + +public: + explicit SettingsSwitch(QWidget *parent = nullptr); + + [[nodiscard]] QSize sizeHint() const override; + [[nodiscard]] QSize minimumSizeHint() const override; + +protected: + void paintEvent(QPaintEvent *event) override; +}; + +} // namespace OCC + +#endif // SETTINGSSWITCH_H diff --git a/theme.qrc.in b/theme.qrc.in index 8ebb6e3d7b590..398d2fb8d7007 100644 --- a/theme.qrc.in +++ b/theme.qrc.in @@ -271,6 +271,7 @@ theme/files.svg theme/public.svg theme/settings.svg + theme/advanced.svg theme/confirm.svg theme/copy.svg theme/more.svg diff --git a/theme/advanced.svg b/theme/advanced.svg new file mode 100644 index 0000000000000..238c4569bb7c2 --- /dev/null +++ b/theme/advanced.svg @@ -0,0 +1,5 @@ + +