]> cloud.milkyroute.net Git - dolphin.git/commitdiff
ViewProperties: Store view properties in extended file attributes
authorMéven Car <meven@kde.org>
Sun, 27 Oct 2024 17:20:40 +0000 (17:20 +0000)
committerMéven Car <meven@kde.org>
Sun, 27 Oct 2024 17:20:40 +0000 (17:20 +0000)
Existing settings are converted.

Works on most FS except FAT/exFAT which fallback to .directory files.

If the extended file attributes (in ADS in Windows) can't be saved, they are saved to file as before.

BUG: 322922

You can see file xattr using for instance for Unix filesystems:

getfattr -d /home/meven

src/CMakeLists.txt
src/settings/dolphin_directoryviewpropertysettings.kcfg
src/settings/viewmodes/generalviewsettingspage.cpp
src/tests/CMakeLists.txt
src/tests/viewpropertiestest.cpp
src/views/dolphinview.cpp
src/views/viewproperties.cpp
src/views/viewproperties.h

index ce5b5461319dbb3c7792d0944f12cbecd470441d..7d1206e48a6326c9bdb4c5aad63bbb4bf61add31 100644 (file)
@@ -217,6 +217,7 @@ target_link_libraries(
     KF6::WidgetsAddons
     KF6::Codecs
     KF6::KCMUtils
+    KF6::FileMetaData
 
     ${FTS_LIB}
 )
@@ -224,7 +225,6 @@ target_link_libraries(
 if(HAVE_BALOO)
     target_link_libraries(
         dolphinprivate PUBLIC
-        KF6::FileMetaData
         KF6::Baloo
         KF6::BalooWidgets
     )
index f4d288369605528ab559a897119be875ab8f720c..e0c8aa1cca430a58817bcb58f69d2c3d90e8c1c3 100644 (file)
@@ -24,7 +24,7 @@
         <entry name="ViewMode" type="Int" >
             <label context="@label">View Mode</label>
             <whatsthis context="@info:whatsthis">This option controls the style of the view. Currently supported values include icons (0), details (1) and column (2) views.</whatsthis>
-            <default>DolphinView::IconsView</default>
+            <default code="true">DolphinView::IconsView</default>
         </entry>
 
         <entry name="PreviewsShown" type="Bool" >
index 51ab664f100da1e640674cb935dae46b8d1b53b5..c518147dd65e974d4f3f2fdd399421f2dde05561 100644 (file)
@@ -40,7 +40,9 @@ GeneralViewSettingsPage::GeneralViewSettingsPage(const QUrl &url, QWidget *paren
     globalViewPropsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
 
     m_localViewProps = new QRadioButton(i18nc("@option:radio", "Remember display style for each folder"));
-    QLabel *localViewPropsLabel = new QLabel(i18nc("@info", "Dolphin will create a hidden .directory file in each folder you change view properties for."));
+    QLabel *localViewPropsLabel = new QLabel(i18nc("@info",
+                                                   "Dolphin will add file system metadata to folders you change view properties for. If that is not possible, "
+                                                   "a hidden .directory file is created instead."));
     localViewPropsLabel->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
     localViewPropsLabel->setWordWrap(true);
     localViewPropsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
index 20b682fe0d80cf88255bfb91b4aa79f71c6acf55..1ef82e3d9e85c1050117f6700a5495504f6830a8 100644 (file)
@@ -59,7 +59,7 @@ endif()
 # ViewPropertiesTest
 ecm_add_test(viewpropertiestest.cpp testdir.cpp
 TEST_NAME viewpropertiestest
-LINK_LIBRARIES dolphinprivate dolphinstatic Qt6::Test)
+LINK_LIBRARIES dolphinprivate dolphinstatic Qt6::Test KF6::FileMetaData)
 
 # DolphinMainWindowTest
 ecm_add_test(dolphinmainwindowtest.cpp testdir.cpp ${CMAKE_SOURCE_DIR}/src/dolphin.qrc
index 7b30203d273098d0984a3be2d24d15b098ae39d3..9c2c9466bc4f74b483b00433420e43b8fd59a64f 100644 (file)
@@ -8,6 +8,9 @@
 #include "dolphin_generalsettings.h"
 #include "testdir.h"
 
+#include <KFileMetaData/UserMetaData>
+
+#include <QStorageInfo>
 #include <QTest>
 
 class ViewPropertiesTest : public QObject
@@ -20,7 +23,11 @@ private Q_SLOTS:
     void cleanup();
 
     void testReadOnlyBehavior();
+    void testReadOnlyDirectory();
     void testAutoSave();
+    void testParamMigrationToFileAttr();
+    void testParamMigrationToFileAttrKeepDirectory();
+    void testExtendedAttributeFull();
 
 private:
     bool m_globalViewProps;
@@ -73,6 +80,45 @@ void ViewPropertiesTest::testReadOnlyBehavior()
     QVERIFY(!QFile::exists(dotDirectoryFile));
 }
 
+void ViewPropertiesTest::testReadOnlyDirectory()
+{
+    auto localFolder = m_testDir->url().toLocalFile();
+    QString dotDirectoryFile = localFolder + "/.directory";
+    QVERIFY(!QFile::exists(dotDirectoryFile));
+
+    // restrict write permissions
+    QVERIFY(QFile(localFolder).setPermissions(QFileDevice::ReadOwner));
+
+    QScopedPointer<ViewProperties> props(new ViewProperties(m_testDir->url()));
+    QVERIFY(props->isAutoSaveEnabled());
+    props->setSortRole("someNewSortRole");
+    props.reset();
+
+    const auto destinationDir = props->destinationDir(QStringLiteral("local")) + localFolder;
+    qDebug() << destinationDir;
+    QVERIFY(QDir(destinationDir).exists());
+
+    QVERIFY(!QFile::exists(dotDirectoryFile));
+    KFileMetaData::UserMetaData metadata(localFolder);
+    auto viewProperties = metadata.attribute(QStringLiteral("kde.fm.viewproperties#1"));
+    QVERIFY(viewProperties.isEmpty());
+
+    props.reset(new ViewProperties(m_testDir->url()));
+    QVERIFY(props->isAutoSaveEnabled());
+    QCOMPARE(props->sortRole(), "someNewSortRole");
+    props.reset();
+
+    metadata = KFileMetaData::UserMetaData(destinationDir);
+    if (metadata.isSupported()) {
+        QVERIFY(metadata.hasAttribute("kde.fm.viewproperties#1"));
+    } else {
+        QVERIFY(QFile::exists(destinationDir + "/.directory"));
+    }
+
+    // un-restrict write permissions
+    QFile(localFolder).setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
+}
+
 void ViewPropertiesTest::testAutoSave()
 {
     QString dotDirectoryFile = m_testDir->url().toLocalFile() + "/.directory";
@@ -83,7 +129,151 @@ void ViewPropertiesTest::testAutoSave()
     props->setSortRole("someNewSortRole");
     props.reset();
 
-    QVERIFY(QFile::exists(dotDirectoryFile));
+    KFileMetaData::UserMetaData metadata(m_testDir->url().toLocalFile());
+    if (metadata.isSupported()) {
+        auto viewProperties = metadata.attribute(QStringLiteral("kde.fm.viewproperties#1"));
+        QVERIFY(!viewProperties.isEmpty());
+        QVERIFY(!QFile::exists(dotDirectoryFile));
+    } else {
+        QVERIFY(QFile::exists(dotDirectoryFile));
+    }
+}
+
+void ViewPropertiesTest::testParamMigrationToFileAttr()
+{
+    QString dotDirectoryFilePath = m_testDir->url().toLocalFile() + "/.directory";
+    QVERIFY(!QFile::exists(dotDirectoryFilePath));
+
+    const char *settingsContent = R"SETTINGS("
+[Dolphin]
+Version=4
+ViewMode=1
+Timestamp=2023,12,29,10,44,15.793
+VisibleRoles=text,CustomizedDetails,Details_text,Details_modificationtime,Details_type
+
+[Settings]
+HiddenFilesShown=true)SETTINGS";
+    auto dotDirectoryFile = QFile(dotDirectoryFilePath);
+    QVERIFY(dotDirectoryFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate));
+    QTextStream out(&dotDirectoryFile);
+    out << settingsContent;
+    dotDirectoryFile.close();
+
+    KFileMetaData::UserMetaData metadata(m_testDir->url().toLocalFile());
+    {
+        QScopedPointer<ViewProperties> props(new ViewProperties(m_testDir->url()));
+        QCOMPARE(props->viewMode(), DolphinView::Mode::DetailsView);
+        QVERIFY(props->hiddenFilesShown());
+        props->save();
+
+        if (metadata.isSupported()) {
+            auto viewProperties = metadata.attribute(QStringLiteral("kde.fm.viewproperties#1"));
+            QVERIFY(!viewProperties.isEmpty());
+            QVERIFY(!QFile::exists(dotDirectoryFilePath));
+        } else {
+            QVERIFY(QFile::exists(dotDirectoryFilePath));
+        }
+    }
+
+    ViewProperties props(m_testDir->url());
+    QCOMPARE(props.viewMode(), DolphinView::Mode::DetailsView);
+    QVERIFY(props.hiddenFilesShown());
+}
+
+void ViewPropertiesTest::testParamMigrationToFileAttrKeepDirectory()
+{
+    QString dotDirectoryFilePath = m_testDir->url().toLocalFile() + "/.directory";
+    QVERIFY(!QFile::exists(dotDirectoryFilePath));
+
+    const char *settingsContent = R"SETTINGS("
+[Dolphin]
+Version=4
+ViewMode=1
+Timestamp=2023,12,29,10,44,15.793
+VisibleRoles=text,CustomizedDetails,Details_text,Details_modificationtime,Details_type
+
+[Settings]
+HiddenFilesShown=true
+
+[Other]
+ThoseShouldBeKept=true
+)SETTINGS";
+    auto dotDirectoryFile = QFile(dotDirectoryFilePath);
+    QVERIFY(dotDirectoryFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate));
+    QTextStream out(&dotDirectoryFile);
+    out << settingsContent;
+    dotDirectoryFile.close();
+
+    KFileMetaData::UserMetaData metadata(m_testDir->url().toLocalFile());
+    {
+        QScopedPointer<ViewProperties> props(new ViewProperties(m_testDir->url()));
+        QCOMPARE(props->viewMode(), DolphinView::Mode::DetailsView);
+        QVERIFY(props->hiddenFilesShown());
+        props->save();
+
+        if (metadata.isSupported()) {
+            auto viewProperties = metadata.attribute(QStringLiteral("kde.fm.viewproperties#1"));
+            QVERIFY(!viewProperties.isEmpty());
+        }
+
+        QVERIFY(QFile::exists(dotDirectoryFilePath));
+        KConfig directorySettings(dotDirectoryFilePath, KConfig::SimpleConfig);
+        QCOMPARE(directorySettings.groupList(), {"Other"});
+    }
+
+    ViewProperties props(m_testDir->url());
+    QVERIFY(props.hiddenFilesShown());
+    QCOMPARE(props.viewMode(), DolphinView::Mode::DetailsView);
+
+    QVERIFY(QFile::exists(dotDirectoryFilePath));
+}
+
+void ViewPropertiesTest::testExtendedAttributeFull()
+{
+#ifndef Q_OS_UNIX
+    QSKIP("Only unix is supported, for this test");
+#endif
+    QString dotDirectoryFile = m_testDir->url().toLocalFile() + "/.directory";
+    QVERIFY(!QFile::exists(dotDirectoryFile));
+
+    KFileMetaData::UserMetaData metadata(m_testDir->url().toLocalFile());
+    if (!metadata.isSupported()) {
+        QSKIP("need extended attribute/filesystem metadata to be usefull");
+    }
+
+    QStorageInfo storageInfo(m_testDir->url().toLocalFile());
+    auto blockSize = storageInfo.blockSize();
+
+    KFileMetaData::UserMetaData::Error result;
+    // write a close to block size theorical maximum size for attributes in Linux for ext4
+    // and btrfs (4Kib typically) when ReiserFS/XFS allow XATTR_SIZE_MAX (64Kib)
+    result = metadata.setAttribute("data", QString(blockSize - 50, 'a'));
+    if (result != KFileMetaData::UserMetaData::NoSpace) {
+        QSKIP("File system supports metadata bigger than file system block size");
+    }
+
+    // write a close to 4k attribute, maximum size in Linux for ext4
+    // so next writing the file metadata fails
+    result = metadata.setAttribute("data", QString(blockSize - 60, 'a'));
+    QCOMPARE(result, KFileMetaData::UserMetaData::NoError);
+
+    QScopedPointer<ViewProperties> props(new ViewProperties(m_testDir->url()));
+    QVERIFY(props->isAutoSaveEnabled());
+    props->setSortRole("someNewSortRole");
+    props.reset();
+
+    if (metadata.isSupported()) {
+        auto viewProperties = metadata.attribute(QStringLiteral("kde.fm.viewproperties#1"));
+        QVERIFY(viewProperties.isEmpty());
+        QVERIFY(QFile::exists(dotDirectoryFile));
+
+        QFile dotDirectory(dotDirectoryFile);
+        KConfig viewSettings(dotDirectoryFile, KConfig::SimpleConfig);
+        QCOMPARE(viewSettings.groupList(), {"Dolphin"});
+        QCOMPARE(viewSettings.group("Dolphin").readEntry("SortRole"), "someNewSortRole");
+    } else {
+        QVERIFY(QFile::exists(dotDirectoryFile));
+    }
 }
 
 QTEST_GUILESS_MAIN(ViewPropertiesTest)
index f9f32d35cbc28b6fda0e41667dfd06cccb94788c..0c5ebb1df72a2a82e0372751d8183cf40ed119ec 100644 (file)
@@ -375,9 +375,8 @@ void DolphinView::setGroupedSorting(bool grouped)
 
     ViewProperties props(viewPropertiesUrl());
     props.setGroupedSorting(grouped);
-    props.save();
 
-    m_container->controller()->model()->setGroupedSorting(grouped);
+    m_model->setGroupedSorting(grouped);
 
     Q_EMIT groupedSortingChanged(grouped);
 }
index d65572f21ee1c203bd1e405c3dd5a7fe28bd3517..0536e028dae764cb4fb54d81d56941046d5d510a 100644 (file)
 #include "dolphindebug.h"
 
 #include <QCryptographicHash>
+#include <QTemporaryFile>
 
 #include <KFileItem>
+#include <KFileMetaData/UserMetaData>
 
 namespace
 {
@@ -31,6 +33,68 @@ const char CustomizedDetailsString[] = "CustomizedDetails";
 const char ViewPropertiesFileName[] = ".directory";
 }
 
+ViewPropertySettings *ViewProperties::loadProperties(const QString &folderPath) const
+{
+    const QString settingsFile = folderPath + QDir::separator() + ViewPropertiesFileName;
+
+    KFileMetaData::UserMetaData metadata(folderPath);
+    if (!metadata.isSupported()) {
+        return new ViewPropertySettings(KSharedConfig::openConfig(settingsFile, KConfig::SimpleConfig));
+    }
+
+    QTemporaryFile tempFile;
+    tempFile.setAutoRemove(false);
+    if (!tempFile.open()) {
+        qCWarning(DolphinDebug) << "Could not open temp file";
+        return nullptr;
+    }
+
+    if (QFile::exists(settingsFile)) {
+        // copy settings to tempfile to load them separately
+        QFile::remove(tempFile.fileName());
+        QFile::copy(settingsFile, tempFile.fileName());
+
+        auto config = KConfig(tempFile.fileName(), KConfig::SimpleConfig);
+        // ignore settings that are outside of dolphin scope
+        if (config.hasGroup("Dolphin") || config.hasGroup("Settings")) {
+            const auto groupList = config.groupList();
+            for (const auto &group : groupList) {
+                if (group != QStringLiteral("Dolphin") && group != QStringLiteral("Settings")) {
+                    config.deleteGroup(group);
+                }
+            }
+            return new ViewPropertySettings(KSharedConfig::openConfig(tempFile.fileName(), KConfig::SimpleConfig));
+
+        } else if (!config.groupList().isEmpty()) {
+            // clear temp file content
+            QFile::remove(tempFile.fileName());
+        }
+    }
+
+    // load from metadata
+    const QString viewPropertiesString = metadata.attribute(QStringLiteral("kde.fm.viewproperties#1"));
+    if (!viewPropertiesString.isEmpty()) {
+        // load view properties from xattr to temp file then loads into ViewPropertySettings
+        // clear the temp file
+        QFile outputFile(tempFile.fileName());
+        outputFile.open(QIODevice::WriteOnly);
+        outputFile.write(viewPropertiesString.toUtf8());
+        outputFile.close();
+    }
+    return new ViewPropertySettings(KSharedConfig::openConfig(tempFile.fileName(), KConfig::SimpleConfig));
+}
+
+ViewPropertySettings *ViewProperties::defaultProperties() const
+{
+    auto props = loadProperties(destinationDir(QStringLiteral("global")));
+    if (props == nullptr) {
+        qCWarning(DolphinDebug) << "Could not load default global viewproperties";
+        props = new ViewPropertySettings;
+    }
+
+    return props;
+}
+
 ViewProperties::ViewProperties(const QUrl &url)
     : m_changedProps(false)
     , m_autoSave(true)
@@ -90,14 +154,22 @@ ViewProperties::ViewProperties(const QUrl &url)
         m_filePath = destinationDir(QStringLiteral("remote")) + m_filePath;
     }
 
-    const QString file = m_filePath + QDir::separator() + ViewPropertiesFileName;
-    m_node = new ViewPropertySettings(KSharedConfig::openConfig(file));
+    m_node = loadProperties(m_filePath);
+
+    bool useDefaultSettings = useGlobalViewProps ||
+        // If the props timestamp is too old,
+        // use default values instead.
+        (m_node != nullptr && (!useGlobalViewProps || useSearchView || useTrashView || useRecentDocumentsView || useDownloadsView)
+         && m_node->timestamp() < settings->viewPropsTimestamp());
 
-    // If the .directory file does not exist or the timestamp is too old,
-    // use default values instead.
-    const bool useDefaultProps = (!useGlobalViewProps || useSearchView || useTrashView || useRecentDocumentsView || useDownloadsView)
-        && (!QFile::exists(file) || (m_node->timestamp() < settings->viewPropsTimestamp()));
-    if (useDefaultProps) {
+    if (m_node == nullptr) {
+        // no settings found for m_filepath, load defaults
+        m_node = defaultProperties();
+        useDefaultSettings = true;
+    }
+
+    // default values for special directories
+    if (useDefaultSettings) {
         if (useSearchView) {
             const QString path = url.path();
 
@@ -132,14 +204,6 @@ ViewProperties::ViewProperties(const QUrl &url)
                 setSortRole(QByteArrayLiteral("modificationtime"));
             }
         } else {
-            // The global view-properties act as default for directories without
-            // any view-property configuration. Constructing a ViewProperties
-            // instance for an empty QUrl ensures that the global view-properties
-            // are loaded.
-            QUrl emptyUrl;
-            ViewProperties defaultProps(emptyUrl);
-            setDirProperties(defaultProps);
-
             m_changedProps = false;
         }
     }
@@ -172,6 +236,11 @@ ViewProperties::~ViewProperties()
         save();
     }
 
+    if (!m_node->config()->name().endsWith(ViewPropertiesFileName)) {
+        // remove temp file
+        QFile::remove(m_node->config()->name());
+    }
+
     delete m_node;
     m_node = nullptr;
 }
@@ -412,17 +481,94 @@ void ViewProperties::update()
 void ViewProperties::save()
 {
     qCDebug(DolphinDebug) << "Saving view-properties to" << m_filePath;
+
+    auto cleanDotDirectoryFile = [this]() {
+        const QString settingsFile = m_filePath + QDir::separator() + ViewPropertiesFileName;
+        if (QFile::exists(settingsFile)) {
+            qCDebug(DolphinDebug) << "cleaning .directory" << settingsFile;
+            KConfig cfg(settingsFile, KConfig::OpenFlag::SimpleConfig);
+            const auto groupList = cfg.groupList();
+            for (const auto &group : groupList) {
+                if (group == QStringLiteral("Dolphin") || group == QStringLiteral("Settings")) {
+                    cfg.deleteGroup(group);
+                }
+            }
+            if (cfg.groupList().isEmpty()) {
+                QFile::remove(settingsFile);
+            } else if (cfg.isDirty()) {
+                cfg.sync();
+            }
+        }
+    };
+
+    // ensures the destination dir exists, in case we don't write metadata directly on the folder
+    QDir destinationDir(m_filePath);
+    if (!destinationDir.exists() && !destinationDir.mkpath(m_filePath)) {
+        qCWarning(DolphinDebug) << "Could not create fake directory to store metadata";
+    }
+
+    KFileMetaData::UserMetaData metaData(m_filePath);
+    if (metaData.isSupported()) {
+        const auto metaDataKey = QStringLiteral("kde.fm.viewproperties#1");
+
+        const auto items = m_node->items();
+        const bool allDefault = std::all_of(items.cbegin(), items.cend(), [this](const KConfigSkeletonItem *item) {
+            return item->name() == "Timestamp" || (item->name() == "Version" && m_node->version() == CurrentViewPropertiesVersion) || item->isDefault();
+        });
+        if (allDefault) {
+            if (metaData.hasAttribute(metaDataKey)) {
+                qCDebug(DolphinDebug) << "clearing extended attributes for " << m_filePath;
+                const auto result = metaData.setAttribute(metaDataKey, QString());
+                if (result != KFileMetaData::UserMetaData::NoError) {
+                    qCWarning(DolphinDebug) << "could not clear extended attributes for " << m_filePath << "error:" << result;
+                }
+            }
+            cleanDotDirectoryFile();
+            return;
+        }
+
+        // save config to disk
+        if (!m_node->save()) {
+            qCWarning(DolphinDebug) << "could not save viewproperties" << m_node->config()->name();
+            return;
+        }
+
+        QFile configFile(m_node->config()->name());
+        if (!configFile.open(QIODevice::ReadOnly)) {
+            qCWarning(DolphinDebug) << "Could not open readonly config file" << m_node->config()->name();
+        } else {
+            // load config from disk
+            const QString viewPropertiesString = configFile.readAll();
+
+            // save to xattr
+            const auto result = metaData.setAttribute(metaDataKey, viewPropertiesString);
+            if (result != KFileMetaData::UserMetaData::NoError) {
+                if (result == KFileMetaData::UserMetaData::NoSpace) {
+                    // copy settings to dotDirectory file as fallback
+                    if (!configFile.copy(m_filePath + QDir::separator() + ViewPropertiesFileName)) {
+                        qCWarning(DolphinDebug) << "could not write viewproperties to .directory for dir " << m_filePath;
+                    }
+                    // free the space used by viewproperties from the file metadata
+                    metaData.setAttribute(metaDataKey, "");
+                } else {
+                    qCWarning(DolphinDebug) << "could not save viewproperties to extended attributes for dir " << m_filePath << "error:" << result;
+                }
+                // keep .directory file
+                return;
+            }
+            cleanDotDirectoryFile();
+        }
+
+        m_changedProps = false;
+        return;
+    }
+
     QDir dir;
     dir.mkpath(m_filePath);
     m_node->setVersion(CurrentViewPropertiesVersion);
     m_node->save();
-    m_changedProps = false;
-}
 
-bool ViewProperties::exist() const
-{
-    const QString file = m_filePath + QDir::separator() + ViewPropertiesFileName;
-    return QFile::exists(file);
+    m_changedProps = false;
 }
 
 QString ViewProperties::destinationDir(const QString &subDir) const
index 29827c38b61c4175e37e366bd07b3c092740dec8..44c7034823ac116449d1ed040401a4b8203998b2 100644 (file)
@@ -108,15 +108,6 @@ public:
      */
     void save();
 
-    /**
-     * @return True if properties for the given URL exist:
-     *         As soon as the properties for an URL have been saved with
-     *         ViewProperties::save(), true will be returned. If false is
-     *         returned, the default view-properties are used.
-     */
-    bool exist() const;
-
-private:
     /**
      * Returns the destination directory path where the view
      * properties are stored. \a subDir specifies the used sub
@@ -124,6 +115,7 @@ private:
      */
     QString destinationDir(const QString &subDir) const;
 
+private:
     /**
      * Returns the view-mode prefix when storing additional properties for
      * a view-mode.
@@ -161,6 +153,11 @@ private:
      */
     static QString directoryHashForUrl(const QUrl &url);
 
+    /** @returns a ViewPropertySettings object with properties loaded for the directory at @param filePath. Ownership is returned to the caller. */
+    ViewPropertySettings *loadProperties(const QString &folderPath) const;
+    /** @returns a ViewPropertySettings object with the globally configured default values. Ownership is returned to the caller. */
+    ViewPropertySettings *defaultProperties() const;
+
     Q_DISABLE_COPY(ViewProperties)
 
 private: