Skip to content

Commit b0fb4fd

Browse files
committed
feat(macOS): Improved file provider switch user experience
Signed-off-by: Iva Horn <iva.horn@nextcloud.com>
1 parent f4b8486 commit b0fb4fd

22 files changed

Lines changed: 863 additions & 291 deletions
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: GPL-2.0-or-later
4+
-->
5+
6+
# TC-0007: Switching the app-level File Provider mode
7+
8+
- **Platform:** macOS
9+
- **Related Issues:** None yet
10+
11+
## Description
12+
13+
The File Provider integration and classic sync folders (FinderSync extension) cannot run at the same time on macOS: as soon as any file provider domain exists, the system deactivates the FinderSync extension. The client therefore offers one app-level File Provider switch in the General settings. Enabling it discards all classic sync folder connections (local files are kept); disabling it removes all file provider domains without recreating classic folders. Both directions require explicit confirmation.
14+
15+
## Preconditions
16+
17+
- A client build with the file provider module (`Nextcloud-vfs-…`) on macOS 14 or later.
18+
- At least one configured account with one or more classic sync folders containing synced files.
19+
20+
## Test Steps
21+
22+
| Step | Action | Expected Result |
23+
|------|--------|-----------------|
24+
| 1 | Open Settings → General | A "Use macOS File Provider Integration" switch is shown with an explanatory text; it is off |
25+
| 2 | Open the account's settings page | The "Classic sync" panel with the folder list is shown; no File Provider panel exists |
26+
| 3 | Turn the File Provider switch on | A confirmation dialog appears, listing the affected accounts and their classic sync folder connections, stating that files stay on disk |
27+
| 4 | Click "Cancel" | Nothing changes: switch stays off, classic folders keep syncing |
28+
| 5 | Turn the switch on again and click "Enable File Provider" | The switch is disabled while setting up; afterwards every account appears in Finder under "Locations", all classic sync folder connections are removed, the local folders and their files remain on disk, FinderSync badges disappear, and the "Classic sync" panel is hidden in every account's settings |
29+
| 6 | Turn the switch off | A confirmation dialog explains that files leave Finder "Locations", unsynced items are preserved, and classic sync folders are not recreated automatically |
30+
| 7 | Click "Disable File Provider" | All file provider domains are removed; no classic folders are created; the "Classic sync" panel (with the "Add Folder Sync Connection" button) reappears in the account settings |
31+
| 8 | Add a new folder sync connection and restart the client | The classic folder syncs again and FinderSync badges work |
32+
33+
## Expected Results
34+
35+
- Classic sync folders and File Provider are never active at the same time.
36+
- No local files are deleted by either switch direction.
37+
- A deployment updating with both configured (or a client killed mid-switch) shows a "Choose how to sync your files" dialog at startup offering "Keep File Provider", "Keep classic sync folders" and "Decide later"; "Decide later" keeps both working, shows a warning banner in the affected account settings, and asks again on the next launch.
38+
- New accounts added while the mode is on automatically get a file provider domain; the account wizard skips the sync options step. With the mode off, the wizard offers classic folder setup only.

resources.qrc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@
8080
<file>src/gui/wizard/qml/WizardComboBox.qml</file>
8181
<file>src/gui/wizard/qml/WizardDialogFrame.qml</file>
8282
<file>src/gui/wizard/qml/WizardTextField.qml</file>
83-
<file>src/gui/macOS/ui/FileProviderSettings.qml</file>
8483
<file>src/gui/macOS/ui/FileProviderFileDelegate.qml</file>
8584
<file>src/gui/integration/FileActionsWindow.qml</file>
8685
<file>src/gui/GovernanceLabelsDialog.qml</file>

src/gui/accountsettings.cpp

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -196,16 +196,14 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
196196
connect(connectionSettingsButton, &QPushButton::clicked, this, &AccountSettings::showConnectionSettingsDialog);
197197

198198
_ui->verticalLayout_2->removeWidget(_ui->accountStatusPanel);
199-
_ui->verticalLayout_2->removeWidget(_ui->fileProviderPanel);
200199
_ui->verticalLayout_2->removeWidget(_ui->syncFoldersPanel);
201200
_ui->verticalLayout_2->removeWidget(_ui->connectionSettingsPanel);
202201
_ui->verticalLayout_2->removeWidget(_ui->accountActionsPanel);
203202
_ui->connectionSettingsPanel->hide();
204-
_ui->verticalLayout_2->insertWidget(0, _ui->fileProviderPanel);
205-
_ui->verticalLayout_2->insertWidget(1, _ui->syncFoldersPanel);
206-
_ui->verticalLayout_2->insertWidget(2, _encryptionPanel);
207-
_ui->verticalLayout_2->insertWidget(3, _ui->accountStatusPanel);
208-
_ui->verticalLayout_2->insertWidget(4, _ui->accountActionsPanel);
203+
_ui->verticalLayout_2->insertWidget(0, _ui->syncFoldersPanel);
204+
_ui->verticalLayout_2->insertWidget(1, _encryptionPanel);
205+
_ui->verticalLayout_2->insertWidget(2, _ui->accountStatusPanel);
206+
_ui->verticalLayout_2->insertWidget(3, _ui->accountActionsPanel);
209207

210208
_model->setAccountState(_accountState);
211209
_model->setParent(this);
@@ -215,9 +213,6 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
215213
_ui->syncFoldersPanelContents->setAutoFillBackground(false);
216214
_ui->syncFoldersPanelContents->setAttribute(Qt::WA_StyledBackground, false);
217215
_ui->syncFoldersPanelContents->setContentsMargins(0, 0, 0, 0);
218-
_ui->fileProviderPanelContents->setAutoFillBackground(false);
219-
_ui->fileProviderPanelContents->setAttribute(Qt::WA_StyledBackground, false);
220-
_ui->fileProviderPanelContents->setContentsMargins(0, 0, 0, 0);
221216
_ui->connectionSettingsPanelContents->setAutoFillBackground(false);
222217
_ui->connectionSettingsPanelContents->setAttribute(Qt::WA_StyledBackground, false);
223218
_ui->connectionSettingsPanelContents->setContentsMargins(0, 0, 0, 0);
@@ -244,30 +239,20 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
244239
new ToolTipUpdater(_ui->_folderList);
245240

246241
#if defined(BUILD_FILE_PROVIDER_MODULE)
247-
if (Mac::FileProvider::available()) {
248-
const auto fileProviderPanelContents = _ui->fileProviderPanelContents;
249-
const auto fpSettingsLayout = new QVBoxLayout(fileProviderPanelContents);
250-
const auto fpAccountUserIdAtHost = _accountState->account()->userIdAtHostWithPort();
251-
const auto fpSettingsController = Mac::FileProviderSettingsController::instance();
252-
const auto fpSettingsWidget = fpSettingsController->settingsViewWidget(fpAccountUserIdAtHost, fileProviderPanelContents,
253-
QQuickWidget::SizeRootObjectToView);
254-
fpSettingsLayout->setContentsMargins(0, 0, 0, 0);
255-
fpSettingsLayout->setSpacing(0);
256-
257-
fpSettingsWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
258-
if (const auto fpSettingsWidgetLayout = fpSettingsWidget->layout()) {
259-
fpSettingsWidgetLayout->setContentsMargins(0, 0, 0, 0);
260-
}
261-
fpSettingsLayout->addWidget(fpSettingsWidget, 1);
262-
fileProviderPanelContents->setLayout(fpSettingsLayout);
263-
} else {
264-
// macOS 13 Ventura: the file provider feature is unsupported there.
265-
// This branch can be removed once Ventura is no longer supported.
266-
_ui->fileProviderPanel->setVisible(false);
267-
}
268-
#else
269-
_ui->fileProviderPanel->setVisible(false);
242+
// The File Provider integration is an app-level mode, toggled in the General
243+
// settings. While it is enabled, classic sync folders are unavailable: hide the
244+
// "Classic sync" panel, except when the account still has classic folders
245+
// configured (a conflict the banner asks the user to resolve).
246+
const auto fpSettingsController = Mac::FileProviderSettingsController::instance();
247+
connect(fpSettingsController, &Mac::FileProviderSettingsController::fileProviderModeEnabledChanged,
248+
this, &AccountSettings::updateSyncFoldersPanelVisibility);
249+
connect(FolderMan::instance(), &FolderMan::folderListChanged,
250+
this, &AccountSettings::updateSyncFoldersPanelVisibility);
251+
connect(_ui->fileProviderConflictResolveButton, &QPushButton::clicked, this, [fpSettingsController] {
252+
fpSettingsController->performStartupReconciliation();
253+
});
270254
#endif
255+
updateSyncFoldersPanelVisibility();
271256

272257
const auto mouseCursorChanger = new MouseCursorChanger(this);
273258
mouseCursorChanger->folderList = _ui->_folderList;
@@ -940,6 +925,13 @@ void AccountSettings::slotFolderWizardAccepted()
940925
QStringList() << QLatin1String("/"));
941926
folderMan->scheduleAllFolders();
942927
emit folderChanged();
928+
} else {
929+
// addFolder can refuse (e.g. classic sync folders are unavailable while the
930+
// File Provider integration is enabled). Don't leave the user believing the
931+
// wizard succeeded.
932+
QMessageBox::warning(this, tr("Folder creation failed"),
933+
tr("<p>Could not add the folder sync connection for <i>%1</i>.</p>")
934+
.arg(Utility::escape(QDir::toNativeSeparators(definition.localPath))));
943935
}
944936
}
945937

@@ -1957,6 +1949,39 @@ void AccountSettings::setEncryptionPanelVisible(bool visible)
19571949
}
19581950
}
19591951

1952+
void AccountSettings::updateSyncFoldersPanelVisibility()
1953+
{
1954+
#if defined(BUILD_FILE_PROVIDER_MODULE)
1955+
const auto fpModeOn = Mac::FileProvider::available()
1956+
&& Mac::FileProviderSettingsController::instance()->fileProviderModeEnabled();
1957+
1958+
auto hasClassicFolders = false;
1959+
const auto folderMap = FolderMan::instance()->map();
1960+
1961+
for (const auto folder : folderMap) {
1962+
if (folder->accountState() == _accountState) {
1963+
hasClassicFolders = true;
1964+
break;
1965+
}
1966+
}
1967+
1968+
// Hide the classic sync settings while File Provider mode is on — except when this
1969+
// account still has classic folders configured (mixed state): those must stay
1970+
// visible, together with the banner asking the user to resolve the conflict.
1971+
_ui->syncFoldersPanel->setVisible(!fpModeOn || hasClassicFolders);
1972+
_ui->fileProviderConflictBanner->setVisible(fpModeOn && hasClassicFolders);
1973+
1974+
if (fpModeOn && hasClassicFolders && _ui->fileProviderConflictBannerIcon->pixmap().isNull()) {
1975+
const auto iconSize = style()->pixelMetric(QStyle::PM_SmallIconSize, nullptr, this);
1976+
const auto warningIcon = Theme::createColorAwareIcon(QStringLiteral(":/client/theme/black/warning.svg"));
1977+
_ui->fileProviderConflictBannerIcon->setPixmap(warningIcon.pixmap(iconSize, iconSize));
1978+
}
1979+
#else
1980+
_ui->syncFoldersPanel->setVisible(true);
1981+
_ui->fileProviderConflictBanner->setVisible(false);
1982+
#endif
1983+
}
1984+
19601985
void AccountSettings::setEncryptionMessageIcon(const QIcon &icon)
19611986
{
19621987
if (icon.isNull()) {

src/gui/accountsettings.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ private slots:
141141
void checkClientSideEncryptionState();
142142
void removeActionFromEncryptionMessage(const QString &actionId);
143143
void setEncryptionPanelVisible(bool visible);
144+
void updateSyncFoldersPanelVisibility();
144145

145146
private:
146147
bool event(QEvent *) override;

src/gui/accountsettings.ui

Lines changed: 54 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -133,46 +133,6 @@
133133
</layout>
134134
</widget>
135135
</item>
136-
<item>
137-
<widget class="QFrame" name="fileProviderPanel">
138-
<property name="sizePolicy">
139-
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
140-
<horstretch>0</horstretch>
141-
<verstretch>0</verstretch>
142-
</sizepolicy>
143-
</property>
144-
<property name="frameShape">
145-
<enum>QFrame::Shape::NoFrame</enum>
146-
</property>
147-
<layout class="QVBoxLayout" name="fileProviderPanelLayout">
148-
<property name="sizeConstraint">
149-
<enum>QLayout::SizeConstraint::SetDefaultConstraint</enum>
150-
</property>
151-
<item>
152-
<widget class="QLabel" name="fileProviderPanelTitle">
153-
<property name="font">
154-
<font>
155-
<bold>true</bold>
156-
</font>
157-
</property>
158-
<property name="text">
159-
<string>File Provider</string>
160-
</property>
161-
</widget>
162-
</item>
163-
<item>
164-
<widget class="QWidget" name="fileProviderPanelContents" native="true">
165-
<property name="sizePolicy">
166-
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
167-
<horstretch>0</horstretch>
168-
<verstretch>0</verstretch>
169-
</sizepolicy>
170-
</property>
171-
</widget>
172-
</item>
173-
</layout>
174-
</widget>
175-
</item>
176136
<item>
177137
<widget class="QFrame" name="syncFoldersPanel">
178138
<property name="sizePolicy">
@@ -206,6 +166,60 @@
206166
</property>
207167
</widget>
208168
</item>
169+
<item>
170+
<widget class="QWidget" name="fileProviderConflictBanner" native="true">
171+
<property name="visible">
172+
<bool>false</bool>
173+
</property>
174+
<layout class="QHBoxLayout" name="fileProviderConflictBannerLayout">
175+
<item>
176+
<widget class="QLabel" name="fileProviderConflictBannerIcon">
177+
<property name="sizePolicy">
178+
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
179+
<horstretch>0</horstretch>
180+
<verstretch>0</verstretch>
181+
</sizepolicy>
182+
</property>
183+
<property name="text">
184+
<string/>
185+
</property>
186+
<property name="alignment">
187+
<set>Qt::AlignmentFlag::AlignHCenter|Qt::AlignmentFlag::AlignTop</set>
188+
</property>
189+
</widget>
190+
</item>
191+
<item>
192+
<widget class="QLabel" name="fileProviderConflictBannerLabel">
193+
<property name="sizePolicy">
194+
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
195+
<horstretch>0</horstretch>
196+
<verstretch>0</verstretch>
197+
</sizepolicy>
198+
</property>
199+
<property name="text">
200+
<string>File Provider is enabled, but this account still has classic sync folders. The Finder integration stays unavailable until this is resolved.</string>
201+
</property>
202+
<property name="wordWrap">
203+
<bool>true</bool>
204+
</property>
205+
</widget>
206+
</item>
207+
<item>
208+
<widget class="QPushButton" name="fileProviderConflictResolveButton">
209+
<property name="sizePolicy">
210+
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
211+
<horstretch>0</horstretch>
212+
<verstretch>0</verstretch>
213+
</sizepolicy>
214+
</property>
215+
<property name="text">
216+
<string>Resolve…</string>
217+
</property>
218+
</widget>
219+
</item>
220+
</layout>
221+
</widget>
222+
</item>
209223
<item>
210224
<widget class="QWidget" name="syncFoldersPanelContents" native="true">
211225
<property name="sizePolicy">

src/gui/application.cpp

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -515,14 +515,22 @@ Application::Application(int &argc, char **argv)
515515

516516
#if defined(BUILD_FILE_PROVIDER_MODULE)
517517
Mac::FileProvider::instance();
518+
519+
// Instantiating the settings controller runs its startup migrations: initialising
520+
// the app-level File Provider mode flag, and — on macOS 13 Ventura, where the file
521+
// provider feature is unsupported — gracefully tearing down any pre-existing VFS
522+
// domains.
523+
Mac::FileProviderSettingsController::instance();
524+
518525
if (Mac::FileProvider::available()) {
519526
Mac::FileProvider::instance()->configureXPC();
520-
} else {
521-
// macOS 13 Ventura: instantiating the settings controller triggers the
522-
// one-time cleanup that gracefully tears down any pre-existing VFS
523-
// domains. The check (and this branch) can be removed once Ventura is
524-
// no longer supported.
525-
Mac::FileProviderSettingsController::instance();
527+
528+
// Deferred so that the event loop is running: reconciles the file provider
529+
// domains with the app-level mode and, if both File Provider mode and classic
530+
// sync folders are configured, asks the user which one to keep.
531+
QTimer::singleShot(0, this, [] {
532+
Mac::FileProviderSettingsController::instance()->performStartupReconciliation();
533+
});
526534
}
527535
#endif
528536

src/gui/folderman.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1392,6 +1392,15 @@ void FolderMan::slotFolderSyncFinished(const SyncResult &)
13921392

13931393
Folder *FolderMan::addFolder(AccountState *accountState, const FolderDefinition &folderDefinition)
13941394
{
1395+
#ifdef BUILD_FILE_PROVIDER_MODULE
1396+
// Classic sync folders and the File Provider integration are mutually exclusive:
1397+
// the FinderSync extension cannot run while the File Provider extension is active.
1398+
if (ConfigFile().macFileProviderModeEnabled()) {
1399+
qCWarning(lcFolderMan) << "Refusing to add classic sync folder: File Provider mode is enabled.";
1400+
return nullptr;
1401+
}
1402+
#endif
1403+
13951404
// Choose a db filename
13961405
auto definition = folderDefinition;
13971406
definition.journalPath = definition.defaultJournalPath(accountState->account());

0 commit comments

Comments
 (0)