Implement URI handler for local file editing
authoralex-z <blackslayer4@gmail.com>
Tue, 19 Jul 2022 10:05:20 +0000 (13:05 +0300)
committeralex-z <blackslayer4@gmail.com>
Wed, 3 Aug 2022 08:38:33 +0000 (11:38 +0300)
Signed-off-by: alex-z <blackslayer4@gmail.com>
18 files changed:
NEXTCLOUD.cmake
admin/win/msi/Nextcloud.wxs
admin/win/msi/OEM.wxi.in
cmake/modules/MacOSXBundleInfo.plist.in
config.h.in
doc/architecture.rst
mirall.desktop.in
src/3rdparty/qtsingleapplication/qtsingleapplication.cpp
src/3rdparty/qtsingleapplication/qtsingleapplication.h
src/common/utility.h
src/common/utility_mac.cpp
src/common/utility_unix.cpp
src/common/utility_win.cpp
src/gui/application.cpp
src/gui/application.h
src/gui/cocoainitializer_mac.mm
src/gui/folderman.cpp
src/gui/folderman.h

index a28ef387eda7b8a26e0752872e8b118e220116f7..d43553f283cfb0e0dbe3fa23015c9f52e9b4ffae 100644 (file)
@@ -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" )
index 8e694b3f69a515168d4565b67f0c753cecf5c3ab..05b1ba595d52d41e6841242e676cf48e6b6e6db2 100644 (file)
                 <!-- Property to disable update checks -->
                 <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>
 
 
         <ComponentRef Id="RegistryVersionInfo" />
         <ComponentRef Id="RegistryDefaultSettings" />
+               <ComponentRef Id="RegistryUriHandler" />
 
         <Feature Id="ShellExtensions" Title="Integration for Windows Explorer"
             Description="This feature requires a reboot." >
index 4d3eafcf9828839eb0237c80642223067146c1c3..e35451eef2904bb6acdfb3837a22eb41e7cb18b5 100644 (file)
@@ -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"...
index fc66c62f108cecf56ffe37fab8dc0d41c3b15d7f..cb9b794d8466216cc141b3ffdab176020fdf6960 100644 (file)
     </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>
index d2afefca1ee69d4b491b4532e7e169b4a4a4c9dc..9ea359863bb5c956995e0ead3dce64102f5c0e37 100644 (file)
@@ -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@"
 
index 9c455a362500c8ee6aa57def7dc5d5aa06c84d11..8f750e95448c11d92276aa874eb16286b8bd5d26 100644 (file)
@@ -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
index 8ed71c32410c683eb1e4574660acd2b41379029a..1363a6d81e8675188f11dbb30aced18c3d5a037b 100644 (file)
@@ -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
index b1895a5a444baf230eedda3ce2f8ee54d880d645..422608d1afb767c7d13714a152ae7fbf47241f17 100644 (file)
@@ -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) {
index 674649e4cf077830727bf5445dd60944913468a9..1fa8d9131491a9695bc4a27f515cbb469f72720a 100644 (file)
@@ -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);
index 98dc3030ab8d550cf5d1eceef8fb1893fb3998ab..6179be9d53061b03440871e33c7aabc908f3be21 100644 (file)
@@ -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);
index d3a3d480a0e159648b1792969ab6d37f9139ad58..9a220527fa5ff7b69c523727fd1ef4a86e2d910d 100644 (file)
@@ -141,4 +141,6 @@ QString Utility::getCurrentUserName()
     return {};
 }
 
+void Utility::registerUriHandlerForLocalEditing() { /* URI handler is registered via MacOSXBundleInfo.plist.in */ }
+
 } // namespace OCC
index 887213f09ace7a99696ad8d7c270ce3daa233aef..27e66dc73aad902ddf4f923b10152b0a107c2b0f 100644 (file)
@@ -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
index ed322669b1bc79ed4552be47d7ceaed56886b531..37b348660c0de48a5d2b6116aaf733a76f5281df 100644 (file)
@@ -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++;
index bc0ceb6c203d5d901f1308bf17679743b3492c69..b99bf4a6a67455c387afb47d2646e7cfbf66a822 100644 (file)
@@ -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);
 }
 
index 048b795a632c6204521eb40da2a04c1864ca7088..edbb0b7121398e8b26176a9fc31abc1e4f90871c 100644 (file)
@@ -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;
 
index bc8ff7c72ee68dfa48ee9e55e492d820023c81fc..155aa8b1619efcb00544bcbf5691ba162228896a 100644 (file)
 #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
index 56eb9c0927f6fb543e1d13d3da4c5ea928a65a78..2191037d011ec839938785addf744e256bfddd2e 100644 (file)
@@ -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)
 {
index 14575da463fa0f245a4067dcbfe8103238914465..3677073c34cb88f4eb3e5ff1a2f239ae19c21827 100644 (file)
@@ -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.