diff --git a/storybook/pagesmodel.cpp b/storybook/pagesmodel.cpp index ea7f902d55..9476a2632e 100644 --- a/storybook/pagesmodel.cpp +++ b/storybook/pagesmodel.cpp @@ -1,43 +1,148 @@ #include "pagesmodel.h" -#include #include +#include +#include namespace { const auto categoryUncategorized QStringLiteral("Uncategorized"); } PagesModel::PagesModel(const QString &path, QObject *parent) - : QAbstractListModel{parent} + : QAbstractListModel{parent}, m_path{path}, + fsWatcher(new QFileSystemWatcher(this)) { - QDir dir(path); - dir.setFilter(QDir::Files); + m_items = load(); + readMetadata(m_items); + fsWatcher->addPath(path); + + connect(fsWatcher, &QFileSystemWatcher::directoryChanged, + this, &PagesModel::reload); +} + +QList PagesModel::load() { static QRegularExpression fileNameRegex( QRegularExpression::anchoredPattern("(.*)Page\\.qml")); - static QRegularExpression categoryRegex( - "^//(\\s)*category:(.+)$", QRegularExpression::MultilineOption); + + QDir dir(m_path); + dir.setFilter(QDir::Files); const QFileInfoList files = dir.entryInfoList(); + QList items; - std::for_each(files.begin(), files.end(), [this] (auto &fileInfo) { + std::for_each(files.begin(), files.end(), [this, &items] (auto &fileInfo) { QString fileName = fileInfo.fileName(); QRegularExpressionMatch fileNameMatch = fileNameRegex.match(fileName); if (!fileNameMatch.hasMatch()) return; - QFile file(fileInfo.filePath()); - file.open(QIODevice::ReadOnly); - QByteArray content = file.readAll(); + PagesModelItem item; + item.path = fileInfo.filePath(); + item.title = fileNameMatch.captured(1); + item.lastModified = fileInfo.lastModified(); - QRegularExpressionMatch categoryMatch = categoryRegex.match(content); - QString category = categoryMatch.hasMatch() - ? categoryMatch.captured(2).trimmed() : categoryUncategorized; - - QString title = fileNameMatch.captured(1); - m_items << PagesModelItem { title, category }; + items << item; }); + + return items; +} + +void PagesModel::readMetadata(PagesModelItem& item) { + static QRegularExpression categoryRegex( + "^//(\\s)*category:(.+)$", QRegularExpression::MultilineOption); + + QFile file(item.path); + file.open(QIODevice::ReadOnly); + QByteArray content = file.readAll(); + + QRegularExpressionMatch categoryMatch = categoryRegex.match(content); + QString category = categoryMatch.hasMatch() + ? categoryMatch.captured(2).trimmed() : categoryUncategorized; + + item.category = category; +} + +void PagesModel::readMetadata(QList &items) { + std::for_each(items.begin(), items.end(), [](auto&item) { + readMetadata(item); + }); +} + +void PagesModel::reload() { + QList currentItems = load(); + std::map mapping; + + for (const PagesModelItem &item : qAsConst(m_items)) + mapping[item.title] = item; + + std::vector newItems; + std::vector changedItems; + std::vector removedItems; + + for (const PagesModelItem &item : qAsConst(currentItems)) { + auto it = mapping.find(item.title); + + if (it == mapping.end()) { + newItems.push_back(item); + } else { + if (item.lastModified != it->second.lastModified) + changedItems.push_back(item); + + mapping.erase(it); + } + } + + for (auto& [key, value] : mapping) + removedItems.push_back(value); + + for (auto& item : removedItems) { + + auto it = std::find_if(m_items.begin(), m_items.end(), [&item](auto& it){ + return it.title == item.title; + }); + + auto index = std::distance(m_items.begin(), it); + + beginRemoveRows(QModelIndex{}, index, index); + m_items.removeAt(index); + endRemoveRows(); + } + + if (newItems.size()) { + beginInsertRows(QModelIndex{}, rowCount(), rowCount() + newItems.size() - 1); + + for (auto& item : newItems) { + readMetadata(item); + m_items << item; + } + + endInsertRows(); + } + + for (auto& item : changedItems) { + auto it = std::find_if(m_items.begin(), m_items.end(), [&item](auto& it){ + return it.title == item.title; + }); + + auto index = std::distance(m_items.begin(), it); + const auto& previous = *it; + readMetadata(item); + + if (previous.category != item.category) { + // For simplicity category change is handled by removing and + // adding item. In the future it can be changed to regular dataChanged + // event and handled properly in upstream models like SectionSDecoratorModel. + beginRemoveRows(QModelIndex{}, index, index); + m_items.removeAt(index); + endRemoveRows(); + + beginInsertRows(QModelIndex{}, rowCount(), rowCount()); + m_items << item; + endInsertRows(); + } + } } QHash PagesModel::roleNames() const diff --git a/storybook/pagesmodel.h b/storybook/pagesmodel.h index 5defc708e6..80cc3eb79e 100644 --- a/storybook/pagesmodel.h +++ b/storybook/pagesmodel.h @@ -1,8 +1,13 @@ #pragma once #include +#include + +class QFileSystemWatcher; struct PagesModelItem { + QString path; + QDateTime lastModified; QString title; QString category; }; @@ -22,6 +27,14 @@ public: int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; + void reload(); private: + QList load(); + + static void readMetadata(PagesModelItem &item); + static void readMetadata(QList &items); + + QString m_path; QList m_items; + QFileSystemWatcher* fsWatcher; };