Skip to content

Commit b8238c8

Browse files
committed
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 <github@scherello.de>
1 parent e9fff81 commit b8238c8

6 files changed

Lines changed: 215 additions & 0 deletions

File tree

src/gui/macOS/trayaccountpopup/nctraypopup.mm

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,23 @@ - (void)populate
215215
[weakSelf closeAccountActionsPopup];
216216
}]);
217217
}
218+
const auto syncControlState = OCC::Systray::instance()->syncControlState();
219+
if (syncControlState != OCC::Systray::SyncControlState::Unavailable) {
220+
const auto pausesSync = syncControlState == OCC::Systray::SyncControlState::Pause;
221+
auto syncControlTitle = OCC::Systray::tr("Pause sync for all");
222+
if (!pausesSync) {
223+
syncControlTitle = OCC::Systray::tr("Resume sync for all");
224+
}
225+
addOwnedArrangedSubview(_stack, [[NCActionRow alloc] initWithTitle:syncControlTitle.toNSString()
226+
width:kPopupWidth
227+
enabled:YES
228+
action:^{
229+
[weakSelf closeAllPopups];
230+
OCC::Systray::instance()->toggleSyncPaused();
231+
} hoverAction:^(NSView *) {
232+
[weakSelf closeAccountActionsPopup];
233+
}]);
234+
}
218235
addOwnedArrangedSubview(_stack, [[NCActionRow alloc] initWithTitle:OCC::Systray::tr("Settings").toNSString()
219236
width:kPopupWidth
220237
enabled:YES

src/gui/systray.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,6 +1194,28 @@ bool Systray::anySyncFolders() const
11941194
return _anySyncFolders;
11951195
}
11961196

1197+
Systray::SyncControlState Systray::syncControlState() const
1198+
{
1199+
if (!anySyncFolders()) {
1200+
return SyncControlState::Unavailable;
1201+
}
1202+
return syncIsPaused() ? SyncControlState::Resume : SyncControlState::Pause;
1203+
}
1204+
1205+
void Systray::toggleSyncPaused()
1206+
{
1207+
switch (syncControlState()) {
1208+
case SyncControlState::Pause:
1209+
setSyncIsPaused(true);
1210+
break;
1211+
case SyncControlState::Resume:
1212+
setSyncIsPaused(false);
1213+
break;
1214+
case SyncControlState::Unavailable:
1215+
break;
1216+
}
1217+
}
1218+
11971219
/********************************************************************************************/
11981220
/* Helper functions for cross-platform tray icon position and taskbar orientation detection */
11991221
/********************************************************************************************/

src/gui/systray.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ class Systray : public QSystemTrayIcon
8383
enum class WindowPosition { Default, Center };
8484
Q_ENUM(WindowPosition);
8585

86+
/** @brief Action offered by the global tray synchronization control. */
87+
enum class SyncControlState {
88+
Unavailable, //!< No classic synchronization folders are configured.
89+
Pause, //!< At least one classic synchronization folder is not paused.
90+
Resume, //!< All classic synchronization folders are paused.
91+
};
92+
Q_ENUM(SyncControlState);
93+
8694
enum class FileDetailsPage { Activity, Sharing };
8795
Q_ENUM(FileDetailsPage);
8896

@@ -91,6 +99,8 @@ class Systray : public QSystemTrayIcon
9199

92100
[[nodiscard]] bool syncIsPaused() const;
93101
[[nodiscard]] bool anySyncFolders() const;
102+
/** @brief Returns the action that the global tray synchronization control should offer. */
103+
[[nodiscard]] SyncControlState syncControlState() const;
94104
[[nodiscard]] bool isOpen() const;
95105
[[nodiscard]] bool isActivitySurfaceVisible() const;
96106
void setTrayContextMenuVisible(const bool visible);
@@ -159,6 +169,8 @@ public slots:
159169
void showUserStatusWindow(int userIndex);
160170

161171
void setSyncIsPaused(const bool syncIsPaused);
172+
/** @brief Pauses or resumes every configured classic synchronization folder. */
173+
void toggleSyncPaused();
162174
void setIsOpen(const bool isOpen);
163175

164176
void createShareDialog(const QString &localPath);

src/gui/trayaccountpopup_qt.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,20 @@ void populateTrayMenu(QMenu *menu, Systray *systray)
874874
});
875875
}
876876

877+
const auto syncControlState = systray->syncControlState();
878+
if (syncControlState != Systray::SyncControlState::Unavailable) {
879+
const auto pausesSync = syncControlState == Systray::SyncControlState::Pause;
880+
const auto syncControlIconUrl = pausesSync ? Theme::instance()->pause() : Theme::instance()->sync();
881+
const auto syncControlAction = addMenuAction(menu,
882+
templateIconFromIcon(iconFromUrl(syncControlIconUrl), menuIconSize, menuIconPalette),
883+
pausesSync ? Systray::tr("Pause sync for all") : Systray::tr("Resume sync for all"));
884+
syncControlAction->setObjectName(QStringLiteral("traySyncControlAction"));
885+
QObject::connect(syncControlAction, &QAction::triggered, syncControlAction, [systray] {
886+
closeTrayPopup();
887+
systray->toggleSyncPaused();
888+
});
889+
}
890+
877891
const auto settingsAction = addMenuAction(menu,
878892
templateThemeIcon(QStringLiteral("settings.svg"), menuIconSize, menuIconPalette),
879893
Systray::tr("Settings"));

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ nextcloud_add_test(IconUtils)
9898
nextcloud_add_test(NotificationSoundPlayer)
9999
nextcloud_add_test(SetUserStatusDialog)
100100
nextcloud_add_test(TrayAccountMenuPolicy)
101+
nextcloud_add_test(SystraySyncControl)
101102
nextcloud_add_test(UnifiedSearchListmodel)
102103
nextcloud_add_test(ActivityListModel)
103104
nextcloud_add_test(SortedActivityListModel)

test/testsystraysynccontrol.cpp

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: CC0-1.0
4+
*
5+
* This software is in the public domain, furnished "as is", without technical
6+
* support, and with no warranty, express or implied, as to its usefulness for
7+
* any purpose.
8+
*/
9+
10+
#include <QtTest>
11+
12+
#include <QMenu>
13+
#include <QTemporaryDir>
14+
15+
#include "account.h"
16+
#include "accountmanager.h"
17+
#include "configfile.h"
18+
#include "folder.h"
19+
#include "folderman.h"
20+
#include "systray.h"
21+
22+
#include "foldermantestutils.h"
23+
#include "testhelper.h"
24+
25+
using namespace OCC;
26+
27+
class TestSystraySyncControl : public QObject
28+
{
29+
Q_OBJECT
30+
31+
QTemporaryDir _configDir;
32+
QTemporaryDir _firstFolderDir;
33+
QTemporaryDir _secondFolderDir;
34+
FolderManTestHelper _folderManHelper;
35+
AccountState *_firstAccountState = nullptr;
36+
AccountState *_secondAccountState = nullptr;
37+
Folder *_firstFolder = nullptr;
38+
Folder *_secondFolder = nullptr;
39+
40+
static AccountState *addTestAccount(const QString &url, const QString &user)
41+
{
42+
auto account = Account::create();
43+
account->setUrl(QUrl(url));
44+
account->setDavUser(user);
45+
account->setCredentials(new HttpCredentialsTest(user, QStringLiteral("secret")));
46+
return AccountManager::instance()->addAccount(account);
47+
}
48+
49+
#ifndef Q_OS_MACOS
50+
static QAction *syncControlAction(QMenu &menu, Systray *systray)
51+
{
52+
setupQtTrayContextMenu(&menu, systray);
53+
return menu.findChild<QAction *>(QStringLiteral("traySyncControlAction"));
54+
}
55+
#endif
56+
57+
private slots:
58+
void initTestCase()
59+
{
60+
QVERIFY(_configDir.isValid());
61+
QVERIFY(_firstFolderDir.isValid());
62+
QVERIFY(_secondFolderDir.isValid());
63+
64+
QStandardPaths::setTestModeEnabled(true);
65+
ConfigFile::setConfDir(_configDir.path());
66+
67+
_firstAccountState = addTestAccount(QStringLiteral("https://one.example.com"), QStringLiteral("alice"));
68+
QVERIFY(_firstAccountState);
69+
70+
Systray::instance()->create();
71+
}
72+
73+
void cleanupTestCase()
74+
{
75+
const auto folderMan = FolderMan::instance();
76+
if (_firstFolder) {
77+
folderMan->removeFolder(_firstFolder);
78+
}
79+
if (_secondFolder) {
80+
folderMan->removeFolder(_secondFolder);
81+
}
82+
_firstFolder = nullptr;
83+
_secondFolder = nullptr;
84+
85+
if (_firstAccountState) {
86+
AccountManager::instance()->removeAccountState(_firstAccountState);
87+
}
88+
if (_secondAccountState) {
89+
AccountManager::instance()->removeAccountState(_secondAccountState);
90+
}
91+
_firstAccountState = nullptr;
92+
_secondAccountState = nullptr;
93+
}
94+
95+
void globalActionIsHiddenWithoutClassicFoldersAndTogglesAllFolders()
96+
{
97+
const auto systray = Systray::instance();
98+
99+
// An account without classic folders is also the state used by a File Provider-only client.
100+
QVERIFY(systray->syncControlState() == Systray::SyncControlState::Unavailable);
101+
systray->toggleSyncPaused();
102+
QVERIFY(systray->syncControlState() == Systray::SyncControlState::Unavailable);
103+
#ifndef Q_OS_MACOS
104+
auto unavailableMenu = QMenu{};
105+
QVERIFY(!syncControlAction(unavailableMenu, systray));
106+
#endif
107+
108+
_firstFolder = FolderMan::instance()->addFolder(_firstAccountState, folderDefinition(_firstFolderDir.path()));
109+
QVERIFY(_firstFolder);
110+
111+
_secondAccountState = addTestAccount(QStringLiteral("https://two.example.com"), QStringLiteral("bob"));
112+
QVERIFY(_secondAccountState);
113+
_secondFolder = FolderMan::instance()->addFolder(_secondAccountState, folderDefinition(_secondFolderDir.path()));
114+
QVERIFY(_secondFolder);
115+
116+
QVERIFY(systray->syncControlState() == Systray::SyncControlState::Pause);
117+
118+
#ifndef Q_OS_MACOS
119+
auto pauseMenu = QMenu{};
120+
const auto pauseAction = syncControlAction(pauseMenu, systray);
121+
QVERIFY(pauseAction);
122+
QCOMPARE(pauseAction->text(), Systray::tr("Pause sync for all"));
123+
pauseAction->trigger();
124+
#else
125+
systray->toggleSyncPaused();
126+
#endif
127+
128+
QVERIFY(_firstFolder->syncPaused());
129+
QVERIFY(_secondFolder->syncPaused());
130+
QVERIFY(systray->syncControlState() == Systray::SyncControlState::Resume);
131+
132+
#ifndef Q_OS_MACOS
133+
auto resumeMenu = QMenu{};
134+
const auto resumeAction = syncControlAction(resumeMenu, systray);
135+
QVERIFY(resumeAction);
136+
QCOMPARE(resumeAction->text(), Systray::tr("Resume sync for all"));
137+
resumeAction->trigger();
138+
#else
139+
systray->toggleSyncPaused();
140+
#endif
141+
142+
QVERIFY(!_firstFolder->syncPaused());
143+
QVERIFY(!_secondFolder->syncPaused());
144+
QVERIFY(systray->syncControlState() == Systray::SyncControlState::Pause);
145+
}
146+
};
147+
148+
QTEST_MAIN(TestSystraySyncControl)
149+
#include "testsystraysynccontrol.moc"

0 commit comments

Comments
 (0)