Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/desktop.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEXTCLOUD.cmake1
-rw-r--r--admin/win/msi/Nextcloud.wxs14
-rw-r--r--admin/win/msi/OEM.wxi.in2
-rw-r--r--cmake/modules/MacOSXBundleInfo.plist.in11
-rw-r--r--config.h.in1
-rw-r--r--doc/architecture.rst20
-rw-r--r--mirall.desktop.in4
-rw-r--r--src/3rdparty/qtsingleapplication/qtsingleapplication.cpp11
-rw-r--r--src/3rdparty/qtsingleapplication/qtsingleapplication.h1
-rw-r--r--src/common/utility.h5
-rw-r--r--src/common/utility_mac.cpp2
-rw-r--r--src/common/utility_unix.cpp23
-rw-r--r--src/common/utility_win.cpp2
-rw-r--r--src/gui/application.cpp67
-rw-r--r--src/gui/application.h8
-rw-r--r--src/gui/cocoainitializer_mac.mm61
-rw-r--r--src/gui/folderman.cpp62
-rw-r--r--src/gui/folderman.h3
18 files changed, 269 insertions, 29 deletions
diff --git a/NEXTCLOUD.cmake b/NEXTCLOUD.cmake
index a28ef387e..d43553f28 100644
--- a/NEXTCLOUD.cmake
+++ b/NEXTCLOUD.cmake
@@ -5,6 +5,7 @@ set( APPLICATION_DOMAIN "nextcloud.com" )
set( APPLICATION_VENDOR "Nextcloud GmbH" )
set( APPLICATION_UPDATE_URL "https://updates.nextcloud.org/client/" CACHE STRING "URL for updater" )
set( APPLICATION_HELP_URL "" CACHE STRING "URL for the help menu" )
+set( APPLICATION_URI_HANDLER_SCHEME "nc")
if(APPLE AND APPLICATION_NAME STREQUAL "Nextcloud" AND EXISTS "${CMAKE_SOURCE_DIR}/theme/colored/Nextcloud-macOS-icon.svg")
set( APPLICATION_ICON_NAME "Nextcloud-macOS" )
diff --git a/admin/win/msi/Nextcloud.wxs b/admin/win/msi/Nextcloud.wxs
index 8e694b3f6..05b1ba595 100644
--- a/admin/win/msi/Nextcloud.wxs
+++ b/admin/win/msi/Nextcloud.wxs
@@ -191,6 +191,19 @@
<RegistryValue Type="integer" Name="skipUpdateCheck" Value="[SKIPAUTOUPDATE]" />
</RegistryKey>
</Component>
+ <!-- Register URI handler -->
+ <Component Id="RegistryUriHandler" Guid="*" Win64="$(var.PlatformWin64)">
+ <RegistryKey Root="HKCU" Key="Software\Classes\$(var.AppCommandOpenUrlScheme)" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
+ <RegistryValue Type="string" Value="URL:$(var.AppName) Protocol" />
+ <RegistryValue Type="string" Name="URL Protocol" Value="" />
+ </RegistryKey>
+ <RegistryKey Root="HKCU" Key="Software\Classes\$(var.AppCommandOpenUrlScheme)\DefaultIcon" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
+ <RegistryValue Type="string" Value="[INSTALLDIR]$(var.AppExe)" />
+ </RegistryKey>
+ <RegistryKey Root="HKCU" Key="Software\Classes\$(var.AppCommandOpenUrlScheme)\shell\open\command" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
+ <RegistryValue Type="string" Value="&quot;[INSTALLDIR]$(var.AppExe)&quot; &quot;%1&quot;" />
+ </RegistryKey>
+ </Component>
</DirectoryRef>
<!-- Features -->
@@ -200,6 +213,7 @@
<ComponentRef Id="RegistryVersionInfo" />
<ComponentRef Id="RegistryDefaultSettings" />
+ <ComponentRef Id="RegistryUriHandler" />
<Feature Id="ShellExtensions" Title="Integration for Windows Explorer"
Description="This feature requires a reboot." >
diff --git a/admin/win/msi/OEM.wxi.in b/admin/win/msi/OEM.wxi.in
index 4d3eafcf9..e35451eef 100644
--- a/admin/win/msi/OEM.wxi.in
+++ b/admin/win/msi/OEM.wxi.in
@@ -28,6 +28,8 @@
<?define AppHelpLink = "https://@APPLICATION_DOMAIN@/" ?>
<?define AppInfoLink = "$(var.AppHelpLink)" ?>
+
+ <?define AppCommandOpenUrlScheme = "@APPLICATION_URI_HANDLER_SCHEME@" ?>
<!-- Custom license: To use it, also remove the "Skip the license page" stuff in the <UI> section
and uncomment <WixVariable Id="WixUILicenseRtf"...
diff --git a/cmake/modules/MacOSXBundleInfo.plist.in b/cmake/modules/MacOSXBundleInfo.plist.in
index fc66c62f1..cb9b794d8 100644
--- a/cmake/modules/MacOSXBundleInfo.plist.in
+++ b/cmake/modules/MacOSXBundleInfo.plist.in
@@ -76,6 +76,17 @@
</dict>
</array>
+<key>CFBundleURLTypes</key>
+<array>
+ <dict>
+ <key>CFBundleURLName</key>
+ <string>@APPLICATION_NAME@ Edit Locally</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>@APPLICATION_URI_HANDLER_SCHEME@</string>
+ </array>
+ </dict>
+</array>
</dict>
</plist>
diff --git a/config.h.in b/config.h.in
index d2afefca1..9ea359863 100644
--- a/config.h.in
+++ b/config.h.in
@@ -32,6 +32,7 @@
#cmakedefine APPLICATION_OCSP_STAPLING_ENABLED "@APPLICATION_OCSP_STAPLING_ENABLED@"
#cmakedefine APPLICATION_FORBID_BAD_SSL "@APPLICATION_FORBID_BAD_SSL@"
#define APPLICATION_DOTVIRTUALFILE_SUFFIX "." APPLICATION_VIRTUALFILE_SUFFIX
+#define APPLICATION_URI_HANDLER_SCHEME "@APPLICATION_URI_HANDLER_SCHEME@"
#cmakedefine01 ENFORCE_VIRTUAL_FILES_SYNC_FOLDER
#cmakedefine DO_NOT_USE_PROXY "@DO_NOT_USE_PROXY@"
diff --git a/doc/architecture.rst b/doc/architecture.rst
index 9c455a362..8f750e954 100644
--- a/doc/architecture.rst
+++ b/doc/architecture.rst
@@ -440,3 +440,23 @@ Files that must be removed from the local storage only, need to be dehydrated vi
.. note::
* End-to-end Encryption works with Virtual Files (VFS) but only on a per-folder level. Folders with E2EE can be made available offline in their entirety, but the individual files in them can not be retrieved on demand. This is mainly due to two technical reasons. First, the Windows VFS API is not designed for handling encrypted files. Second, while the VFS is designed to deal mostly with large files, E2EE is mostly recommended for use with small files as encrypting and decrypting large files puts large demands on the computer infrastructure.
+
+Local file editing
+------------------
+
+The Nextcloud desktop GUI client supports local editing when opening a URL that starts with
+a scheme ``nc://`` followed by an ``open`` command, followed by a user email (with port when needed),
+followed by file path relative to remote root.
+
+Examples of URLs that Nextcloud can handle if the user email and a path to a file is correct:
+- ``nc://open/admin@example.cloud:8080/Photos/lovely.jpg``
+- ``nc://open/user@example.cloud/Photos/lovely.jpg``
+- ``nc://open/user@example.cloud/Documents/sheets/report.xlsx``
+- ``nc://open/user@example.cloud/Documents/docs/document.docx``
+
+.. note::
+ * All the file paths that begin after user email are relative to remote root (``/``).
+ * The server is responsible for generating a correct URL that a user then clicks to edit file locally.
+ * The Nextcloud desktop client is registered in macOS, Linux, and Windows as a custom URI handler for the ``nc://`` scheme.
+ * The URL is parsed and validated by Nextcloud desktop client, so, opening an incorrectly formatted URL will not have any effect.
+ * The port after user email is necessary if the default :80 or :443 is not used. The rule of thumb is to always have a port added if you need it when accessing your server via Web UI \ No newline at end of file
diff --git a/mirall.desktop.in b/mirall.desktop.in
index 8ed71c324..1363a6d81 100644
--- a/mirall.desktop.in
+++ b/mirall.desktop.in
@@ -1,14 +1,14 @@
[Desktop Entry]
Categories=Utility;X-SuSE-SyncUtility;
Type=Application
-Exec=@APPLICATION_EXECUTABLE@
+Exec=@APPLICATION_EXECUTABLE@ %u
Name=@APPLICATION_NAME@ Desktop
Comment=@APPLICATION_NAME@ desktop synchronization client
GenericName=Folder Sync
Icon=@APPLICATION_ICON_NAME@
Keywords=@APPLICATION_NAME@;syncing;file;sharing;
X-GNOME-Autostart-Delay=3
-MimeType=application/vnd.@APPLICATION_EXECUTABLE@;
+MimeType=application/vnd.@APPLICATION_EXECUTABLE@;x-scheme-handler/@APPLICATION_URI_HANDLER_SCHEME@;
Actions=Quit;
# Translations
diff --git a/src/3rdparty/qtsingleapplication/qtsingleapplication.cpp b/src/3rdparty/qtsingleapplication/qtsingleapplication.cpp
index b1895a5a4..422608d1a 100644
--- a/src/3rdparty/qtsingleapplication/qtsingleapplication.cpp
+++ b/src/3rdparty/qtsingleapplication/qtsingleapplication.cpp
@@ -33,7 +33,6 @@
#include <qtlockedfile.h>
#include <QDir>
-#include <QFileOpenEvent>
#include <QSharedMemory>
#include <QWidget>
@@ -119,16 +118,6 @@ QtSingleApplication::~QtSingleApplication()
lockfile.unlock();
}
-bool QtSingleApplication::event(QEvent *event)
-{
- if (event->type() == QEvent::FileOpen) {
- auto *foe = static_cast<QFileOpenEvent*>(event);
- emit fileOpenRequest(foe->file());
- return true;
- }
- return QApplication::event(event);
-}
-
bool QtSingleApplication::isRunning(qint64 pid)
{
if (pid == -1) {
diff --git a/src/3rdparty/qtsingleapplication/qtsingleapplication.h b/src/3rdparty/qtsingleapplication/qtsingleapplication.h
index 674649e4c..1fa8d9131 100644
--- a/src/3rdparty/qtsingleapplication/qtsingleapplication.h
+++ b/src/3rdparty/qtsingleapplication/qtsingleapplication.h
@@ -50,7 +50,6 @@ public:
void setActivationWindow(QWidget* aw, bool activateOnMessage = true);
QWidget* activationWindow() const;
- bool event(QEvent *event) override;
QString applicationId() const;
void setBlock(bool value);
diff --git a/src/common/utility.h b/src/common/utility.h
index 98dc3030a..6179be9d5 100644
--- a/src/common/utility.h
+++ b/src/common/utility.h
@@ -247,6 +247,11 @@ namespace Utility {
*/
OCSYNC_EXPORT QString getCurrentUserName();
+ /**
+ * @brief Registers the desktop app as a handler for a custom URI to enable local editing
+ */
+ OCSYNC_EXPORT void registerUriHandlerForLocalEditing();
+
#ifdef Q_OS_WIN
OCSYNC_EXPORT bool registryKeyExists(HKEY hRootKey, const QString &subKey);
OCSYNC_EXPORT QVariant registryGetKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName);
diff --git a/src/common/utility_mac.cpp b/src/common/utility_mac.cpp
index d3a3d480a..9a220527f 100644
--- a/src/common/utility_mac.cpp
+++ b/src/common/utility_mac.cpp
@@ -141,4 +141,6 @@ QString Utility::getCurrentUserName()
return {};
}
+void Utility::registerUriHandlerForLocalEditing() { /* URI handler is registered via MacOSXBundleInfo.plist.in */ }
+
} // namespace OCC
diff --git a/src/common/utility_unix.cpp b/src/common/utility_unix.cpp
index 887213f09..27e66dc73 100644
--- a/src/common/utility_unix.cpp
+++ b/src/common/utility_unix.cpp
@@ -19,6 +19,7 @@
#include <QStandardPaths>
#include <QtGlobal>
+#include <QProcess>
namespace OCC {
@@ -113,4 +114,26 @@ QString Utility::getCurrentUserName()
return {};
}
+void Utility::registerUriHandlerForLocalEditing()
+{
+ const auto appImagePath = qEnvironmentVariable("APPIMAGE");
+ const auto runningInsideAppImage = !appImagePath.isNull() && QFile::exists(appImagePath);
+
+ if (!runningInsideAppImage) {
+ // only register x-scheme-handler if running inside appImage
+ return;
+ }
+
+ // mirall.desktop.in must have an x-scheme-handler mime type specified
+ const QString desktopFileName = QLatin1String(LINUX_APPLICATION_ID) + QLatin1String(".desktop");
+ QProcess process;
+ const QStringList args = {
+ QLatin1String("default"),
+ desktopFileName,
+ QStringLiteral("x-scheme-handler/%1").arg(QStringLiteral(APPLICATION_URI_HANDLER_SCHEME))
+ };
+ process.start(QStringLiteral("xdg-mime"), args, QIODevice::ReadOnly);
+ process.waitForFinished();
+}
+
} // namespace OCC
diff --git a/src/common/utility_win.cpp b/src/common/utility_win.cpp
index ed322669b..37b348660 100644
--- a/src/common/utility_win.cpp
+++ b/src/common/utility_win.cpp
@@ -448,6 +448,8 @@ QString Utility::getCurrentUserName()
return QString::fromWCharArray(username);
}
+void Utility::registerUriHandlerForLocalEditing() { /* URI handler is registered via Nextcloud.wxs */ }
+
Utility::NtfsPermissionLookupRAII::NtfsPermissionLookupRAII()
{
qt_ntfs_permission_lookup++;
diff --git a/src/gui/application.cpp b/src/gui/application.cpp
index bc0ceb6c2..b99bf4a6a 100644
--- a/src/gui/application.cpp
+++ b/src/gui/application.cpp
@@ -406,6 +406,8 @@ Application::Application(int &argc, char **argv)
connect(_gui.data(), &ownCloudGui::isShowingSettingsDialog, this, &Application::slotGuiIsShowingSettings);
_gui->createTray();
+
+ handleEditLocallyFromOptions();
}
Application::~Application()
@@ -572,6 +574,8 @@ void Application::slotParseMessage(const QString &msg, QObject *)
qApp->quit();
}
+ handleEditLocallyFromOptions();
+
} else if (msg.startsWith(QLatin1String("MSG_SHOWMAINDIALOG"))) {
qCInfo(lcApplication) << "Running for" << _startedAt.elapsed() / 1000.0 << "sec";
if (_startedAt.elapsed() < 10 * 1000) {
@@ -647,7 +651,17 @@ void Application::parseOptions(const QStringList &options)
} else if (option.endsWith(QStringLiteral(APPLICATION_DOTVIRTUALFILE_SUFFIX))) {
// virtual file, open it after the Folder were created (if the app is not terminated)
QTimer::singleShot(0, this, [this, option] { openVirtualFile(option); });
- } else {
+ } else if (option.startsWith(QStringLiteral(APPLICATION_URI_HANDLER_SCHEME "://open"))) {
+ // see the section Local file editing of the Architecture page of the user documenation
+ _editFileLocallyUrl = QUrl::fromUserInput(option);
+ if (!_editFileLocallyUrl.isValid()) {
+ _editFileLocallyUrl.clear();
+ const auto errorParsingLocalFileEditingUrl = QStringLiteral("The supplied url for local file editing '%1' is invalid!").arg(option);
+ qCInfo(lcApplication) << errorParsingLocalFileEditingUrl;
+ showHint(errorParsingLocalFileEditingUrl.toStdString());
+ }
+ }
+ else {
showHint("Unrecognized option '" + option.toStdString() + "'");
}
}
@@ -728,6 +742,32 @@ void Application::setHelp()
_helpOnly = true;
}
+void Application::handleEditLocallyFromOptions()
+{
+ if (!_editFileLocallyUrl.isValid()) {
+ return;
+ }
+
+ handleEditLocally(_editFileLocallyUrl);
+ _editFileLocallyUrl.clear();
+}
+
+void Application::handleEditLocally(const QUrl &url) const
+{
+ auto pathSplit = url.path().split('/', Qt::SkipEmptyParts);
+
+ if (pathSplit.size() < 2) {
+ qCWarning(lcApplication) << "Invalid URL for file local editing: " + pathSplit.join('/');
+ return;
+ }
+
+ // for a sample URL "nc://open/admin@nextcloud.lan:8080/Photos/lovely.jpg", QUrl::path would return "admin@nextcloud.lan:8080/Photos/lovely.jpg"
+ const auto accountDisplayName = pathSplit.takeFirst();
+ const auto fileRemotePath = pathSplit.join('/');
+
+ FolderMan::instance()->editFileLocally(accountDisplayName, fileRemotePath);
+}
+
QString substLang(const QString &lang)
{
// Map the more appropriate script codes
@@ -855,15 +895,26 @@ void Application::tryTrayAgain()
bool Application::event(QEvent *event)
{
-#ifdef Q_OS_MAC
if (event->type() == QEvent::FileOpen) {
- QFileOpenEvent *openEvent = static_cast<QFileOpenEvent *>(event);
- qCDebug(lcApplication) << "QFileOpenEvent" << openEvent->file();
- // virtual file, open it after the Folder were created (if the app is not terminated)
- QString fn = openEvent->file();
- QTimer::singleShot(0, this, [this, fn] { openVirtualFile(fn); });
+ const auto openEvent = static_cast<QFileOpenEvent *>(event);
+ qCDebug(lcApplication) << "macOS: Received a QFileOpenEvent";
+
+ if(!openEvent->file().isEmpty()) {
+ qCDebug(lcApplication) << "QFileOpenEvent" << openEvent->file();
+ // virtual file, open it after the Folder were created (if the app is not terminated)
+ const auto fn = openEvent->file();
+ QTimer::singleShot(0, this, [this, fn] { openVirtualFile(fn); });
+ } else if (!openEvent->url().isEmpty() && openEvent->url().isValid()) {
+ // On macOS, Qt does not handle receiving a custom URI as it does on other systems (as an application argument).
+ // Instead, it sends out a QFileOpenEvent. We therefore need custom handling for our URI handling on macOS.
+ qCInfo(lcApplication) << "macOS: Opening local file for editing: " << openEvent->url();
+ handleEditLocally(openEvent->url());
+ } else {
+ const auto errorParsingLocalFileEditingUrl = QStringLiteral("The supplied url for local file editing '%1' is invalid!").arg(openEvent->url().toString());
+ qCInfo(lcApplication) << errorParsingLocalFileEditingUrl;
+ showHint(errorParsingLocalFileEditingUrl.toStdString());
+ }
}
-#endif
return SharedTools::QtSingleApplication::event(event);
}
diff --git a/src/gui/application.h b/src/gui/application.h
index 048b795a6..edbb0b712 100644
--- a/src/gui/application.h
+++ b/src/gui/application.h
@@ -72,6 +72,8 @@ public:
ownCloudGui *gui() const;
+ bool event(QEvent *event) override;
+
public slots:
// TODO: this should not be public
void slotownCloudWizardDone(int);
@@ -85,11 +87,12 @@ public slots:
/// Attempt to show() the tray icon again. Used if no systray was available initially.
void tryTrayAgain();
+ void handleEditLocally(const QUrl &url) const;
+
protected:
void parseOptions(const QStringList &);
void setupTranslations();
void setupLogging();
- bool event(QEvent *event) override;
signals:
void folderRemoved();
@@ -109,6 +112,8 @@ protected slots:
private:
void setHelp();
+ void handleEditLocallyFromOptions();
+
/**
* Maybe a newer version of the client was used with this config file:
* if so, backup, confirm with user and remove the config that can't be read.
@@ -135,6 +140,7 @@ private:
bool _userTriggeredConnect;
bool _debugMode;
bool _backgroundMode;
+ QUrl _editFileLocallyUrl;
ClientProxy _proxy;
diff --git a/src/gui/cocoainitializer_mac.mm b/src/gui/cocoainitializer_mac.mm
index bc8ff7c72..155aa8b16 100644
--- a/src/gui/cocoainitializer_mac.mm
+++ b/src/gui/cocoainitializer_mac.mm
@@ -17,23 +17,72 @@
#import <Foundation/NSAutoreleasePool.h>
#import <AppKit/NSApplication.h>
+#include "application.h"
+
+/* In theory, we should be able to just capture QFileOpenEvents
+ * when we open our custom URLs in our Application class and be
+ * done with it, but in practice the QFileOpenEvent often doesn't
+ * get sent for our URLs. We have this in place to work around
+ * the issue.
+ *
+ * This class sets a callback selector on URL-related events
+ * before the application is fully done launching. This lets us
+ * properly receive and process "open url" events even if the
+ * client was closed when these events were sent. */
+
+@interface URLEventHandler : NSObject
+@end
+
+@implementation URLEventHandler
+- (id)init {
+ self = [super init];
+
+ if (self) {
+ NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
+ [defaultCenter addObserver:self
+ selector:@selector(applicationWillFinishLaunching:)
+ name:NSApplicationWillFinishLaunchingNotification
+ object:nil];
+ }
+ return self;
+}
+
+- (void)applicationWillFinishLaunching:(NSNotification *)aNotification {
+ [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self
+ andSelector:@selector(handleURLEvent:withReplyEvent:)
+ forEventClass:kInternetEventClass
+ andEventID:kAEGetURL];
+}
+
+- (void)handleURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent
+{
+ NSURL* url = [NSURL URLWithString:[[event paramDescriptorForKeyword:keyDirectObject] stringValue]];
+ const auto app = qobject_cast<OCC::Application *>(QApplication::instance());
+ const auto qtUrl = QUrl::fromNSURL(url);
+ app->handleEditLocally(qtUrl);
+}
+
+@end
+
namespace OCC {
namespace Mac {
class CocoaInitializer::Private {
- public:
+public:
NSAutoreleasePool* autoReleasePool;
+ URLEventHandler* handler;
};
CocoaInitializer::CocoaInitializer() {
- d = new CocoaInitializer::Private();
- NSApplicationLoad();
- d->autoReleasePool = [[NSAutoreleasePool alloc] init];
+ d = new CocoaInitializer::Private();
+ d->handler = [[URLEventHandler alloc] init];
+ NSApplicationLoad();
+ d->autoReleasePool = [[NSAutoreleasePool alloc] init];
}
CocoaInitializer::~CocoaInitializer() {
- [d->autoReleasePool release];
- delete d;
+ [d->autoReleasePool release];
+ delete d;
}
} // namespace Mac
diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp
index 56eb9c092..2191037d0 100644
--- a/src/gui/folderman.cpp
+++ b/src/gui/folderman.cpp
@@ -36,6 +36,8 @@
#include <QMutableSetIterator>
#include <QSet>
#include <QNetworkProxy>
+#include <QDesktopServices>
+#include <QtConcurrent>
static const char versionC[] = "version";
static const int maxFoldersVersion = 1;
@@ -163,6 +165,8 @@ void FolderMan::registerFolderWithSocketApi(Folder *folder)
int FolderMan::setupFolders()
{
+ Utility::registerUriHandlerForLocalEditing();
+
unloadAndDeleteAllFolders();
QStringList skipSettingsKeys;
@@ -1402,6 +1406,64 @@ void FolderMan::setDirtyNetworkLimits()
}
}
+void FolderMan::editFileLocally(const QString &accountDisplayName, const QString &relPath)
+{
+ const auto showError = [this](const OCC::AccountStatePtr accountState, const QString &errorMessage, const QString &subject) {
+ if (accountState && accountState->account()) {
+ const auto foundFolder = std::find_if(std::cbegin(map()), std::cend(map()), [accountState](const auto &folder) {
+ return accountState->account()->davUrl() == folder->remoteUrl();
+ });
+
+ if (foundFolder != std::cend(map())) {
+ (*foundFolder)->syncEngine().addErrorToGui(SyncFileItem::SoftError, errorMessage, subject);
+ }
+ }
+
+ // to make sure the error is not missed, show a message box in addition
+ const auto messageBox = new QMessageBox;
+ messageBox->setAttribute(Qt::WA_DeleteOnClose);
+ messageBox->setText(errorMessage);
+ messageBox->setInformativeText(subject);
+ messageBox->setIcon(QMessageBox::Warning);
+ messageBox->addButton(QMessageBox::StandardButton::Ok);
+ messageBox->show();
+ messageBox->activateWindow();
+ messageBox->raise();
+ };
+
+ const auto accountFound = AccountManager::instance()->account(accountDisplayName);
+
+ if (!accountFound) {
+ qCWarning(lcFolderMan) << "Could not find an account " << accountDisplayName << " to edit file " << relPath << " locally.";
+ showError(accountFound, tr("Could not find an account for local editing"), accountDisplayName);
+ return;
+ }
+
+ const auto foundFiles = findFileInLocalFolders(relPath, accountFound->account());
+
+ if (foundFiles.isEmpty()) {
+ for (const auto &folder : map()) {
+ bool result = false;
+ const auto excludedThroughSelectiveSync = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &result);
+ for (const auto &excludedPath : excludedThroughSelectiveSync) {
+ if (relPath.startsWith(excludedPath)) {
+ showError(accountFound, tr("Could not find a file for local editing. Make sure it is not excluded via selective sync."), relPath);
+ return;
+ }
+ }
+ }
+
+ showError(accountFound, tr("Could not find a file for local editing. Make sure its path is valid and it is synced locally."), relPath);
+ return;
+ }
+
+ // In case the VFS mode is enabled and a file is not yet hydrated, we must call QDesktopServices::openUrl from a separate thread, or, there will be a freeze.
+ // To avoid searching for a specific folder and checking if the VFS is enabled - we just always call it from a separate thread.
+ QtConcurrent::run([foundFiles] {
+ QDesktopServices::openUrl(QUrl::fromLocalFile(foundFiles.first()));
+ });
+}
+
void FolderMan::trayOverallStatus(const QList<Folder *> &folders,
SyncResult::Status *status, bool *unresolvedConflicts)
{
diff --git a/src/gui/folderman.h b/src/gui/folderman.h
index 14575da46..3677073c3 100644
--- a/src/gui/folderman.h
+++ b/src/gui/folderman.h
@@ -202,6 +202,9 @@ public:
void setDirtyProxy();
void setDirtyNetworkLimits();
+ /** opens a file with default app, if the file is present **/
+ void editFileLocally(const QString &accountDisplayName, const QString &relPath);
+
signals:
/**
* signal to indicate a folder has changed its sync state.