From de0ed86b58d38b5aa4c55228f620717c718fc763 Mon Sep 17 00:00:00 2001 From: Rello Date: Mon, 3 Aug 2026 23:30:05 +0200 Subject: [PATCH 1/8] fix(tray): restore pause and resume sync action - Restores the global Pause sync for all / Resume sync for all action lost during the tray-menu migration. - Adds the action to both Qt and macOS root tray menus, between Add account and Settings. - Centralizes availability and behavior in Systray. - Hides the action when no classic sync folders exist, including macOS File Provider mode. - Preserves the previous behavior across all configured accounts. - Adds regression coverage for visibility, labels, and pause/resume behavior across two accounts. Assisted-by: Codex:GPT-5 Signed-off-by: Rello --- src/gui/macOS/trayaccountpopup/nctraypopup.mm | 17 ++ src/gui/systray.cpp | 22 +++ src/gui/systray.h | 12 ++ src/gui/trayaccountpopup_qt.cpp | 14 ++ test/CMakeLists.txt | 1 + test/testsystraysynccontrol.cpp | 149 ++++++++++++++++++ 6 files changed, 215 insertions(+) create mode 100644 test/testsystraysynccontrol.cpp diff --git a/src/gui/macOS/trayaccountpopup/nctraypopup.mm b/src/gui/macOS/trayaccountpopup/nctraypopup.mm index 670e25359b764..a10d452f754f2 100644 --- a/src/gui/macOS/trayaccountpopup/nctraypopup.mm +++ b/src/gui/macOS/trayaccountpopup/nctraypopup.mm @@ -215,6 +215,23 @@ - (void)populate [weakSelf closeAccountActionsPopup]; }]); } + const auto syncControlState = OCC::Systray::instance()->syncControlState(); + if (syncControlState != OCC::Systray::SyncControlState::Unavailable) { + const auto pausesSync = syncControlState == OCC::Systray::SyncControlState::Pause; + auto syncControlTitle = OCC::Systray::tr("Pause sync for all"); + if (!pausesSync) { + syncControlTitle = OCC::Systray::tr("Resume sync for all"); + } + addOwnedArrangedSubview(_stack, [[NCActionRow alloc] initWithTitle:syncControlTitle.toNSString() + width:kPopupWidth + enabled:YES + action:^{ + [weakSelf closeAllPopups]; + OCC::Systray::instance()->toggleSyncPaused(); + } hoverAction:^(NSView *) { + [weakSelf closeAccountActionsPopup]; + }]); + } addOwnedArrangedSubview(_stack, [[NCActionRow alloc] initWithTitle:OCC::Systray::tr("Settings").toNSString() width:kPopupWidth enabled:YES diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index e9459e41c2734..7501f1207dd78 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -1195,6 +1195,28 @@ bool Systray::anySyncFolders() const return _anySyncFolders; } +Systray::SyncControlState Systray::syncControlState() const +{ + if (!anySyncFolders()) { + return SyncControlState::Unavailable; + } + return syncIsPaused() ? SyncControlState::Resume : SyncControlState::Pause; +} + +void Systray::toggleSyncPaused() +{ + switch (syncControlState()) { + case SyncControlState::Pause: + setSyncIsPaused(true); + break; + case SyncControlState::Resume: + setSyncIsPaused(false); + break; + case SyncControlState::Unavailable: + break; + } +} + /********************************************************************************************/ /* Helper functions for cross-platform tray icon position and taskbar orientation detection */ /********************************************************************************************/ diff --git a/src/gui/systray.h b/src/gui/systray.h index e976b1bee88cf..ed76c5b56b351 100644 --- a/src/gui/systray.h +++ b/src/gui/systray.h @@ -83,6 +83,14 @@ class Systray : public QSystemTrayIcon enum class WindowPosition { Default, Center }; Q_ENUM(WindowPosition); + /** @brief Action offered by the global tray synchronization control. */ + enum class SyncControlState { + Unavailable, //!< No classic synchronization folders are configured. + Pause, //!< At least one classic synchronization folder is not paused. + Resume, //!< All classic synchronization folders are paused. + }; + Q_ENUM(SyncControlState); + enum class FileDetailsPage { Activity, Sharing }; Q_ENUM(FileDetailsPage); @@ -91,6 +99,8 @@ class Systray : public QSystemTrayIcon [[nodiscard]] bool syncIsPaused() const; [[nodiscard]] bool anySyncFolders() const; + /** @brief Returns the action that the global tray synchronization control should offer. */ + [[nodiscard]] SyncControlState syncControlState() const; [[nodiscard]] bool isOpen() const; [[nodiscard]] bool isActivitySurfaceVisible() const; void setTrayContextMenuVisible(const bool visible); @@ -159,6 +169,8 @@ public slots: void showUserStatusWindow(int userIndex); void setSyncIsPaused(const bool syncIsPaused); + /** @brief Pauses or resumes every configured classic synchronization folder. */ + void toggleSyncPaused(); void setIsOpen(const bool isOpen); void createShareDialog(const QString &localPath); diff --git a/src/gui/trayaccountpopup_qt.cpp b/src/gui/trayaccountpopup_qt.cpp index 8242b16fd2f41..d29b1fa0d16a7 100644 --- a/src/gui/trayaccountpopup_qt.cpp +++ b/src/gui/trayaccountpopup_qt.cpp @@ -874,6 +874,20 @@ void populateTrayMenu(QMenu *menu, Systray *systray) }); } + const auto syncControlState = systray->syncControlState(); + if (syncControlState != Systray::SyncControlState::Unavailable) { + const auto pausesSync = syncControlState == Systray::SyncControlState::Pause; + const auto syncControlIconUrl = pausesSync ? Theme::instance()->pause() : Theme::instance()->sync(); + const auto syncControlAction = addMenuAction(menu, + templateIconFromIcon(iconFromUrl(syncControlIconUrl), menuIconSize, menuIconPalette), + pausesSync ? Systray::tr("Pause sync for all") : Systray::tr("Resume sync for all")); + syncControlAction->setObjectName(QStringLiteral("traySyncControlAction")); + QObject::connect(syncControlAction, &QAction::triggered, syncControlAction, [systray] { + closeTrayPopup(); + systray->toggleSyncPaused(); + }); + } + const auto settingsAction = addMenuAction(menu, templateThemeIcon(QStringLiteral("settings.svg"), menuIconSize, menuIconPalette), Systray::tr("Settings")); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9f5aa21df81a8..5b6b966bec655 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -104,6 +104,7 @@ nextcloud_add_test(SetUserStatusDialog) nextcloud_add_test(TrayAccountMenuPolicy) nextcloud_add_test(TrayActivationPolicy) nextcloud_add_test(TrayAccountPopupPresentation) +nextcloud_add_test(SystraySyncControl) nextcloud_add_test(UnifiedSearchListmodel) nextcloud_add_test(ActivityListModel) nextcloud_add_test(SortedActivityListModel) diff --git a/test/testsystraysynccontrol.cpp b/test/testsystraysynccontrol.cpp new file mode 100644 index 0000000000000..7b0971a4b8745 --- /dev/null +++ b/test/testsystraysynccontrol.cpp @@ -0,0 +1,149 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: CC0-1.0 + * + * This software is in the public domain, furnished "as is", without technical + * support, and with no warranty, express or implied, as to its usefulness for + * any purpose. + */ + +#include + +#include +#include + +#include "account.h" +#include "accountmanager.h" +#include "configfile.h" +#include "folder.h" +#include "folderman.h" +#include "systray.h" + +#include "foldermantestutils.h" +#include "testhelper.h" + +using namespace OCC; + +class TestSystraySyncControl : public QObject +{ + Q_OBJECT + + QTemporaryDir _configDir; + QTemporaryDir _firstFolderDir; + QTemporaryDir _secondFolderDir; + FolderManTestHelper _folderManHelper; + AccountState *_firstAccountState = nullptr; + AccountState *_secondAccountState = nullptr; + Folder *_firstFolder = nullptr; + Folder *_secondFolder = nullptr; + + static AccountState *addTestAccount(const QString &url, const QString &user) + { + auto account = Account::create(); + account->setUrl(QUrl(url)); + account->setDavUser(user); + account->setCredentials(new HttpCredentialsTest(user, QStringLiteral("secret"))); + return AccountManager::instance()->addAccount(account); + } + +#ifndef Q_OS_MACOS + static QAction *syncControlAction(QMenu &menu, Systray *systray) + { + setupQtTrayContextMenu(&menu, systray); + return menu.findChild(QStringLiteral("traySyncControlAction")); + } +#endif + +private slots: + void initTestCase() + { + QVERIFY(_configDir.isValid()); + QVERIFY(_firstFolderDir.isValid()); + QVERIFY(_secondFolderDir.isValid()); + + QStandardPaths::setTestModeEnabled(true); + ConfigFile::setConfDir(_configDir.path()); + + _firstAccountState = addTestAccount(QStringLiteral("https://one.example.com"), QStringLiteral("alice")); + QVERIFY(_firstAccountState); + + Systray::instance()->create(); + } + + void cleanupTestCase() + { + const auto folderMan = FolderMan::instance(); + if (_firstFolder) { + folderMan->removeFolder(_firstFolder); + } + if (_secondFolder) { + folderMan->removeFolder(_secondFolder); + } + _firstFolder = nullptr; + _secondFolder = nullptr; + + if (_firstAccountState) { + AccountManager::instance()->removeAccountState(_firstAccountState); + } + if (_secondAccountState) { + AccountManager::instance()->removeAccountState(_secondAccountState); + } + _firstAccountState = nullptr; + _secondAccountState = nullptr; + } + + void globalActionIsHiddenWithoutClassicFoldersAndTogglesAllFolders() + { + const auto systray = Systray::instance(); + + // An account without classic folders is also the state used by a File Provider-only client. + QVERIFY(systray->syncControlState() == Systray::SyncControlState::Unavailable); + systray->toggleSyncPaused(); + QVERIFY(systray->syncControlState() == Systray::SyncControlState::Unavailable); +#ifndef Q_OS_MACOS + auto unavailableMenu = QMenu{}; + QVERIFY(!syncControlAction(unavailableMenu, systray)); +#endif + + _firstFolder = FolderMan::instance()->addFolder(_firstAccountState, folderDefinition(_firstFolderDir.path())); + QVERIFY(_firstFolder); + + _secondAccountState = addTestAccount(QStringLiteral("https://two.example.com"), QStringLiteral("bob")); + QVERIFY(_secondAccountState); + _secondFolder = FolderMan::instance()->addFolder(_secondAccountState, folderDefinition(_secondFolderDir.path())); + QVERIFY(_secondFolder); + + QVERIFY(systray->syncControlState() == Systray::SyncControlState::Pause); + +#ifndef Q_OS_MACOS + auto pauseMenu = QMenu{}; + const auto pauseAction = syncControlAction(pauseMenu, systray); + QVERIFY(pauseAction); + QCOMPARE(pauseAction->text(), Systray::tr("Pause sync for all")); + pauseAction->trigger(); +#else + systray->toggleSyncPaused(); +#endif + + QVERIFY(_firstFolder->syncPaused()); + QVERIFY(_secondFolder->syncPaused()); + QVERIFY(systray->syncControlState() == Systray::SyncControlState::Resume); + +#ifndef Q_OS_MACOS + auto resumeMenu = QMenu{}; + const auto resumeAction = syncControlAction(resumeMenu, systray); + QVERIFY(resumeAction); + QCOMPARE(resumeAction->text(), Systray::tr("Resume sync for all")); + resumeAction->trigger(); +#else + systray->toggleSyncPaused(); +#endif + + QVERIFY(!_firstFolder->syncPaused()); + QVERIFY(!_secondFolder->syncPaused()); + QVERIFY(systray->syncControlState() == Systray::SyncControlState::Pause); + } +}; + +QTEST_MAIN(TestSystraySyncControl) +#include "testsystraysynccontrol.moc" From de5ee1e2b3d1d41827d0b772124c09f9fdb175dd Mon Sep 17 00:00:00 2001 From: Rello Date: Tue, 4 Aug 2026 12:02:40 +0200 Subject: [PATCH 2/8] fix(tray): separate qt and maocOS test cases Assisted-by: Codex:GPT-5 Signed-off-by: Rello --- src/gui/macOS/trayaccountpopup/nctraypopup.mm | 7 +- test/CMakeLists.txt | 6 +- ...rol.cpp => systraysynccontroltesthelper.h} | 148 +++++++----------- test/testsystraysynccontrolmacos.cpp | 62 ++++++++ test/testsystraysynccontrolqt.cpp | 81 ++++++++++ 5 files changed, 211 insertions(+), 93 deletions(-) rename test/{testsystraysynccontrol.cpp => systraysynccontroltesthelper.h} (51%) create mode 100644 test/testsystraysynccontrolmacos.cpp create mode 100644 test/testsystraysynccontrolqt.cpp diff --git a/src/gui/macOS/trayaccountpopup/nctraypopup.mm b/src/gui/macOS/trayaccountpopup/nctraypopup.mm index a10d452f754f2..bd745b467b62a 100644 --- a/src/gui/macOS/trayaccountpopup/nctraypopup.mm +++ b/src/gui/macOS/trayaccountpopup/nctraypopup.mm @@ -218,10 +218,9 @@ - (void)populate const auto syncControlState = OCC::Systray::instance()->syncControlState(); if (syncControlState != OCC::Systray::SyncControlState::Unavailable) { const auto pausesSync = syncControlState == OCC::Systray::SyncControlState::Pause; - auto syncControlTitle = OCC::Systray::tr("Pause sync for all"); - if (!pausesSync) { - syncControlTitle = OCC::Systray::tr("Resume sync for all"); - } + const auto syncControlTitle = pausesSync + ? OCC::Systray::tr("Pause sync for all") + : OCC::Systray::tr("Resume sync for all"); addOwnedArrangedSubview(_stack, [[NCActionRow alloc] initWithTitle:syncControlTitle.toNSString() width:kPopupWidth enabled:YES diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 5b6b966bec655..8ea6fc554d3b1 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -104,7 +104,11 @@ nextcloud_add_test(SetUserStatusDialog) nextcloud_add_test(TrayAccountMenuPolicy) nextcloud_add_test(TrayActivationPolicy) nextcloud_add_test(TrayAccountPopupPresentation) -nextcloud_add_test(SystraySyncControl) +if(APPLE) + nextcloud_add_test(SystraySyncControlMacOS) +else() + nextcloud_add_test(SystraySyncControlQt) +endif() nextcloud_add_test(UnifiedSearchListmodel) nextcloud_add_test(ActivityListModel) nextcloud_add_test(SortedActivityListModel) diff --git a/test/testsystraysynccontrol.cpp b/test/systraysynccontroltesthelper.h similarity index 51% rename from test/testsystraysynccontrol.cpp rename to test/systraysynccontroltesthelper.h index 7b0971a4b8745..28ef984ed0ac1 100644 --- a/test/testsystraysynccontrol.cpp +++ b/test/systraysynccontroltesthelper.h @@ -7,10 +7,11 @@ * any purpose. */ -#include +#pragma once -#include +#include #include +#include #include "account.h" #include "accountmanager.h" @@ -24,53 +25,30 @@ using namespace OCC; -class TestSystraySyncControl : public QObject +/** @brief Provides the shared account and folder setup for systray sync-control tests. */ +class SystraySyncControlTestHelper { - Q_OBJECT - - QTemporaryDir _configDir; - QTemporaryDir _firstFolderDir; - QTemporaryDir _secondFolderDir; - FolderManTestHelper _folderManHelper; - AccountState *_firstAccountState = nullptr; - AccountState *_secondAccountState = nullptr; - Folder *_firstFolder = nullptr; - Folder *_secondFolder = nullptr; - - static AccountState *addTestAccount(const QString &url, const QString &user) +public: + /** @brief Initializes the test configuration and first account. */ + [[nodiscard]] bool initialize() { - auto account = Account::create(); - account->setUrl(QUrl(url)); - account->setDavUser(user); - account->setCredentials(new HttpCredentialsTest(user, QStringLiteral("secret"))); - return AccountManager::instance()->addAccount(account); - } - -#ifndef Q_OS_MACOS - static QAction *syncControlAction(QMenu &menu, Systray *systray) - { - setupQtTrayContextMenu(&menu, systray); - return menu.findChild(QStringLiteral("traySyncControlAction")); - } -#endif - -private slots: - void initTestCase() - { - QVERIFY(_configDir.isValid()); - QVERIFY(_firstFolderDir.isValid()); - QVERIFY(_secondFolderDir.isValid()); + if (!_configDir.isValid() || !_firstFolderDir.isValid() || !_secondFolderDir.isValid()) { + return false; + } QStandardPaths::setTestModeEnabled(true); ConfigFile::setConfDir(_configDir.path()); - _firstAccountState = addTestAccount(QStringLiteral("https://one.example.com"), QStringLiteral("alice")); - QVERIFY(_firstAccountState); + // User construction references the Systray singleton, so initialize the tray before adding accounts. + const auto systray = Systray::instance(); + systray->create(); - Systray::instance()->create(); + _firstAccountState = addTestAccount(QStringLiteral("https://one.example.com"), QStringLiteral("alice")); + return _firstAccountState != nullptr; } - void cleanupTestCase() + /** @brief Removes the folders and accounts created by the helper. */ + void cleanup() { const auto folderMan = FolderMan::instance(); if (_firstFolder) { @@ -92,58 +70,52 @@ private slots: _secondAccountState = nullptr; } - void globalActionIsHiddenWithoutClassicFoldersAndTogglesAllFolders() + /** @brief Adds one classic sync folder to each of two accounts. */ + [[nodiscard]] bool addClassicFolders() { - const auto systray = Systray::instance(); - - // An account without classic folders is also the state used by a File Provider-only client. - QVERIFY(systray->syncControlState() == Systray::SyncControlState::Unavailable); - systray->toggleSyncPaused(); - QVERIFY(systray->syncControlState() == Systray::SyncControlState::Unavailable); -#ifndef Q_OS_MACOS - auto unavailableMenu = QMenu{}; - QVERIFY(!syncControlAction(unavailableMenu, systray)); -#endif - _firstFolder = FolderMan::instance()->addFolder(_firstAccountState, folderDefinition(_firstFolderDir.path())); - QVERIFY(_firstFolder); + if (!_firstFolder) { + return false; + } _secondAccountState = addTestAccount(QStringLiteral("https://two.example.com"), QStringLiteral("bob")); - QVERIFY(_secondAccountState); + if (!_secondAccountState) { + return false; + } + _secondFolder = FolderMan::instance()->addFolder(_secondAccountState, folderDefinition(_secondFolderDir.path())); - QVERIFY(_secondFolder); - - QVERIFY(systray->syncControlState() == Systray::SyncControlState::Pause); - -#ifndef Q_OS_MACOS - auto pauseMenu = QMenu{}; - const auto pauseAction = syncControlAction(pauseMenu, systray); - QVERIFY(pauseAction); - QCOMPARE(pauseAction->text(), Systray::tr("Pause sync for all")); - pauseAction->trigger(); -#else - systray->toggleSyncPaused(); -#endif - - QVERIFY(_firstFolder->syncPaused()); - QVERIFY(_secondFolder->syncPaused()); - QVERIFY(systray->syncControlState() == Systray::SyncControlState::Resume); - -#ifndef Q_OS_MACOS - auto resumeMenu = QMenu{}; - const auto resumeAction = syncControlAction(resumeMenu, systray); - QVERIFY(resumeAction); - QCOMPARE(resumeAction->text(), Systray::tr("Resume sync for all")); - resumeAction->trigger(); -#else - systray->toggleSyncPaused(); -#endif - - QVERIFY(!_firstFolder->syncPaused()); - QVERIFY(!_secondFolder->syncPaused()); - QVERIFY(systray->syncControlState() == Systray::SyncControlState::Pause); + return _secondFolder != nullptr; + } + + /** @brief Returns the first classic sync folder, or null before it is added. */ + [[nodiscard]] Folder *firstFolder() const + { + return _firstFolder; + } + + /** @brief Returns the second classic sync folder, or null before it is added. */ + [[nodiscard]] Folder *secondFolder() const + { + return _secondFolder; + } + +private: + /** @brief Adds an account configured with test credentials. */ + static AccountState *addTestAccount(const QString &url, const QString &user) + { + auto account = Account::create(); + account->setUrl(QUrl(url)); + account->setDavUser(user); + account->setCredentials(new HttpCredentialsTest(user, QStringLiteral("secret"))); + return AccountManager::instance()->addAccount(account); } -}; -QTEST_MAIN(TestSystraySyncControl) -#include "testsystraysynccontrol.moc" + QTemporaryDir _configDir; + QTemporaryDir _firstFolderDir; + QTemporaryDir _secondFolderDir; + FolderManTestHelper _folderManHelper; + AccountState *_firstAccountState = nullptr; + AccountState *_secondAccountState = nullptr; + Folder *_firstFolder = nullptr; + Folder *_secondFolder = nullptr; +}; diff --git a/test/testsystraysynccontrolmacos.cpp b/test/testsystraysynccontrolmacos.cpp new file mode 100644 index 0000000000000..7c521e7899e3c --- /dev/null +++ b/test/testsystraysynccontrolmacos.cpp @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: CC0-1.0 + * + * This software is in the public domain, furnished "as is", without technical + * support, and with no warranty, express or implied, as to its usefulness for + * any purpose. + */ + +#include + +#include "systray.h" + +#include "systraysynccontroltesthelper.h" + +using namespace OCC; + +class TestSystraySyncControlMacOS : public QObject +{ + Q_OBJECT + + SystraySyncControlTestHelper _helper; + +private slots: + void initTestCase() + { + QVERIFY(_helper.initialize()); + } + + void cleanupTestCase() + { + _helper.cleanup(); + } + + void globalControlIsUnavailableWithoutClassicFoldersAndTogglesAllFolders() + { + const auto systray = Systray::instance(); + + // An account without classic folders is also the state used by a File Provider-only client. + QVERIFY(systray->syncControlState() == Systray::SyncControlState::Unavailable); + systray->toggleSyncPaused(); + QVERIFY(systray->syncControlState() == Systray::SyncControlState::Unavailable); + + QVERIFY(_helper.addClassicFolders()); + QVERIFY(systray->syncControlState() == Systray::SyncControlState::Pause); + + systray->toggleSyncPaused(); + + QVERIFY(_helper.firstFolder()->syncPaused()); + QVERIFY(_helper.secondFolder()->syncPaused()); + QVERIFY(systray->syncControlState() == Systray::SyncControlState::Resume); + + systray->toggleSyncPaused(); + + QVERIFY(!_helper.firstFolder()->syncPaused()); + QVERIFY(!_helper.secondFolder()->syncPaused()); + QVERIFY(systray->syncControlState() == Systray::SyncControlState::Pause); + } +}; + +QTEST_MAIN(TestSystraySyncControlMacOS) +#include "testsystraysynccontrolmacos.moc" diff --git a/test/testsystraysynccontrolqt.cpp b/test/testsystraysynccontrolqt.cpp new file mode 100644 index 0000000000000..094f9868b6a03 --- /dev/null +++ b/test/testsystraysynccontrolqt.cpp @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: CC0-1.0 + * + * This software is in the public domain, furnished "as is", without technical + * support, and with no warranty, express or implied, as to its usefulness for + * any purpose. + */ + +#include + +#include + +#include "systray.h" + +#include "systraysynccontroltesthelper.h" + +using namespace OCC; + +class TestSystraySyncControlQt : public QObject +{ + Q_OBJECT + + SystraySyncControlTestHelper _helper; + + static QAction *syncControlAction(QMenu &menu, Systray *systray) + { + setupQtTrayContextMenu(&menu, systray); + return menu.findChild(QStringLiteral("traySyncControlAction")); + } + +private slots: + void initTestCase() + { + QVERIFY(_helper.initialize()); + } + + void cleanupTestCase() + { + _helper.cleanup(); + } + + void globalActionIsHiddenWithoutClassicFoldersAndTogglesAllFolders() + { + const auto systray = Systray::instance(); + + // An account without classic folders is also the state used by a File Provider-only client. + QVERIFY(systray->syncControlState() == Systray::SyncControlState::Unavailable); + systray->toggleSyncPaused(); + QVERIFY(systray->syncControlState() == Systray::SyncControlState::Unavailable); + + auto unavailableMenu = QMenu{}; + QVERIFY(!syncControlAction(unavailableMenu, systray)); + + QVERIFY(_helper.addClassicFolders()); + QVERIFY(systray->syncControlState() == Systray::SyncControlState::Pause); + + auto pauseMenu = QMenu{}; + const auto pauseAction = syncControlAction(pauseMenu, systray); + QVERIFY(pauseAction); + QCOMPARE(pauseAction->text(), Systray::tr("Pause sync for all")); + pauseAction->trigger(); + + QVERIFY(_helper.firstFolder()->syncPaused()); + QVERIFY(_helper.secondFolder()->syncPaused()); + QVERIFY(systray->syncControlState() == Systray::SyncControlState::Resume); + + auto resumeMenu = QMenu{}; + const auto resumeAction = syncControlAction(resumeMenu, systray); + QVERIFY(resumeAction); + QCOMPARE(resumeAction->text(), Systray::tr("Resume sync for all")); + resumeAction->trigger(); + + QVERIFY(!_helper.firstFolder()->syncPaused()); + QVERIFY(!_helper.secondFolder()->syncPaused()); + QVERIFY(systray->syncControlState() == Systray::SyncControlState::Pause); + } +}; + +QTEST_MAIN(TestSystraySyncControlQt) +#include "testsystraysynccontrolqt.moc" From 175a452ab2beb63982672c8ceb6372f98f42fc66 Mon Sep 17 00:00:00 2001 From: Rello Date: Tue, 11 Aug 2026 16:30:11 +0200 Subject: [PATCH 3/8] Exclude macOS files from clang-tidy analysis Exclude macOS files from clang-tidy analysis Signed-off-by: Rello --- .github/workflows/clang-tidy-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index fd0425ffd4ad9..3b142c35dd9fa 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -24,7 +24,7 @@ jobs: - name: Analyze run: | /usr/bin/git config --global --add safe.directory "$GITHUB_WORKSPACE" - /usr/bin/git diff -U0 HEAD^ | clang-tidy-diff-21.py -checks='-*,modernize-use-auto,modernize-use-using,modernize-use-nodiscard,modernize-use-nullptr,modernize-use-override,cppcoreguidelines-pro-type-static-cast-downcast' -p1 -path build -regex '^(?!.*/macOS/)(?!.*/MacOSX/).*\.(cpp|cc|cxx|c|h|hpp|hxx)$' -export-fixes clang-tidy-result/fixes.yml + /usr/bin/git diff -U0 HEAD^ -- ':(exclude)test/macOS/*' ':(exclude)*macOS*' | clang-tidy-diff-21.py -checks='-*,modernize-use-auto,modernize-use-using,modernize-use-nodiscard,modernize-use-nullptr,modernize-use-override,cppcoreguidelines-pro-type-static-cast-downcast' -p1 -path build -regex '^(?!.*/macOS/)(?!.*/MacOSX/).*\.(cpp|cc|cxx|c|h|hpp|hxx)$' -export-fixes clang-tidy-result/fixes.yml - name: Run clang-tidy-pr-comments action uses: platisd/clang-tidy-pr-comments@28cfb84edafa771c044bde7e4a2a3fae57463818 # v1.6.1 # >1.4.3 switches to composite method w/ a forced python version and breaks things: https://github.com/actions/setup-python/issues/871 with: From a71b9c95a577271040c466c4a873c8617ef43f22 Mon Sep 17 00:00:00 2001 From: Rello Date: Wed, 12 Aug 2026 10:11:38 +0200 Subject: [PATCH 4/8] fix(test): move test into macOS subfolder Assisted-by: Codex:GPT-5 Signed-off-by: Rello --- test/CMakeLists.txt | 4 +-- test/macOS/CMakeLists.txt | 1 + .../testsystraysynccontrolmacos.cpp | 29 +++++++++++++++---- 3 files changed, 26 insertions(+), 8 deletions(-) rename test/{ => macOS}/testsystraysynccontrolmacos.cpp (61%) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 8ea6fc554d3b1..d4f20a481c0e9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -104,9 +104,7 @@ nextcloud_add_test(SetUserStatusDialog) nextcloud_add_test(TrayAccountMenuPolicy) nextcloud_add_test(TrayActivationPolicy) nextcloud_add_test(TrayAccountPopupPresentation) -if(APPLE) - nextcloud_add_test(SystraySyncControlMacOS) -else() +if(NOT APPLE) nextcloud_add_test(SystraySyncControlQt) endif() nextcloud_add_test(UnifiedSearchListmodel) diff --git a/test/macOS/CMakeLists.txt b/test/macOS/CMakeLists.txt index fa9584a79a7ab..ab994b8fb5ab4 100644 --- a/test/macOS/CMakeLists.txt +++ b/test/macOS/CMakeLists.txt @@ -5,4 +5,5 @@ # no GUI, so it runs in PR CI — unlike the signed-bundle checks, which need a signing identity # and a login session and therefore belong to the release pipeline. nextcloud_add_test(FinderSyncBrokerIdentity) +nextcloud_add_test(SystraySyncControlMacOS) nextcloud_add_test(MacSandboxUtility) diff --git a/test/testsystraysynccontrolmacos.cpp b/test/macOS/testsystraysynccontrolmacos.cpp similarity index 61% rename from test/testsystraysynccontrolmacos.cpp rename to test/macOS/testsystraysynccontrolmacos.cpp index 7c521e7899e3c..c08cc38349f5f 100644 --- a/test/testsystraysynccontrolmacos.cpp +++ b/test/macOS/testsystraysynccontrolmacos.cpp @@ -32,30 +32,49 @@ private slots: _helper.cleanup(); } - void globalControlIsUnavailableWithoutClassicFoldersAndTogglesAllFolders() + void globalControlIsUnavailableWithoutClassicFoldersAndPausesAndResumesAllFolders() { const auto systray = Systray::instance(); // An account without classic folders is also the state used by a File Provider-only client. QVERIFY(systray->syncControlState() == Systray::SyncControlState::Unavailable); - systray->toggleSyncPaused(); - QVERIFY(systray->syncControlState() == Systray::SyncControlState::Unavailable); QVERIFY(_helper.addClassicFolders()); QVERIFY(systray->syncControlState() == Systray::SyncControlState::Pause); - systray->toggleSyncPaused(); + systray->setSyncIsPaused(true); QVERIFY(_helper.firstFolder()->syncPaused()); QVERIFY(_helper.secondFolder()->syncPaused()); QVERIFY(systray->syncControlState() == Systray::SyncControlState::Resume); - systray->toggleSyncPaused(); + systray->setSyncIsPaused(false); QVERIFY(!_helper.firstFolder()->syncPaused()); QVERIFY(!_helper.secondFolder()->syncPaused()); QVERIFY(systray->syncControlState() == Systray::SyncControlState::Pause); } + + void partiallyPausedFoldersCanPauseAndResumeAll() + { + const auto systray = Systray::instance(); + + _helper.firstFolder()->setSyncPaused(true); + QVERIFY(systray->syncControlState() == Systray::SyncControlState::PauseAndResume); + + systray->setSyncIsPaused(false); + QVERIFY(!_helper.firstFolder()->syncPaused()); + QVERIFY(!_helper.secondFolder()->syncPaused()); + QVERIFY(systray->syncControlState() == Systray::SyncControlState::Pause); + + _helper.firstFolder()->setSyncPaused(true); + QVERIFY(systray->syncControlState() == Systray::SyncControlState::PauseAndResume); + + systray->setSyncIsPaused(true); + QVERIFY(_helper.firstFolder()->syncPaused()); + QVERIFY(_helper.secondFolder()->syncPaused()); + QVERIFY(systray->syncControlState() == Systray::SyncControlState::Resume); + } }; QTEST_MAIN(TestSystraySyncControlMacOS) From c3ec30aa827b6dc81ade9e803e4277048e76ab57 Mon Sep 17 00:00:00 2001 From: Rello Date: Wed, 12 Aug 2026 10:33:59 +0200 Subject: [PATCH 5/8] fix(tray): handle partially paused sync folders Show both pause-all and resume-all actions when paused and running folders coexist, preserving the previous tray behavior. Assisted-by: Codex:GPT-5 Signed-off-by: Rello --- src/gui/macOS/trayaccountpopup/nctraypopup.mm | 47 ++++++++++++----- src/gui/systray.cpp | 25 ++++----- src/gui/systray.h | 9 ++-- src/gui/trayaccountpopup_qt.cpp | 25 +++++++-- test/testsystraysynccontrolqt.cpp | 52 ++++++++++++++++--- 5 files changed, 113 insertions(+), 45 deletions(-) diff --git a/src/gui/macOS/trayaccountpopup/nctraypopup.mm b/src/gui/macOS/trayaccountpopup/nctraypopup.mm index bd745b467b62a..ba656b9ae467e 100644 --- a/src/gui/macOS/trayaccountpopup/nctraypopup.mm +++ b/src/gui/macOS/trayaccountpopup/nctraypopup.mm @@ -77,6 +77,24 @@ - (void)clearActiveAccountRow _activeAccountRow = nil; } +/** @brief Adds a root action that pauses or resumes every classic synchronization folder. */ +- (void)addSyncControlRowPausing:(BOOL)pausesSync +{ + __unsafe_unretained NCTrayPopup *weakSelf = self; + const auto syncControlTitle = pausesSync + ? OCC::Systray::tr("Pause sync for all") + : OCC::Systray::tr("Resume sync for all"); + addOwnedArrangedSubview(_stack, [[NCActionRow alloc] initWithTitle:syncControlTitle.toNSString() + width:kPopupWidth + enabled:YES + action:^{ + [weakSelf closeAllPopups]; + OCC::Systray::instance()->setSyncIsPaused(pausesSync); + } hoverAction:^(NSView *) { + [weakSelf closeAccountActionsPopup]; + }]); +} + - (NCAccountRow *)makeRowForIndex:(int)index name:(NSString *)name server:(NSString *)server @@ -215,22 +233,23 @@ - (void)populate [weakSelf closeAccountActionsPopup]; }]); } + const auto syncControlState = OCC::Systray::instance()->syncControlState(); - if (syncControlState != OCC::Systray::SyncControlState::Unavailable) { - const auto pausesSync = syncControlState == OCC::Systray::SyncControlState::Pause; - const auto syncControlTitle = pausesSync - ? OCC::Systray::tr("Pause sync for all") - : OCC::Systray::tr("Resume sync for all"); - addOwnedArrangedSubview(_stack, [[NCActionRow alloc] initWithTitle:syncControlTitle.toNSString() - width:kPopupWidth - enabled:YES - action:^{ - [weakSelf closeAllPopups]; - OCC::Systray::instance()->toggleSyncPaused(); - } hoverAction:^(NSView *) { - [weakSelf closeAccountActionsPopup]; - }]); + switch (syncControlState) { + case OCC::Systray::SyncControlState::Pause: + [self addSyncControlRowPausing:YES]; + break; + case OCC::Systray::SyncControlState::Resume: + [self addSyncControlRowPausing:NO]; + break; + case OCC::Systray::SyncControlState::PauseAndResume: + [self addSyncControlRowPausing:YES]; + [self addSyncControlRowPausing:NO]; + break; + case OCC::Systray::SyncControlState::Unavailable: + break; } + addOwnedArrangedSubview(_stack, [[NCActionRow alloc] initWithTitle:OCC::Systray::tr("Settings").toNSString() width:kPopupWidth enabled:YES diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index 7501f1207dd78..7e843e3e39555 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -1197,24 +1197,21 @@ bool Systray::anySyncFolders() const Systray::SyncControlState Systray::syncControlState() const { - if (!anySyncFolders()) { + const auto folders = FolderMan::instance()->map(); + if (folders.isEmpty()) { return SyncControlState::Unavailable; } - return syncIsPaused() ? SyncControlState::Resume : SyncControlState::Pause; -} -void Systray::toggleSyncPaused() -{ - switch (syncControlState()) { - case SyncControlState::Pause: - setSyncIsPaused(true); - break; - case SyncControlState::Resume: - setSyncIsPaused(false); - break; - case SyncControlState::Unavailable: - break; + const auto anyPaused = std::any_of(std::cbegin(folders), std::cend(folders), [](const Folder *folder) { + return folder->syncPaused(); + }); + const auto anyRunning = std::any_of(std::cbegin(folders), std::cend(folders), [](const Folder *folder) { + return !folder->syncPaused(); + }); + if (anyPaused && anyRunning) { + return SyncControlState::PauseAndResume; } + return anyPaused ? SyncControlState::Resume : SyncControlState::Pause; } /********************************************************************************************/ diff --git a/src/gui/systray.h b/src/gui/systray.h index ed76c5b56b351..8a910a3734f4b 100644 --- a/src/gui/systray.h +++ b/src/gui/systray.h @@ -83,11 +83,12 @@ class Systray : public QSystemTrayIcon enum class WindowPosition { Default, Center }; Q_ENUM(WindowPosition); - /** @brief Action offered by the global tray synchronization control. */ + /** @brief Actions offered by the global tray synchronization control. */ enum class SyncControlState { Unavailable, //!< No classic synchronization folders are configured. - Pause, //!< At least one classic synchronization folder is not paused. + Pause, //!< All classic synchronization folders are running. Resume, //!< All classic synchronization folders are paused. + PauseAndResume, //!< Some classic synchronization folders are paused and others are running. }; Q_ENUM(SyncControlState); @@ -99,7 +100,7 @@ class Systray : public QSystemTrayIcon [[nodiscard]] bool syncIsPaused() const; [[nodiscard]] bool anySyncFolders() const; - /** @brief Returns the action that the global tray synchronization control should offer. */ + /** @brief Returns the actions that the global tray synchronization control should offer. */ [[nodiscard]] SyncControlState syncControlState() const; [[nodiscard]] bool isOpen() const; [[nodiscard]] bool isActivitySurfaceVisible() const; @@ -169,8 +170,6 @@ public slots: void showUserStatusWindow(int userIndex); void setSyncIsPaused(const bool syncIsPaused); - /** @brief Pauses or resumes every configured classic synchronization folder. */ - void toggleSyncPaused(); void setIsOpen(const bool isOpen); void createShareDialog(const QString &localPath); diff --git a/src/gui/trayaccountpopup_qt.cpp b/src/gui/trayaccountpopup_qt.cpp index d29b1fa0d16a7..6f29d305cd5a7 100644 --- a/src/gui/trayaccountpopup_qt.cpp +++ b/src/gui/trayaccountpopup_qt.cpp @@ -875,17 +875,32 @@ void populateTrayMenu(QMenu *menu, Systray *systray) } const auto syncControlState = systray->syncControlState(); - if (syncControlState != Systray::SyncControlState::Unavailable) { - const auto pausesSync = syncControlState == Systray::SyncControlState::Pause; + const auto addSyncControlAction = [menu, systray, &menuIconPalette](const bool pausesSync) { const auto syncControlIconUrl = pausesSync ? Theme::instance()->pause() : Theme::instance()->sync(); const auto syncControlAction = addMenuAction(menu, templateIconFromIcon(iconFromUrl(syncControlIconUrl), menuIconSize, menuIconPalette), pausesSync ? Systray::tr("Pause sync for all") : Systray::tr("Resume sync for all")); - syncControlAction->setObjectName(QStringLiteral("traySyncControlAction")); - QObject::connect(syncControlAction, &QAction::triggered, syncControlAction, [systray] { + syncControlAction->setObjectName(pausesSync + ? QStringLiteral("trayPauseSyncAction") + : QStringLiteral("trayResumeSyncAction")); + QObject::connect(syncControlAction, &QAction::triggered, syncControlAction, [systray, pausesSync] { closeTrayPopup(); - systray->toggleSyncPaused(); + systray->setSyncIsPaused(pausesSync); }); + }; + switch (syncControlState) { + case Systray::SyncControlState::Pause: + addSyncControlAction(true); + break; + case Systray::SyncControlState::Resume: + addSyncControlAction(false); + break; + case Systray::SyncControlState::PauseAndResume: + addSyncControlAction(true); + addSyncControlAction(false); + break; + case Systray::SyncControlState::Unavailable: + break; } const auto settingsAction = addMenuAction(menu, diff --git a/test/testsystraysynccontrolqt.cpp b/test/testsystraysynccontrolqt.cpp index 094f9868b6a03..40b518fe0c6d7 100644 --- a/test/testsystraysynccontrolqt.cpp +++ b/test/testsystraysynccontrolqt.cpp @@ -23,10 +23,15 @@ class TestSystraySyncControlQt : public QObject SystraySyncControlTestHelper _helper; - static QAction *syncControlAction(QMenu &menu, Systray *systray) + static QAction *pauseSyncAction(QMenu &menu, Systray *systray) { setupQtTrayContextMenu(&menu, systray); - return menu.findChild(QStringLiteral("traySyncControlAction")); + return menu.findChild(QStringLiteral("trayPauseSyncAction")); + } + + static QAction *resumeSyncAction(const QMenu &menu) + { + return menu.findChild(QStringLiteral("trayResumeSyncAction")); } private slots: @@ -46,18 +51,18 @@ private slots: // An account without classic folders is also the state used by a File Provider-only client. QVERIFY(systray->syncControlState() == Systray::SyncControlState::Unavailable); - systray->toggleSyncPaused(); - QVERIFY(systray->syncControlState() == Systray::SyncControlState::Unavailable); auto unavailableMenu = QMenu{}; - QVERIFY(!syncControlAction(unavailableMenu, systray)); + QVERIFY(!pauseSyncAction(unavailableMenu, systray)); + QVERIFY(!resumeSyncAction(unavailableMenu)); QVERIFY(_helper.addClassicFolders()); QVERIFY(systray->syncControlState() == Systray::SyncControlState::Pause); auto pauseMenu = QMenu{}; - const auto pauseAction = syncControlAction(pauseMenu, systray); + const auto pauseAction = pauseSyncAction(pauseMenu, systray); QVERIFY(pauseAction); + QVERIFY(!resumeSyncAction(pauseMenu)); QCOMPARE(pauseAction->text(), Systray::tr("Pause sync for all")); pauseAction->trigger(); @@ -66,14 +71,47 @@ private slots: QVERIFY(systray->syncControlState() == Systray::SyncControlState::Resume); auto resumeMenu = QMenu{}; - const auto resumeAction = syncControlAction(resumeMenu, systray); + QVERIFY(!pauseSyncAction(resumeMenu, systray)); + const auto resumeAction = resumeSyncAction(resumeMenu); + QVERIFY(resumeAction); + QCOMPARE(resumeAction->text(), Systray::tr("Resume sync for all")); + resumeAction->trigger(); + + QVERIFY(!_helper.firstFolder()->syncPaused()); + QVERIFY(!_helper.secondFolder()->syncPaused()); + QVERIFY(systray->syncControlState() == Systray::SyncControlState::Pause); + } + + void partiallyPausedFoldersOfferPauseAndResume() + { + const auto systray = Systray::instance(); + + _helper.firstFolder()->setSyncPaused(true); + QVERIFY(systray->syncControlState() == Systray::SyncControlState::PauseAndResume); + + auto resumeMenu = QMenu{}; + const auto pauseAction = pauseSyncAction(resumeMenu, systray); + const auto resumeAction = resumeSyncAction(resumeMenu); + QVERIFY(pauseAction); QVERIFY(resumeAction); + QCOMPARE(pauseAction->text(), Systray::tr("Pause sync for all")); QCOMPARE(resumeAction->text(), Systray::tr("Resume sync for all")); resumeAction->trigger(); QVERIFY(!_helper.firstFolder()->syncPaused()); QVERIFY(!_helper.secondFolder()->syncPaused()); QVERIFY(systray->syncControlState() == Systray::SyncControlState::Pause); + + _helper.firstFolder()->setSyncPaused(true); + auto pauseMenu = QMenu{}; + const auto mixedPauseAction = pauseSyncAction(pauseMenu, systray); + QVERIFY(mixedPauseAction); + QVERIFY(resumeSyncAction(pauseMenu)); + mixedPauseAction->trigger(); + + QVERIFY(_helper.firstFolder()->syncPaused()); + QVERIFY(_helper.secondFolder()->syncPaused()); + QVERIFY(systray->syncControlState() == Systray::SyncControlState::Resume); } }; From be78eaa9aa12f72095d0e655bf33a9ead793db2e Mon Sep 17 00:00:00 2001 From: Rello Date: Wed, 12 Aug 2026 11:18:14 +0200 Subject: [PATCH 6/8] fix(tray): Fixed the missing lambda capture Assisted-by: Codex:GPT-5 Signed-off-by: Rello --- src/gui/trayaccountpopup_qt.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/trayaccountpopup_qt.cpp b/src/gui/trayaccountpopup_qt.cpp index 6f29d305cd5a7..db8b021c43223 100644 --- a/src/gui/trayaccountpopup_qt.cpp +++ b/src/gui/trayaccountpopup_qt.cpp @@ -875,7 +875,7 @@ void populateTrayMenu(QMenu *menu, Systray *systray) } const auto syncControlState = systray->syncControlState(); - const auto addSyncControlAction = [menu, systray, &menuIconPalette](const bool pausesSync) { + const auto addSyncControlAction = [menu, systray, &menuIconPalette, &menuIconSize](const bool pausesSync) { const auto syncControlIconUrl = pausesSync ? Theme::instance()->pause() : Theme::instance()->sync(); const auto syncControlAction = addMenuAction(menu, templateIconFromIcon(iconFromUrl(syncControlIconUrl), menuIconSize, menuIconPalette), From 8ad4f1165054726b8b59157f5caab5099c031f32 Mon Sep 17 00:00:00 2001 From: Rello Date: Thu, 13 Aug 2026 14:32:11 +0200 Subject: [PATCH 7/8] fix(test): adjustment for clang test Assisted-by: Codex:GPT-5 Signed-off-by: Rello --- .github/workflows/clang-tidy-review.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index 3b142c35dd9fa..34c28b254079b 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -24,7 +24,9 @@ jobs: - name: Analyze run: | /usr/bin/git config --global --add safe.directory "$GITHUB_WORKSPACE" - /usr/bin/git diff -U0 HEAD^ -- ':(exclude)test/macOS/*' ':(exclude)*macOS*' | clang-tidy-diff-21.py -checks='-*,modernize-use-auto,modernize-use-using,modernize-use-nodiscard,modernize-use-nullptr,modernize-use-override,cppcoreguidelines-pro-type-static-cast-downcast' -p1 -path build -regex '^(?!.*/macOS/)(?!.*/MacOSX/).*\.(cpp|cc|cxx|c|h|hpp|hxx)$' -export-fixes clang-tidy-result/fixes.yml + # Header-only test helper: clang-tidy-diff has no target compile command for it. + # Its consumer test targets compile it with the required GUI include paths. + /usr/bin/git diff -U0 HEAD^ -- ':(exclude)test/macOS/*' ':(exclude)test/systraysynccontroltesthelper.h' ':(exclude)*macOS*' | clang-tidy-diff-21.py -checks='-*,modernize-use-auto,modernize-use-using,modernize-use-nodiscard,modernize-use-nullptr,modernize-use-override,cppcoreguidelines-pro-type-static-cast-downcast' -p1 -path build -regex '^(?!.*/macOS/)(?!.*/MacOSX/).*\.(cpp|cc|cxx|c|h|hpp|hxx)$' -export-fixes clang-tidy-result/fixes.yml - name: Run clang-tidy-pr-comments action uses: platisd/clang-tidy-pr-comments@28cfb84edafa771c044bde7e4a2a3fae57463818 # v1.6.1 # >1.4.3 switches to composite method w/ a forced python version and breaks things: https://github.com/actions/setup-python/issues/871 with: From ff872778fec30180e8e2da20c23a8a2130f97539 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 13 Aug 2026 16:53:02 +0200 Subject: [PATCH 8/8] fix: ensure FolderMan singleton is initialized when needed Signed-off-by: Matthieu Gallien --- src/gui/folderman.h | 2 ++ test/testbrowserreauthcontroller.cpp | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/src/gui/folderman.h b/src/gui/folderman.h index e5722a25dd7ac..129b5082fe4c1 100644 --- a/src/gui/folderman.h +++ b/src/gui/folderman.h @@ -29,6 +29,7 @@ class TestSyncConflictsModel; class TestRemoteWipe; class FolderManTestHelper; class TestFileActionsModel; +class TestBrowserReAuthController; namespace OCC { @@ -416,6 +417,7 @@ private slots: friend class ::TestRemoteWipe; friend class ::FolderManTestHelper; friend class ::TestFileActionsModel; + friend class ::TestBrowserReAuthController; }; } // namespace OCC diff --git a/test/testbrowserreauthcontroller.cpp b/test/testbrowserreauthcontroller.cpp index a38ab80a1d1ca..224f7fe6b37eb 100644 --- a/test/testbrowserreauthcontroller.cpp +++ b/test/testbrowserreauthcontroller.cpp @@ -135,14 +135,23 @@ class TestBrowserReAuthController : public QObject { Q_OBJECT + std::unique_ptr _fm; + private slots: void initTestCase() { + OCC::Logger::instance()->setLogFlush(true); + OCC::Logger::instance()->setLogDebug(true); + QStandardPaths::setTestModeEnabled(true); + Q_INIT_RESOURCE(resources); Q_INIT_RESOURCE(theme); qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "UserModel", UserModel::instance()); qmlRegisterSingletonInstance("com.nextcloud.desktopclient", 1, 0, "Theme", Theme::instance()); + + _fm.reset(new FolderMan{}); + Systray::instance()->setTrayEngine(new QQmlApplicationEngine(QCoreApplication::instance())); }