Windows: Add sync folders to Explorer's navigation pane

This is only the navigation pane, the SyncRootManager entries aren't handled yet.

This follows the instructions from:
https://msdn.microsoft.com/en-us/library/windows/desktop/dn889934%28v=vs.85%29.aspx

Issue #5295
This commit is contained in:
Jocelyn Turcotte 2017-10-04 13:49:42 +02:00 committed by Jocelyn Turcotte
parent e85a339d94
commit 56e38e6f80
13 changed files with 385 additions and 2 deletions

View file

@ -213,6 +213,9 @@ add_definitions(-D_UNICODE)
if( WIN32 )
add_definitions( -D__USE_MINGW_ANSI_STDIO=1 )
add_definitions( -DNOMINMAX )
# Get APIs from from Vista onwards.
add_definitions( -D_WIN32_WINNT=0x0600)
add_definitions( -DWINVER=0x0600)
endif( WIN32 )
include(QtVersionAbstraction)

View file

@ -28,8 +28,13 @@
#include <QLoggingCategory>
#include <QMap>
#include <QUrl>
#include <functional>
#include <memory>
#ifdef Q_OS_WIN
#include <windows.h>
#endif
class QSettings;
namespace OCC {
@ -187,6 +192,14 @@ namespace Utility {
* Experimental! Real feature planned for 2.5.
*/
OCSYNC_EXPORT bool shouldUploadConflictFiles();
#ifdef Q_OS_WIN
OCSYNC_EXPORT QVariant registryGetKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName);
OCSYNC_EXPORT bool registrySetKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName, DWORD type, const QVariant &value);
OCSYNC_EXPORT bool registryDeleteKeyTree(HKEY hRootKey, const QString &subKey);
OCSYNC_EXPORT bool registryDeleteKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName);
OCSYNC_EXPORT bool registryWalkSubKeys(HKEY hRootKey, const QString &subKey, const std::function<void(HKEY, const QString &)> &callback);
#endif
}
/** @} */ // \addtogroup

View file

@ -16,8 +16,7 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#define _WIN32_WINNT 0x0600
#define WINVER 0x0600
#include "asserts.h"
#include <shlobj.h>
#include <winbase.h>
#include <windows.h>
@ -93,4 +92,168 @@ static inline bool hasDarkSystray_private()
return true;
}
QVariant Utility::registryGetKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName)
{
QVariant value;
HKEY hKey;
REGSAM sam = KEY_READ | KEY_WOW64_64KEY;
LONG result = RegOpenKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(subKey.utf16()), 0, sam, &hKey);
ASSERT(result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND);
if (result != ERROR_SUCCESS)
return value;
DWORD type = 0, sizeInBytes = 0;
result = RegQueryValueEx(hKey, reinterpret_cast<LPCWSTR>(valueName.utf16()), 0, &type, nullptr, &sizeInBytes);
ASSERT(result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND);
if (result == ERROR_SUCCESS) {
switch (type) {
case REG_DWORD:
DWORD dword;
Q_ASSERT(sizeInBytes == sizeof(dword));
if (RegQueryValueEx(hKey, reinterpret_cast<LPCWSTR>(valueName.utf16()), 0, &type, reinterpret_cast<LPBYTE>(&dword), &sizeInBytes) == ERROR_SUCCESS) {
value = int(dword);
}
break;
case REG_EXPAND_SZ:
case REG_SZ: {
QString string;
string.resize(sizeInBytes / sizeof(QChar));
result = RegQueryValueEx(hKey, reinterpret_cast<LPCWSTR>(valueName.utf16()), 0, &type, reinterpret_cast<LPBYTE>(string.data()), &sizeInBytes);
if (result == ERROR_SUCCESS) {
int newCharSize = sizeInBytes / sizeof(QChar);
// From the doc:
// If the data has the REG_SZ, REG_MULTI_SZ or REG_EXPAND_SZ type, the string may not have been stored with
// the proper terminating null characters. Therefore, even if the function returns ERROR_SUCCESS,
// the application should ensure that the string is properly terminated before using it; otherwise, it may overwrite a buffer.
if (string.at(newCharSize - 1) == QChar('\0'))
string.resize(newCharSize - 1);
value = string;
}
break;
}
default:
Q_UNREACHABLE();
}
}
ASSERT(result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND);
RegCloseKey(hKey);
return value;
}
bool Utility::registrySetKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName, DWORD type, const QVariant &value)
{
HKEY hKey;
// KEY_WOW64_64KEY is necessary because CLSIDs are "Redirected and reflected only for CLSIDs that do not specify InprocServer32 or InprocHandler32."
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa384253%28v=vs.85%29.aspx#redirected__shared__and_reflected_keys_under_wow64
// This shouldn't be an issue in our case since we use shell32.dll as InprocServer32, so we could write those registry keys for both 32 and 64bit.
// FIXME: Not doing so at the moment means that explorer will show the cloud provider, but 32bit processes' open dialogs (like the ownCloud client itself) won't show it.
REGSAM sam = KEY_WRITE | KEY_WOW64_64KEY;
LONG result = RegCreateKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(subKey.utf16()), 0, nullptr, 0, sam, nullptr, &hKey, nullptr);
ASSERT(result == ERROR_SUCCESS);
if (result != ERROR_SUCCESS)
return false;
result = -1;
switch (type) {
case REG_DWORD: {
DWORD dword = value.toInt();
result = RegSetValueEx(hKey, reinterpret_cast<LPCWSTR>(valueName.utf16()), 0, type, reinterpret_cast<const BYTE *>(&dword), sizeof(dword));
break;
}
case REG_EXPAND_SZ:
case REG_SZ: {
QString string = value.toString();
result = RegSetValueEx(hKey, reinterpret_cast<LPCWSTR>(valueName.utf16()), 0, type, reinterpret_cast<const BYTE *>(string.constData()), (string.size() + 1) * sizeof(QChar));
break;
}
default:
Q_UNREACHABLE();
}
ASSERT(result == ERROR_SUCCESS);
RegCloseKey(hKey);
return result == ERROR_SUCCESS;
}
bool Utility::registryDeleteKeyTree(HKEY hRootKey, const QString &subKey)
{
HKEY hKey;
REGSAM sam = DELETE | KEY_ENUMERATE_SUB_KEYS | KEY_QUERY_VALUE | KEY_SET_VALUE | KEY_WOW64_64KEY;
LONG result = RegOpenKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(subKey.utf16()), 0, sam, &hKey);
ASSERT(result == ERROR_SUCCESS);
if (result != ERROR_SUCCESS)
return false;
result = RegDeleteTree(hKey, nullptr);
RegCloseKey(hKey);
ASSERT(result == ERROR_SUCCESS);
result |= RegDeleteKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(subKey.utf16()), sam, 0);
ASSERT(result == ERROR_SUCCESS);
return result == ERROR_SUCCESS;
}
bool Utility::registryDeleteKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName)
{
HKEY hKey;
REGSAM sam = KEY_WRITE | KEY_WOW64_64KEY;
LONG result = RegOpenKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(subKey.utf16()), 0, sam, &hKey);
ASSERT(result == ERROR_SUCCESS);
if (result != ERROR_SUCCESS)
return false;
result = RegDeleteValue(hKey, reinterpret_cast<LPCWSTR>(valueName.utf16()));
ASSERT(result == ERROR_SUCCESS);
RegCloseKey(hKey);
return result == ERROR_SUCCESS;
}
bool Utility::registryWalkSubKeys(HKEY hRootKey, const QString &subKey, const std::function<void(HKEY, const QString &)> &callback)
{
HKEY hKey;
REGSAM sam = KEY_READ | KEY_WOW64_64KEY;
LONG result = RegOpenKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(subKey.utf16()), 0, sam, &hKey);
ASSERT(result == ERROR_SUCCESS);
if (result != ERROR_SUCCESS)
return false;
DWORD maxSubKeyNameSize;
// Get the largest keyname size once instead of relying each call on ERROR_MORE_DATA.
result = RegQueryInfoKey(hKey, nullptr, nullptr, nullptr, nullptr, &maxSubKeyNameSize, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr);
ASSERT(result == ERROR_SUCCESS);
if (result != ERROR_SUCCESS) {
RegCloseKey(hKey);
return false;
}
QString subKeyName;
subKeyName.reserve(maxSubKeyNameSize + 1);
DWORD retCode = ERROR_SUCCESS;
for (DWORD i = 0; retCode == ERROR_SUCCESS; ++i) {
Q_ASSERT(unsigned(subKeyName.capacity()) > maxSubKeyNameSize);
// Make the previously reserved capacity official again.
subKeyName.resize(subKeyName.capacity());
DWORD subKeyNameSize = subKeyName.size();
retCode = RegEnumKeyEx(hKey, i, reinterpret_cast<LPWSTR>(subKeyName.data()), &subKeyNameSize, nullptr, nullptr, nullptr, nullptr);
ASSERT(result == ERROR_SUCCESS || retCode == ERROR_NO_MORE_ITEMS);
if (retCode == ERROR_SUCCESS) {
// subKeyNameSize excludes the trailing \0
subKeyName.resize(subKeyNameSize);
// Pass only the sub keyname, not the full path.
callback(hKey, subKeyName);
}
}
RegCloseKey(hKey);
return retCode != ERROR_NO_MORE_ITEMS;
}
} // namespace OCC

View file

@ -57,6 +57,7 @@ set(client_SRCS
ignorelisteditor.cpp
lockwatcher.cpp
logbrowser.cpp
navigationpanehelper.cpp
networksettings.cpp
ocsjob.cpp
ocssharejob.cpp

View file

@ -401,6 +401,9 @@ void AccountSettings::slotFolderWizardAccepted()
*/
definition.ignoreHiddenFiles = folderMan->ignoreHiddenFiles();
// FIXME: Make this depend on a checkbox in settings.
definition.navigationPaneClsid = QUuid::createUuid();
auto selectiveSyncBlackList = folderWizard->property("selectiveSyncBlackList").toStringList();
folderMan->setSyncEnabled(true);

View file

@ -1075,6 +1075,12 @@ void FolderDefinition::save(QSettings &settings, const FolderDefinition &folder)
settings.setValue(QLatin1String("targetPath"), folder.targetPath);
settings.setValue(QLatin1String("paused"), folder.paused);
settings.setValue(QLatin1String("ignoreHiddenFiles"), folder.ignoreHiddenFiles);
// Happens only on Windows when the explorer integration is enabled.
if (!folder.navigationPaneClsid.isNull())
settings.setValue(QLatin1String("navigationPaneClsid"), folder.navigationPaneClsid);
else
settings.remove(QLatin1String("navigationPaneClsid"));
settings.endGroup();
}
@ -1088,6 +1094,7 @@ bool FolderDefinition::load(QSettings &settings, const QString &alias,
folder->targetPath = settings.value(QLatin1String("targetPath")).toString();
folder->paused = settings.value(QLatin1String("paused")).toBool();
folder->ignoreHiddenFiles = settings.value(QLatin1String("ignoreHiddenFiles"), QVariant(true)).toBool();
folder->navigationPaneClsid = settings.value(QLatin1String("navigationPaneClsid")).toUuid();
settings.endGroup();
// Old settings can contain paths with native separators. In the rest of the

View file

@ -27,6 +27,7 @@
#include <QObject>
#include <QStringList>
#include <QUuid>
#include <set>
class QThread;
@ -64,6 +65,8 @@ public:
bool paused;
/// whether the folder syncs hidden files
bool ignoreHiddenFiles;
/// The CLSID where this folder appears in registry for the Explorer navigation pane entry.
QUuid navigationPaneClsid;
/// Saves the folder definition, creating a new settings group.
static void save(QSettings &settings, const FolderDefinition &folder);
@ -135,6 +138,9 @@ public:
*/
QString remotePath() const;
void setNavigationPaneClsid(const QUuid &clsid) { _definition.navigationPaneClsid = clsid; }
QUuid navigationPaneClsid() const { return _definition.navigationPaneClsid; }
/**
* remote folder path with server url
*/

View file

@ -49,6 +49,7 @@ FolderMan::FolderMan(QObject *parent)
, _currentSyncFolder(0)
, _syncEnabled(true)
, _lockWatcher(new LockWatcher)
, _navigationPaneHelper(this)
, _appRestartRequired(false)
{
ASSERT(!_instance);
@ -894,6 +895,9 @@ Folder *FolderMan::addFolder(AccountState *accountState, const FolderDefinition
emit folderSyncStateChange(folder);
emit folderListChanged(_folderMap);
}
_navigationPaneHelper.scheduleUpdateCloudStorageRegistry();
return folder;
}
@ -1003,6 +1007,8 @@ void FolderMan::removeFolder(Folder *f)
delete f;
}
_navigationPaneHelper.scheduleUpdateCloudStorageRegistry();
emit folderListChanged(_folderMap);
}

View file

@ -22,6 +22,7 @@
#include "folder.h"
#include "folderwatcher.h"
#include "navigationpanehelper.h"
#include "syncfileitem.h"
class TestFolderMan;
@ -115,6 +116,7 @@ public:
static QString unescapeAlias(const QString &);
SocketApi *socketApi();
NavigationPaneHelper &navigationPaneHelper() { return _navigationPaneHelper; }
/**
* Check if @a path is a valid path for a new folder considering the already sync'ed items.
@ -315,6 +317,7 @@ private:
QTimer _startScheduledSyncTimer;
QScopedPointer<SocketApi> _socketApi;
NavigationPaneHelper _navigationPaneHelper;
bool _appRestartRequired;

View file

@ -0,0 +1,133 @@
/*
* Copyright (C) by Jocelyn Turcotte <jturcotte@woboq.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "navigationpanehelper.h"
#include "accountmanager.h"
#include "folderman.h"
#include <QDir>
#include <QCoreApplication>
namespace OCC {
Q_LOGGING_CATEGORY(lcNavPane, "gui.folder.navigationpane", QtInfoMsg)
NavigationPaneHelper::NavigationPaneHelper(FolderMan *folderMan)
: _folderMan(folderMan)
{
_updateCloudStorageRegistryTimer.setSingleShot(true);
connect(&_updateCloudStorageRegistryTimer, &QTimer::timeout, this, &NavigationPaneHelper::updateCloudStorageRegistry);
}
void NavigationPaneHelper::scheduleUpdateCloudStorageRegistry()
{
// Schedule the update to happen a bit later to avoid doing the update multiple times in a row.
if (!_updateCloudStorageRegistryTimer.isActive())
_updateCloudStorageRegistryTimer.start(500);
}
void NavigationPaneHelper::updateCloudStorageRegistry()
{
// Start by looking at every registered namespace extension for the sidebar, and look for an "ApplicationName" value
// that matches ours when we saved.
QVector<QUuid> entriesToRemove;
#ifdef Q_OS_WIN
Utility::registryWalkSubKeys(
HKEY_CURRENT_USER,
QStringLiteral("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Desktop\\NameSpace"),
[&entriesToRemove](HKEY key, const QString &subKey) {
QVariant appName = Utility::registryGetKeyValue(key, subKey, QStringLiteral("ApplicationName"));
if (appName.toString() == QLatin1String(APPLICATION_NAME)) {
QUuid clsid{ subKey };
Q_ASSERT(!clsid.isNull());
entriesToRemove.append(clsid);
}
});
#endif
// Then re-save every folder that has a valid navigationPaneClsid to the registry.
// We currently don't distinguish between new and existing CLSIDs, if it's there we just
// save over it. We at least need to update the tile in case we are suddently using multiple accounts.
foreach (Folder *folder, _folderMan->map()) {
if (!folder->navigationPaneClsid().isNull()) {
// If it already exists, unmark it for removal, this is a valid sync root.
entriesToRemove.removeOne(folder->navigationPaneClsid());
QString clsidStr = folder->navigationPaneClsid().toString();
QString clsidPath = QString() % "Software\\Classes\\CLSID\\" % clsidStr;
QString namespacePath = QString() % "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Desktop\\NameSpace\\" % clsidStr;
QString title = folder->shortGuiRemotePathOrAppName();
// Write the account name in the sidebar only when using more than one account.
if (AccountManager::instance()->accounts().size() > 1)
title = title % " - " % folder->accountState()->account()->displayName();
QString iconPath = QDir::toNativeSeparators(qApp->applicationFilePath());
QString targetFolderPath = QDir::toNativeSeparators(folder->cleanPath());
qCInfo(lcNavPane) << "Explorer Cloud storage provider: saving path" << targetFolderPath << "to CLSID" << clsidStr;
#ifdef Q_OS_WIN
// Steps taken from: https://msdn.microsoft.com/en-us/library/windows/desktop/dn889934%28v=vs.85%29.aspx
// Step 1: Add your CLSID and name your extension
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath, QString(), REG_SZ, title);
// Step 2: Set the image for your icon
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath + QStringLiteral("\\DefaultIcon"), QString(), REG_SZ, iconPath);
// Step 3: Add your extension to the Navigation Pane and make it visible
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath, QStringLiteral("System.IsPinnedToNameSpaceTree"), REG_DWORD, 0x1);
// Step 4: Set the location for your extension in the Navigation Pane
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath, QStringLiteral("SortOrderIndex"), REG_DWORD, 0x41);
// Step 5: Provide the dll that hosts your extension.
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath + QStringLiteral("\\InProcServer32"), QString(), REG_EXPAND_SZ, QStringLiteral("%systemroot%\\system32\\shell32.dll"));
// Step 6: Define the instance object
// Indicate that your namespace extension should function like other file folder structures in File Explorer.
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath + QStringLiteral("\\Instance"), QStringLiteral("CLSID"), REG_SZ, QStringLiteral("{0E5AAE11-A475-4c5b-AB00-C66DE400274E}"));
// Step 7: Provide the file system attributes of the target folder
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath + QStringLiteral("\\Instance\\InitPropertyBag"), QStringLiteral("Attributes"), REG_DWORD, 0x11);
// Step 8: Set the path for the sync root
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath + QStringLiteral("\\Instance\\InitPropertyBag"), QStringLiteral("TargetFolderPath"), REG_SZ, targetFolderPath);
// Step 9: Set appropriate shell flags
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath + QStringLiteral("\\ShellFolder"), QStringLiteral("FolderValueFlags"), REG_DWORD, 0x28);
// Step 10: Set the appropriate flags to control your shell behavior
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath + QStringLiteral("\\ShellFolder"), QStringLiteral("Attributes"), REG_DWORD, 0xF080004D);
// Step 11: Register your extension in the namespace root
Utility::registrySetKeyValue(HKEY_CURRENT_USER, namespacePath, QString(), REG_SZ, title);
// Step 12: Hide your extension from the Desktop
Utility::registrySetKeyValue(HKEY_CURRENT_USER, QStringLiteral("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\HideDesktopIcons\\NewStartPanel"), clsidStr, REG_DWORD, 0x1);
// For us, to later be able to iterate and find our own namespace entries and associated CLSID.
// Use the macro instead of the theme to make sure it matches with the uninstaller.
Utility::registrySetKeyValue(HKEY_CURRENT_USER, namespacePath, QStringLiteral("ApplicationName"), REG_SZ, QLatin1String(APPLICATION_NAME));
#else
// This code path should only occur on Windows (the config will be false, and the checkbox invisible on other platforms).
// Add runtime checks rather than #ifdefing out the whole code to help catch breakages when developing on other platforms.
Q_ASSERT(false);
#endif
}
}
// Then remove anything that isn't in our folder list anymore.
foreach (auto &clsid, entriesToRemove) {
QString clsidStr = clsid.toString();
QString clsidPath = QString() % "Software\\Classes\\CLSID\\" % clsidStr;
QString namespacePath = QString() % "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Desktop\\NameSpace\\" % clsidStr;
qCInfo(lcNavPane) << "Explorer Cloud storage provider: now unused, removing own CLSID" << clsidStr;
#ifdef Q_OS_WIN
Utility::registryDeleteKeyTree(HKEY_CURRENT_USER, clsidPath);
Utility::registryDeleteKeyTree(HKEY_CURRENT_USER, namespacePath);
Utility::registryDeleteKeyValue(HKEY_CURRENT_USER, QStringLiteral("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\HideDesktopIcons\\NewStartPanel"), clsidStr);
#endif
}
}
} // namespace OCC

View file

@ -0,0 +1,42 @@
/*
* Copyright (C) by Jocelyn Turcotte <jturcotte@woboq.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#ifndef NAVIGATIONPANEHELPER_H
#define NAVIGATIONPANEHELPER_H
#include <QObject>
#include <QTimer>
namespace OCC {
class FolderMan;
class NavigationPaneHelper : public QObject
{
Q_OBJECT
public:
NavigationPaneHelper(FolderMan *folderMan);
void scheduleUpdateCloudStorageRegistry();
private:
void updateCloudStorageRegistry();
FolderMan *_folderMan;
QTimer _updateCloudStorageRegistryTimer;
};
} // namespace OCC
#endif // NAVIGATIONPANEHELPER_H

View file

@ -579,6 +579,8 @@ void OwncloudSetupWizard::slotAssistantFinished(int result)
folderDefinition.localPath = localFolder;
folderDefinition.targetPath = FolderDefinition::prepareTargetPath(_remoteFolder);
folderDefinition.ignoreHiddenFiles = folderMan->ignoreHiddenFiles();
// FIXME: Make this depend on a checkbox in settings.
folderDefinition.navigationPaneClsid = QUuid::createUuid();
auto f = folderMan->addFolder(account, folderDefinition);
if (f) {

View file

@ -65,6 +65,7 @@ list(APPEND FolderMan_SRC ../src/gui/accountstate.cpp )
list(APPEND FolderMan_SRC ../src/gui/syncrunfilelog.cpp )
list(APPEND FolderMan_SRC ../src/gui/lockwatcher.cpp )
list(APPEND FolderMan_SRC ../src/gui/guiutility.cpp )
list(APPEND FolderMan_SRC ../src/gui/navigationpanehelper.cpp )
list(APPEND FolderMan_SRC ${FolderWatcher_SRC})
list(APPEND FolderMan_SRC stub.cpp )
owncloud_add_test(FolderMan "${FolderMan_SRC}")