Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/clang-tidy-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ 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
# 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:
Expand Down
2 changes: 2 additions & 0 deletions src/gui/folderman.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#ifndef FOLDERMAN_H
#define FOLDERMAN_H

#include <QByteArray>

Check failure on line 10 in src/gui/folderman.h

View workflow job for this annotation

GitHub Actions / build

src/gui/folderman.h:10:10 [clang-diagnostic-error]

'QByteArray' file not found
#include <QObject>
#include <QQueue>
#include <QList>
Expand All @@ -29,6 +29,7 @@
class TestRemoteWipe;
class FolderManTestHelper;
class TestFileActionsModel;
class TestBrowserReAuthController;

Check warning on line 32 in src/gui/folderman.h

View workflow job for this annotation

GitHub Actions / build

src/gui/folderman.h:32:7 [cppcoreguidelines-avoid-non-const-global-variables]

variable 'TestBrowserReAuthController' is non-const and globally accessible, consider making it const

namespace OCC {

Expand Down Expand Up @@ -416,6 +417,7 @@
friend class ::TestRemoteWipe;
friend class ::FolderManTestHelper;
friend class ::TestFileActionsModel;
friend class ::TestBrowserReAuthController;
};

} // namespace OCC
Expand Down
35 changes: 35 additions & 0 deletions src/gui/macOS/trayaccountpopup/nctraypopup.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -215,6 +233,23 @@ - (void)populate
[weakSelf closeAccountActionsPopup];
}]);
}

const auto syncControlState = OCC::Systray::instance()->syncControlState();
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
Expand Down
19 changes: 19 additions & 0 deletions src/gui/systray.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,25 @@ bool Systray::anySyncFolders() const
return _anySyncFolders;
}

Systray::SyncControlState Systray::syncControlState() const
{
const auto folders = FolderMan::instance()->map();
if (folders.isEmpty()) {
return SyncControlState::Unavailable;
}

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;
}

/********************************************************************************************/
/* Helper functions for cross-platform tray icon position and taskbar orientation detection */
/********************************************************************************************/
Expand Down
11 changes: 11 additions & 0 deletions src/gui/systray.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ class Systray : public QSystemTrayIcon
enum class WindowPosition { Default, Center };
Q_ENUM(WindowPosition);

/** @brief Actions offered by the global tray synchronization control. */
enum class SyncControlState {
Unavailable, //!< No classic synchronization folders are configured.
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);

enum class FileDetailsPage { Activity, Sharing };
Q_ENUM(FileDetailsPage);

Expand All @@ -91,6 +100,8 @@ class Systray : public QSystemTrayIcon

[[nodiscard]] bool syncIsPaused() const;
[[nodiscard]] bool anySyncFolders() const;
/** @brief Returns the actions 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);
Expand Down
29 changes: 29 additions & 0 deletions src/gui/trayaccountpopup_qt.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
Expand Down Expand Up @@ -874,6 +874,35 @@
});
}

const auto syncControlState = systray->syncControlState();
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),
pausesSync ? Systray::tr("Pause sync for all") : Systray::tr("Resume sync for all"));
syncControlAction->setObjectName(pausesSync
? QStringLiteral("trayPauseSyncAction")
: QStringLiteral("trayResumeSyncAction"));
QObject::connect(syncControlAction, &QAction::triggered, syncControlAction, [systray, pausesSync] {
closeTrayPopup();
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,
templateThemeIcon(QStringLiteral("settings.svg"), menuIconSize, menuIconPalette),
Systray::tr("Settings"));
Expand Down
3 changes: 3 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ nextcloud_add_test(SetUserStatusDialog)
nextcloud_add_test(TrayAccountMenuPolicy)
nextcloud_add_test(TrayActivationPolicy)
nextcloud_add_test(TrayAccountPopupPresentation)
if(NOT APPLE)
nextcloud_add_test(SystraySyncControlQt)
endif()
nextcloud_add_test(UnifiedSearchListmodel)
nextcloud_add_test(ActivityListModel)
nextcloud_add_test(SortedActivityListModel)
Expand Down
1 change: 1 addition & 0 deletions test/macOS/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
81 changes: 81 additions & 0 deletions test/macOS/testsystraysynccontrolmacos.cpp
Original file line number Diff line number Diff line change
@@ -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 <QtTest>

Check failure on line 10 in test/macOS/testsystraysynccontrolmacos.cpp

View workflow job for this annotation

GitHub Actions / build

test/macOS/testsystraysynccontrolmacos.cpp:10:10 [clang-diagnostic-error]

'QtTest' file not found

#include "systray.h"

#include "systraysynccontroltesthelper.h"

using namespace OCC;

class TestSystraySyncControlMacOS : public QObject

Check warning on line 18 in test/macOS/testsystraysynccontrolmacos.cpp

View workflow job for this annotation

GitHub Actions / build

test/macOS/testsystraysynccontrolmacos.cpp:18:7 [cppcoreguidelines-pro-type-member-init]

constructor does not initialize these fields: SystraySyncControlTestHelper,
{
Q_OBJECT

SystraySyncControlTestHelper _helper;

private slots:
void initTestCase()
{
QVERIFY(_helper.initialize());
}

void cleanupTestCase()
{
_helper.cleanup();
}

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);

QVERIFY(_helper.addClassicFolders());
QVERIFY(systray->syncControlState() == Systray::SyncControlState::Pause);

systray->setSyncIsPaused(true);

QVERIFY(_helper.firstFolder()->syncPaused());
QVERIFY(_helper.secondFolder()->syncPaused());
QVERIFY(systray->syncControlState() == Systray::SyncControlState::Resume);

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)

Check warning on line 80 in test/macOS/testsystraysynccontrolmacos.cpp

View workflow job for this annotation

GitHub Actions / build

test/macOS/testsystraysynccontrolmacos.cpp:80:12 [cppcoreguidelines-avoid-non-const-global-variables]

variable 'TestSystraySyncControlMacOS' is non-const and globally accessible, consider making it const
#include "testsystraysynccontrolmacos.moc"
121 changes: 121 additions & 0 deletions test/systraysynccontroltesthelper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* 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.
*/

#pragma once

#include <QStandardPaths>

Check failure on line 12 in test/systraysynccontroltesthelper.h

View workflow job for this annotation

GitHub Actions / build

test/systraysynccontroltesthelper.h:12:10 [clang-diagnostic-error]

'QStandardPaths' file not found
#include <QTemporaryDir>
#include <QUrl>

#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;

Check warning on line 26 in test/systraysynccontroltesthelper.h

View workflow job for this annotation

GitHub Actions / build

test/systraysynccontroltesthelper.h:26:7 [cppcoreguidelines-avoid-non-const-global-variables]

variable 'namespace' is non-const and globally accessible, consider making it const

/** @brief Provides the shared account and folder setup for systray sync-control tests. */
class SystraySyncControlTestHelper

Check warning on line 29 in test/systraysynccontroltesthelper.h

View workflow job for this annotation

GitHub Actions / build

test/systraysynccontroltesthelper.h:29:7 [cppcoreguidelines-avoid-non-const-global-variables]

variable 'SystraySyncControlTestHelper' is non-const and globally accessible, consider making it const
{
public:
/** @brief Initializes the test configuration and first account. */
[[nodiscard]] bool initialize()
{
if (!_configDir.isValid() || !_firstFolderDir.isValid() || !_secondFolderDir.isValid()) {
return false;
}

QStandardPaths::setTestModeEnabled(true);
ConfigFile::setConfDir(_configDir.path());

// User construction references the Systray singleton, so initialize the tray before adding accounts.
const auto systray = Systray::instance();
systray->create();

_firstAccountState = addTestAccount(QStringLiteral("https://one.example.com"), QStringLiteral("alice"));
return _firstAccountState != nullptr;
}

/** @brief Removes the folders and accounts created by the helper. */
void cleanup()
{
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;
}

/** @brief Adds one classic sync folder to each of two accounts. */
[[nodiscard]] bool addClassicFolders()
{
_firstFolder = FolderMan::instance()->addFolder(_firstAccountState, folderDefinition(_firstFolderDir.path()));
if (!_firstFolder) {
return false;
}

_secondAccountState = addTestAccount(QStringLiteral("https://two.example.com"), QStringLiteral("bob"));
if (!_secondAccountState) {
return false;
}

_secondFolder = FolderMan::instance()->addFolder(_secondAccountState, folderDefinition(_secondFolderDir.path()));
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);
}

QTemporaryDir _configDir;
QTemporaryDir _firstFolderDir;
QTemporaryDir _secondFolderDir;
FolderManTestHelper _folderManHelper;
AccountState *_firstAccountState = nullptr;
AccountState *_secondAccountState = nullptr;
Folder *_firstFolder = nullptr;
Folder *_secondFolder = nullptr;
};
Loading
Loading