From 047a4e461e322778cb0a05b2221b38afa871213c Mon Sep 17 00:00:00 2001 From: Martchus Date: Sun, 7 Apr 2024 23:29:23 +0200 Subject: [PATCH] Allow refreshing root in file browser --- syncthingconnector/syncthingconnection.h | 10 +- .../syncthingconnection_requests.cpp | 55 ++++---- .../syncthingconnectionmockhelpers.cpp | 11 +- .../testfiles/mocks/browse.json | 46 +++++++ syncthingmodel/CMakeLists.txt | 3 +- syncthingmodel/syncthingfilemodel.cpp | 127 +++++++++++++----- syncthingmodel/syncthingfilemodel.h | 12 +- syncthingmodel/syncthingicons.cpp | 2 +- syncthingmodel/tests/models.cpp | 94 ++++++++++++- syncthingwidgets/misc/otherdialogs.cpp | 2 +- testhelper/helper.h | 3 +- 11 files changed, 286 insertions(+), 79 deletions(-) create mode 100644 syncthingconnector/testfiles/mocks/browse.json diff --git a/syncthingconnector/syncthingconnection.h b/syncthingconnector/syncthingconnection.h index 1fb6ef6..ec6dfbc 100644 --- a/syncthingconnector/syncthingconnection.h +++ b/syncthingconnector/syncthingconnection.h @@ -53,10 +53,10 @@ enum class SyncthingItemType { Unknown, File, Directory }; struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItem { QString name; - CppUtilities::DateTime modificationTime; + CppUtilities::DateTime modificationTime = CppUtilities::DateTime(); std::size_t size = std::size_t(); SyncthingItemType type = SyncthingItemType::Unknown; - std::vector children; + std::vector> children; SyncthingItem *parent = nullptr; // not populated but might be set as needed (take care in case the pointer gets invalidated) std::size_t index = std::size_t(); int level = 0; // the level of nesting, does *not* include levels of the prefix @@ -253,8 +253,8 @@ public Q_SLOTS: public: // methods to GET or POST information from/to Syncthing (non-slots) - QMetaObject::Connection browse( - const QString &dirId, const QString &prefix, int level, std::function &&)> &&callback); + QMetaObject::Connection browse(const QString &dirId, const QString &prefix, int level, + std::function> &&, QString &&)> &&callback); Q_SIGNALS: void newConfig(const QJsonObject &rawConfig); @@ -369,7 +369,7 @@ private Q_SLOTS: private: // handler to evaluate results from request...() methods - void readBrowse(const QString &dirId, int levels, std::function &&)> &&callback); + void readBrowse(const QString &dirId, int levels, std::function> &&, QString &&)> &&callback); // internal helper methods struct Reply { diff --git a/syncthingconnector/syncthingconnection_requests.cpp b/syncthingconnector/syncthingconnection_requests.cpp index 5097c73..b3015e4 100644 --- a/syncthingconnector/syncthingconnection_requests.cpp +++ b/syncthingconnector/syncthingconnection_requests.cpp @@ -1590,8 +1590,8 @@ void SyncthingConnection::readRevert() * consume results of a specific request. Errors are still reported via the error() signal so there's no extra error handling * required. Note that \a callback is *not* invoked in the error case. */ -QMetaObject::Connection SyncthingConnection::browse( - const QString &dirId, const QString &prefix, int levels, std::function &&)> &&callback) +QMetaObject::Connection SyncthingConnection::browse(const QString &dirId, const QString &prefix, int levels, + std::function> &&, QString &&)> &&callback) { auto query = QUrlQuery(); query.addQueryItem(QStringLiteral("folder"), formatQueryItem(dirId)); @@ -1607,7 +1607,7 @@ QMetaObject::Connection SyncthingConnection::browse( } /// \cond -static void readSyncthingItems(const QJsonArray &array, std::vector &into, int level, int levels) +static void readSyncthingItems(const QJsonArray &array, std::vector> &into, int level, int levels) { into.reserve(static_cast(array.size())); for (const auto &jsonItem : array) { @@ -1618,57 +1618,66 @@ static void readSyncthingItems(const QJsonArray &array, std::vector(jsonItemObj - .value(QLatin1String("size")) + auto &item = into.emplace_back(std::make_unique()); + item->name = jsonItemObj.value(QLatin1String("name")).toString(); + item->modificationTime = CppUtilities::DateTime::fromIsoStringGmt(jsonItemObj.value(QLatin1String("modTime")).toString().toUtf8().data()); + item->size = static_cast(jsonItemObj + .value(QLatin1String("size")) #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) - .toInteger() + .toInteger() #else - .toDouble() + .toDouble() #endif ); - item.index = index; - item.level = level; + item->index = index; + item->level = level; if (type == QLatin1String("FILE_INFO_TYPE_FILE")) { - item.type = SyncthingItemType::File; + item->type = SyncthingItemType::File; } else if (type == QLatin1String("FILE_INFO_TYPE_DIRECTORY")) { - item.type = SyncthingItemType::Directory; + item->type = SyncthingItemType::Directory; } - readSyncthingItems(children.toArray(), item.children, level + 1, levels); - item.childrenPopulated = !levels || level < levels; + readSyncthingItems(children.toArray(), item->children, level + 1, levels); + item->childrenPopulated = !levels || level < levels; } } /// \endcond /*! - * \brief Reads the response of browse() and reports results via the specified \a callback or emits error() in case of an error. + * \brief Reads the response of browse() and reports results via the specified \a callback. Emits error() in case of an error. + * \remarks The \a callback is also emitted in the error case (with the error message as second parameter and an empty list of items). */ -void SyncthingConnection::readBrowse(const QString &dirId, int levels, std::function &&)> &&callback) +void SyncthingConnection::readBrowse( + const QString &dirId, int levels, std::function> &&, QString &&)> &&callback) { auto const [reply, response] = prepareReply(); if (!reply) { return; } - auto items = std::vector(); + auto items = std::vector>(); switch (reply->error()) { case QNetworkReply::NoError: { auto jsonError = QJsonParseError(); const auto replyDoc = QJsonDocument::fromJson(response, &jsonError); if (jsonError.error != QJsonParseError::NoError) { - emit error(tr("Unable to parse response for browsing \"%1\": ").arg(dirId) + jsonError.errorString(), SyncthingErrorCategory::Parsing, - QNetworkReply::NoError); + auto errorMessage = tr("Unable to parse response for browsing \"%1\": ").arg(dirId) + jsonError.errorString(); + emit error(errorMessage, SyncthingErrorCategory::Parsing, QNetworkReply::NoError); + if (callback) { + callback(std::move(items), std::move(errorMessage)); + } return; } readSyncthingItems(replyDoc.array(), items, 0, levels); if (callback) { - callback(std::move(items)); + callback(std::move(items), QString()); } break; } default: - emitError(tr("Unable to browse \"%1\": ").arg(dirId), SyncthingErrorCategory::SpecificRequest, reply); + auto errorMessage = tr("Unable to browse \"%1\": ").arg(dirId); + emitError(errorMessage, SyncthingErrorCategory::SpecificRequest, reply); + if (callback) { + callback(std::move(items), std::move(errorMessage)); + } } } diff --git a/syncthingconnector/syncthingconnectionmockhelpers.cpp b/syncthingconnector/syncthingconnectionmockhelpers.cpp index 880f4c3..0af2aa6 100644 --- a/syncthingconnector/syncthingconnectionmockhelpers.cpp +++ b/syncthingconnector/syncthingconnectionmockhelpers.cpp @@ -24,7 +24,7 @@ namespace Data { */ namespace TestData { static bool initialized = false; -static std::string config, status, folderStats, deviceStats, errors, folderStatus, folderStatus2, folderStatus3, pullErrors, connections, version, empty; +static std::string config, status, folderStats, deviceStats, errors, folderStatus, folderStatus2, folderStatus3, pullErrors, connections, version, empty, browse; static std::string events[7]; } // namespace TestData @@ -62,10 +62,10 @@ void setupTestData() // read mock files for REST-API const char *const fileNames[] = { "config", "status", "folderstats", "devicestats", "errors", "folderstatus-01", "folderstatus-02", - "folderstatus-03", "pullerrors-01", "connections", "version", "empty" }; + "folderstatus-03", "pullerrors-01", "connections", "version", "empty", "browse" }; const char *const *fileName = fileNames; for (auto *const testDataVariable : { &config, &status, &folderStats, &deviceStats, &errors, &folderStatus, &folderStatus2, &folderStatus3, - &pullErrors, &connections, &version, &empty }) { + &pullErrors, &connections, &version, &empty, &browse }) { *testDataVariable = readMockFile(testApp.testFilePath(argsToString("mocks/", *fileName, ".json"))); ++fileName; } @@ -171,6 +171,11 @@ MockedReply *MockedReply::forRequest(const QString &method, const QString &path, } else if (folder == QLatin1String("forever-alone")) { buffer = &folderStatus3; } + } else if (path == QLatin1String("db/browse") && !query.hasQueryItem(QStringLiteral("prefix"))) { + const auto folder = query.queryItemValue(QStringLiteral("folder")); + if (folder == QLatin1String("GXWxf-3zgnU")) { + buffer = &browse; + } } else if (path == QLatin1String("folder/pullerrors")) { const QString folder(query.queryItemValue(QStringLiteral("folder"))); if (folder == QLatin1String("GXWxf-3zgnU") && s_eventIndex >= 6) { diff --git a/syncthingconnector/testfiles/mocks/browse.json b/syncthingconnector/testfiles/mocks/browse.json new file mode 100644 index 0000000..63aebdd --- /dev/null +++ b/syncthingconnector/testfiles/mocks/browse.json @@ -0,0 +1,46 @@ +[ + { + "modTime" : "2020-10-02T23:48:52.076996974+02:00", + "name" : "100ANDRO", + "size" : 128, + "type" : "FILE_INFO_TYPE_DIRECTORY" + }, + { + "modTime" : "2020-10-09T13:04:42.4410738+02:00", + "name" : "Camera", + "size" : 128, + "type" : "FILE_INFO_TYPE_DIRECTORY", + "children" : [ + { + "modTime" : "2020-12-16T23:31:34.5009668+01:00", + "name" : "IMG_20201114_124821.jpg", + "size" : 10682189, + "type" : "FILE_INFO_TYPE_FILE" + }, + { + "modTime" : "2020-12-16T23:31:35.0106367+01:00", + "name" : "IMG_20201213_122451.jpg", + "size" : 7936351, + "type" : "FILE_INFO_TYPE_FILE" + }, + { + "modTime" : "2020-12-13T12:25:05.017097469+01:00", + "name" : "IMG_20201213_122504.jpg", + "size" : 8406507, + "type" : "FILE_INFO_TYPE_FILE" + }, + { + "modTime" : "2020-12-13T12:25:06.127097469+01:00", + "name" : "IMG_20201213_122505.jpg", + "size" : 8381931, + "type" : "FILE_INFO_TYPE_FILE" + }, + { + "modTime" : "2020-12-13T12:53:29.707298401+01:00", + "name" : "IMG_20201213_125329.jpg", + "size" : 4388331, + "type" : "FILE_INFO_TYPE_FILE" + } + ] + } +] diff --git a/syncthingmodel/CMakeLists.txt b/syncthingmodel/CMakeLists.txt index 4fdbe52..186712c 100644 --- a/syncthingmodel/CMakeLists.txt +++ b/syncthingmodel/CMakeLists.txt @@ -39,7 +39,8 @@ set(TS_FILES translations/${META_PROJECT_NAME}_zh_CN.ts translations/${META_PROJ translations/${META_PROJECT_NAME}_de_DE.ts translations/${META_PROJECT_NAME}_en_US.ts) set(QT_TESTS models) -set(QT_TEST_SRC_FILES_models syncthingicons.cpp syncthingmodel.cpp syncthingdirectorymodel.cpp syncthingdevicemodel.cpp) +set(QT_TEST_SRC_FILES_models syncthingicons.cpp syncthingmodel.cpp syncthingdirectorymodel.cpp syncthingdevicemodel.cpp + syncthingfilemodel.cpp) # find c++utilities find_package(${PACKAGE_NAMESPACE_PREFIX}c++utilities${CONFIGURATION_PACKAGE_SUFFIX} 5.0.0 REQUIRED) diff --git a/syncthingmodel/syncthingfilemodel.cpp b/syncthingmodel/syncthingfilemodel.cpp index 566b732..e9e6a26 100644 --- a/syncthingmodel/syncthingfilemodel.cpp +++ b/syncthingmodel/syncthingfilemodel.cpp @@ -13,15 +13,28 @@ using namespace CppUtilities; namespace Data { -SyncthingFileModel::SyncthingFileModel(SyncthingConnection &connection, const QString &dirId, QObject *parent) +SyncthingFileModel::SyncthingFileModel(SyncthingConnection &connection, const SyncthingDir &dir, QObject *parent) : SyncthingModel(connection, parent) , m_connection(connection) - , m_dirId(dirId) + , m_dirId(dir.id) + , m_root(std::make_unique()) { - m_connection.browse(m_dirId, QString(), 1, [this](std::vector &&items) { + m_root->name = dir.displayName(); + m_root->modificationTime = dir.lastFileTime; + m_root->size = dir.globalStats.bytes; + m_root->type = SyncthingItemType::Directory; + m_fetchQueue.append(QString()); + m_connection.browse(m_dirId, QString(), 1, [this](std::vector> &&items, QString &&errorMessage) { + Q_UNUSED(errorMessage) + + m_fetchQueue.removeAll(QString()); + if (items.empty()) { + return; + } const auto last = items.size() - 1; - beginInsertRows(QModelIndex(), 0, last < std::numeric_limits::max() ? static_cast(last) : std::numeric_limits::max()); - m_items = std::move(items); + beginInsertRows(index(0, 0), 0, last < std::numeric_limits::max() ? static_cast(last) : std::numeric_limits::max()); + m_root->children = std::move(items); + m_root->childrenPopulated = true; endInsertRows(); }); } @@ -46,25 +59,48 @@ QHash SyncthingFileModel::roleNames() const QModelIndex SyncthingFileModel::index(int row, int column, const QModelIndex &parent) const { - if (row < 0 || column < 0 || column > 2) { + if (row < 0 || column < 0 || column > 2 || parent.column() > 0) { return QModelIndex(); } - if (!parent.isValid()) { - if (static_cast(row) >= m_items.size()) { - return QModelIndex(); - } - return createIndex(row, column, &m_items[static_cast(row)]); + return static_cast(row) ? QModelIndex() : createIndex(row, column, m_root.get()); } - auto *const parentItem = reinterpret_cast(parent.internalPointer()); + if (!parentItem) { + return QModelIndex(); + } auto &items = parentItem->children; if (static_cast(row) >= items.size()) { return QModelIndex(); } auto &item = items[static_cast(row)]; - item.parent = parentItem; - return createIndex(row, column, &item); + item->parent = parentItem; + return createIndex(row, column, item.get()); +} + +QModelIndex SyncthingFileModel::index(const QString &path) const +{ + auto parts = path.split(QChar('/'), Qt::SkipEmptyParts); + auto *parent = m_root.get(); + auto res = createIndex(0, 0, parent); + for (const auto &part : parts) { + auto index = 0; + for (const auto &child : parent->children) { + if (child->name == part) { + child->parent = parent; + parent = child.get(); + res = createIndex(index, 0, parent); + index = -1; + break; + } + ++index; + } + if (index >= 0) { + res = QModelIndex(); + return res; + } + } + return res; } QString SyncthingFileModel::path(const QModelIndex &index) const @@ -77,6 +113,10 @@ QString SyncthingFileModel::path(const QModelIndex &index) const auto size = QString::size_type(); parts.reserve(reinterpret_cast(index.internalPointer())->level + 1); for (auto i = index; i.isValid(); i = i.parent()) { + const auto *const item = reinterpret_cast(i.internalPointer()); + if (item == m_root.get()) { + break; + } parts.append(reinterpret_cast(i.internalPointer())->name); size += parts.back().size(); } @@ -94,10 +134,10 @@ QModelIndex SyncthingFileModel::parent(const QModelIndex &child) const return QModelIndex(); } auto *const childItem = reinterpret_cast(child.internalPointer()); - if (!childItem->parent) { + if (!childItem) { return QModelIndex(); } - return createIndex(static_cast(childItem->index), 0, childItem->parent); + return !childItem->parent ? QModelIndex() : createIndex(static_cast(childItem->parent->index), 0, childItem->parent); } QVariant SyncthingFileModel::headerData(int section, Qt::Orientation orientation, int role) const @@ -192,7 +232,7 @@ int SyncthingFileModel::rowCount(const QModelIndex &parent) const { auto res = std::size_t(); if (!parent.isValid()) { - res = m_items.size(); + res = 1; } else { auto *const parentItem = reinterpret_cast(parent.internalPointer()); res = parentItem->childrenPopulated || parentItem->type != SyncthingItemType::Directory ? parentItem->children.size() : 1; @@ -216,11 +256,11 @@ bool SyncthingFileModel::canFetchMore(const QModelIndex &parent) const } /// \cond -static void addLevel(std::vector &items, int level) +static void addLevel(std::vector> &items, int level) { for (auto &item : items) { - item.level += level; - addLevel(item.children, level); + item->level += level; + addLevel(item->children, level); } } /// \endcond @@ -230,7 +270,7 @@ void SyncthingFileModel::fetchMore(const QModelIndex &parent) if (!parent.isValid()) { return; } - m_fetchQueue.append(parent); + m_fetchQueue.append(path(parent)); if (m_fetchQueue.size() == 1) { processFetchQueue(); } @@ -261,21 +301,36 @@ void SyncthingFileModel::processFetchQueue() if (m_fetchQueue.isEmpty()) { return; } - const auto &parent = m_fetchQueue.front(); - m_pendingRequest = m_connection.browse(m_dirId, path(parent), 1, [this, parent](std::vector &&items) { - auto *const parentItem = reinterpret_cast(parent.internalPointer()); - addLevel(items, parentItem->level); - beginRemoveRows(parent, 0, static_cast(parentItem->children.size() - 1)); - parentItem->children.clear(); - endRemoveRows(); - const auto last = items.size() - 1; - beginInsertRows(parent, 0, last < std::numeric_limits::max() ? static_cast(last) : std::numeric_limits::max()); - parentItem->children = std::move(items); - parentItem->childrenPopulated = true; - endInsertRows(); - m_fetchQueue.removeAll(parent); - processFetchQueue(); - }); + const auto &path = m_fetchQueue.front(); + m_pendingRequest = m_connection.browse( + m_dirId, path, 1, [this, p = path](std::vector> &&items, QString &&errorMessage) mutable { + Q_UNUSED(errorMessage) + + m_fetchQueue.removeAll(p); + const auto refreshedIndex = index(p); + if (!refreshedIndex.isValid()) { + processFetchQueue(); + return; + } + auto *const refreshedItem = reinterpret_cast(refreshedIndex.internalPointer()); + if (!refreshedItem->children.empty()) { + beginRemoveRows(refreshedIndex, 0, static_cast(refreshedItem->children.size() - 1)); + refreshedItem->children.clear(); + endRemoveRows(); + } + if (!items.empty()) { + const auto last = items.size() - 1; + addLevel(items, refreshedItem->level); + for (auto &item : items) { + item->parent = refreshedItem; + } + beginInsertRows(refreshedIndex, 0, last < std::numeric_limits::max() ? static_cast(last) : std::numeric_limits::max()); + refreshedItem->children = std::move(items); + refreshedItem->childrenPopulated = true; + endInsertRows(); + } + processFetchQueue(); + }); } } // namespace Data diff --git a/syncthingmodel/syncthingfilemodel.h b/syncthingmodel/syncthingfilemodel.h index d1931cf..4a4811a 100644 --- a/syncthingmodel/syncthingfilemodel.h +++ b/syncthingmodel/syncthingfilemodel.h @@ -3,23 +3,25 @@ #include "./syncthingmodel.h" +#include + +#include #include namespace Data { -struct SyncthingItem; - class LIB_SYNCTHING_MODEL_EXPORT SyncthingFileModel : public SyncthingModel { Q_OBJECT public: enum SyncthingFileModelRole { NameRole = SyncthingModelUserRole + 1, SizeRole, ModificationTimeRole, Actions, ActionNames, ActionIcons }; - explicit SyncthingFileModel(SyncthingConnection &connection, const QString &dirId, QObject *parent = nullptr); + explicit SyncthingFileModel(SyncthingConnection &connection, const SyncthingDir &dir, QObject *parent = nullptr); ~SyncthingFileModel() override; public Q_SLOTS: QHash roleNames() const override; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex index(const QString &path) const; QModelIndex parent(const QModelIndex &child) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; @@ -44,9 +46,9 @@ private: private: SyncthingConnection &m_connection; QString m_dirId; - QModelIndexList m_fetchQueue; + QStringList m_fetchQueue; QMetaObject::Connection m_pendingRequest; - mutable std::vector m_items; + std::unique_ptr m_root; }; } // namespace Data diff --git a/syncthingmodel/syncthingicons.cpp b/syncthingmodel/syncthingicons.cpp index 919c416..d7ce686 100644 --- a/syncthingmodel/syncthingicons.cpp +++ b/syncthingmodel/syncthingicons.cpp @@ -278,7 +278,7 @@ QString StatusIconSettings::toString() const StatusIcons::StatusIcons(const StatusIconSettings &settings) : disconnected( - QIcon(renderSvgImage(makeSyncthingIcon(settings.disconnectedColor, StatusEmblem::None, settings.strokeWidth), settings.renderSize))) + QIcon(renderSvgImage(makeSyncthingIcon(settings.disconnectedColor, StatusEmblem::None, settings.strokeWidth), settings.renderSize))) , idling(QIcon(renderSvgImage(makeSyncthingIcon(settings.idleColor, StatusEmblem::None, settings.strokeWidth), settings.renderSize))) , scanninig(QIcon(renderSvgImage(makeSyncthingIcon(settings.scanningColor, StatusEmblem::Scanning, settings.strokeWidth), settings.renderSize))) , notify(QIcon(renderSvgImage(makeSyncthingIcon(settings.warningColor, StatusEmblem::Alert, settings.strokeWidth), settings.renderSize))) diff --git a/syncthingmodel/tests/models.cpp b/syncthingmodel/tests/models.cpp index 55bcb53..8b74287 100644 --- a/syncthingmodel/tests/models.cpp +++ b/syncthingmodel/tests/models.cpp @@ -1,13 +1,14 @@ -#include "../syncthingdirectorymodel.h" #include "../syncthingdevicemodel.h" +#include "../syncthingdirectorymodel.h" +#include "../syncthingfilemodel.h" #include #include #include -#include #include +#include #include @@ -20,6 +21,7 @@ private Q_SLOTS: void testDirectoryModel(); void testDevicesModel(); + void testFileModel(); private: QTimer m_timeout; @@ -38,7 +40,7 @@ void ModelTests::initTestCase() m_timeout.start(); connect(&m_timeout, &QTimer::timeout, this, [this] { m_loop.quit(); - QFAIL("Timeout exceeded when loading mocked config/status for test"); + QFAIL("Timeout exceeded"); }); // request config and status and wait until available @@ -97,5 +99,91 @@ void ModelTests::testDevicesModel() QCOMPARE(model.index(1, 1, dev2Idx).data(), QStringLiteral("dynamic, tcp://192.168.1.3:22000")); } +void ModelTests::testFileModel() +{ + auto row = 0; + const auto *dirInfo = m_connection.findDirInfo(QStringLiteral("GXWxf-3zgnU"), row); + QVERIFY(dirInfo); + + // test behavior of empty/unpopulated model + auto model = Data::SyncthingFileModel(m_connection, *dirInfo); + QCOMPARE(model.rowCount(QModelIndex()), 1); + const auto rootIdx = QPersistentModelIndex(model.index(0, 0)); + QVERIFY(rootIdx.isValid()); + QVERIFY(!model.index(1, 0).isValid()); + QCOMPARE(model.rowCount(rootIdx), 1); + QCOMPARE(model.index(0, 0, rootIdx).data(), QVariant()); + QCOMPARE(model.index(1, 0, rootIdx).data(), QVariant()); + QVERIFY(model.canFetchMore(rootIdx)); + + // wait until the root has been updated + connect(&model, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) { + Q_UNUSED(first) + Q_UNUSED(last) + if (!parent.parent().isValid() && parent.row() == 0 && parent.column() == 0) { + m_timeout.stop(); + m_loop.quit(); + } + }); + m_timeout.start(); + m_loop.exec(); + + QVERIFY(rootIdx.isValid()); + QCOMPARE(model.rowCount(rootIdx), 2); + + // test access to nested folders + const auto androidIdx = QPersistentModelIndex(model.index(0, 0, rootIdx)); + const auto cameraIdx = QPersistentModelIndex(model.index(1, 0, rootIdx)); + const auto nestedIdx = QPersistentModelIndex(model.index(0, 0, cameraIdx)); + const auto initialAndroidPtr = androidIdx.constInternalPointer(); + const auto initialCameraPtr = cameraIdx.constInternalPointer(); + QVERIFY(androidIdx.isValid()); + QVERIFY(cameraIdx.isValid()); + QCOMPARE(androidIdx.parent(), rootIdx); + QCOMPARE(cameraIdx.parent(), rootIdx); + QCOMPARE(nestedIdx.parent(), cameraIdx); + QCOMPARE(model.rowCount(androidIdx), 0); + QCOMPARE(model.rowCount(cameraIdx), 5); + QCOMPARE(androidIdx.data(), QStringLiteral("100ANDRO")); + QCOMPARE(cameraIdx.data(), QStringLiteral("Camera")); + QCOMPARE(model.index(0, 0, cameraIdx).data(), QStringLiteral("IMG_20201114_124821.jpg")); + QCOMPARE(model.index(0, 1, cameraIdx).data(), QStringLiteral("10.19 MiB")); + QCOMPARE(model.index(0, 2, cameraIdx).data(), QStringLiteral("2020-12-16 22:31:34.500")); + QCOMPARE(model.index(1, 0, cameraIdx).data(), QStringLiteral("IMG_20201213_122451.jpg")); + QCOMPARE(model.index(2, 0, cameraIdx).data(), QStringLiteral("IMG_20201213_122504.jpg")); + QCOMPARE(model.index(3, 0, cameraIdx).data(), QStringLiteral("IMG_20201213_122505.jpg")); + QCOMPARE(model.index(4, 0, cameraIdx).data(), QStringLiteral("IMG_20201213_125329.jpg")); + QCOMPARE(model.index(5, 0, cameraIdx).data(), QVariant()); + QCOMPARE(model.index(5, 1, cameraIdx).data(), QVariant()); + QCOMPARE(model.index(5, 2, cameraIdx).data(), QVariant()); + QCOMPARE(model.index(5, 3, cameraIdx).data(), QVariant()); + + // test conversion of indexes to/from paths + const auto testPath = QStringLiteral("Camera/IMG_20201213_122504.jpg/"); + const auto testPathIdx = model.index(2, 0, cameraIdx); + QCOMPARE(model.path(testPathIdx), testPath); + QCOMPARE(model.index(testPath), testPathIdx); + + // re-load the data again and wait for the update + model.fetchMore(rootIdx); + m_timeout.start(); + m_loop.exec(); + + // verify that only the root index is still valid (all other indexes have been invalidated) + QVERIFY(rootIdx.isValid()); + QCOMPARE(model.rowCount(rootIdx), 2); + QVERIFY(androidIdx.constInternalPointer() != initialAndroidPtr); + QVERIFY(!androidIdx.isValid()); + QVERIFY(cameraIdx.constInternalPointer() != initialCameraPtr); + QVERIFY(!cameraIdx.isValid()); + QVERIFY(!nestedIdx.isValid()); + + // verify that data was re-loaded + const auto androidIdx2 = QPersistentModelIndex(model.index(0, 0, rootIdx)); + const auto cameraIdx2 = QPersistentModelIndex(model.index(1, 0, rootIdx)); + QCOMPARE(androidIdx2.data(), QStringLiteral("100ANDRO")); + QCOMPARE(cameraIdx2.data(), QStringLiteral("Camera")); +} + QTEST_MAIN(ModelTests) #include "models.moc" diff --git a/syncthingwidgets/misc/otherdialogs.cpp b/syncthingwidgets/misc/otherdialogs.cpp index d167ab2..1a0a8f1 100644 --- a/syncthingwidgets/misc/otherdialogs.cpp +++ b/syncthingwidgets/misc/otherdialogs.cpp @@ -88,7 +88,7 @@ QDialog *browseRemoteFilesDialog(Data::SyncthingConnection &connection, const Da dlg->setAttribute(Qt::WA_DeleteOnClose); // setup model/view - auto model = new Data::SyncthingFileModel(connection, dir.id, &connection); + auto model = new Data::SyncthingFileModel(connection, dir, &connection); auto view = new QTreeView(dlg); view->setModel(model); view->setContextMenuPolicy(Qt::CustomContextMenu); diff --git a/testhelper/helper.h b/testhelper/helper.h index 0427fb2..2b604de 100644 --- a/testhelper/helper.h +++ b/testhelper/helper.h @@ -178,7 +178,8 @@ public: #endif } // register own handler to detect whether signal has been emitted - m_emittedConnection = QObject::connect(sender, signal, sender, [this] { m_signalEmitted = true; }, Qt::DirectConnection); + m_emittedConnection = QObject::connect( + sender, signal, sender, [this] { m_signalEmitted = true; }, Qt::DirectConnection); #ifndef SYNCTHINGTESTHELPER_FOR_CLI if (!m_emittedConnection) { CPPUNIT_FAIL(argsToString("Unable to connect signal ", signalName().data(), " to check for signal emmitation"));