#pragma once #include #include #include #include #include #include #include "Resource.h" #include "tasks/Task.h" class QSortFilterProxyModel; /** A basic model for external resources. * * This model manages a list of resources. As such, external users of such resources do not own them, * and the resource's lifetime is contingent on the model's lifetime. * * TODO: Make the resources unique pointers accessible through weak pointers. */ class ResourceFolderModel : public QAbstractListModel { Q_OBJECT public: ResourceFolderModel(QDir, QObject* parent = nullptr); ~ResourceFolderModel() override; /** Starts watching the paths for changes. * * Returns whether starting to watch all the paths was successful. * If one or more fails, it returns false. */ bool startWatching(const QStringList paths); /** Stops watching the paths for changes. * * Returns whether stopping to watch all the paths was successful. * If one or more fails, it returns false. */ bool stopWatching(const QStringList paths); /* Helper methods for subclasses, using a predetermined list of paths. */ virtual bool startWatching() { return startWatching({ m_dir.absolutePath() }); }; virtual bool stopWatching() { return stopWatching({ m_dir.absolutePath() }); }; /** Given a path in the system, install that resource, moving it to its place in the * instance file hierarchy. * * Returns whether the installation was succcessful. */ virtual bool installResource(QString path); /** Uninstall (i.e. remove all data about it) a resource, given its file name. * * Returns whether the removal was successful. */ virtual bool uninstallResource(QString file_name); virtual bool deleteResources(const QModelIndexList&); /** Applies the given 'action' to the resources in 'indexes'. * * Returns whether the action was successfully applied to all resources. */ virtual bool setResourceEnabled(const QModelIndexList& indexes, EnableAction action); /** Creates a new update task and start it. Returns false if no update was done, like when an update is already underway. */ virtual bool update(); /** Creates a new parse task, if needed, for 'res' and start it.*/ virtual void resolveResource(Resource* res); [[nodiscard]] size_t size() const { return m_resources.size(); }; [[nodiscard]] bool empty() const { return size() == 0; } [[nodiscard]] Resource& at(int index) { return *m_resources.at(index); } [[nodiscard]] Resource const& at(int index) const { return *m_resources.at(index); } [[nodiscard]] QList const& all() const { return m_resources; } [[nodiscard]] QDir const& dir() const { return m_dir; } /** Checks whether there's any parse tasks being done. * * Since they can be quite expensive, and are usually done in a separate thread, if we were to destroy the model while having * such tasks would introduce an undefined behavior, most likely resulting in a crash. */ [[nodiscard]] bool hasPendingParseTasks() const; /* Qt behavior */ /* Basic columns */ enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, NUM_COLUMNS }; [[nodiscard]] int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast(size()); } [[nodiscard]] int columnCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : NUM_COLUMNS; }; [[nodiscard]] Qt::DropActions supportedDropActions() const override; /// flags, mostly to support drag&drop [[nodiscard]] Qt::ItemFlags flags(const QModelIndex& index) const override; [[nodiscard]] QStringList mimeTypes() const override; bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; [[nodiscard]] bool validateIndex(const QModelIndex& index) const; [[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; /** This creates a proxy model to filter / sort the model for a UI. * * The actual comparisons and filtering are done directly by the Resource, so to modify behavior go there instead! */ QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr); [[nodiscard]] SortType columnToSortKey(size_t column) const; class ProxyModel : public QSortFilterProxyModel { public: explicit ProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {} protected: [[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; [[nodiscard]] bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override; }; public slots: void enableInteraction(bool enabled); void disableInteraction(bool disabled) { enableInteraction(!disabled); } signals: void updateFinished(); protected: /** This creates a new update task to be executed by update(). * * The task should load and parse all resources necessary, and provide a way of accessing such results. * * This Task is normally executed when opening a page, so it shouldn't contain much heavy work. * If such work is needed, try using it in the Task create by createParseTask() instead! */ [[nodiscard]] virtual Task* createUpdateTask(); /** This creates a new parse task to be executed by onUpdateSucceeded(). * * This task should load and parse all heavy info needed by a resource, such as parsing a manifest. It gets executed * in the background, so it slowly updates the UI as tasks get done. */ [[nodiscard]] virtual Task* createParseTask(Resource&) { return nullptr; }; /** Standard implementation of the model update logic. * * It uses set operations to find differences between the current state and the updated state, * to act only on those disparities. * * The implementation is at the end of this header. */ template void applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources); protected slots: void directoryChanged(QString); /** Called when the update task is successful. * * This usually calls static_cast on the specific Task type returned by createUpdateTask, * so care must be taken in such cases. * TODO: Figure out a way to express this relationship better without templated classes (Q_OBJECT macro disallows that). */ virtual void onUpdateSucceeded(); virtual void onUpdateFailed() {} /** Called when the parse task with the given ticket is successful. * * This is just a simple reference implementation. You probably want to override it with your own logic in a subclass * if the resource is complex and has more stuff to parse. */ virtual void onParseSucceeded(int ticket, QString resource_id); virtual void onParseFailed([[maybe_unused]] int ticket, [[maybe_unused]] QString resource_id) {} protected: // Represents the relationship between a column's index (represented by the list index), and it's sorting key. // As such, the order in with they appear is very important! QList m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE }; bool m_can_interact = true; QDir m_dir; QFileSystemWatcher m_watcher; bool m_is_watching = false; Task::Ptr m_current_update_task = nullptr; bool m_scheduled_update = false; QList m_resources; // Represents the relationship between a resource's internal ID and it's row position on the model. QMap m_resources_index; QMap m_active_parse_tasks; std::atomic m_next_resolution_ticket = 0; }; /* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */ #define RESOURCE_HELPERS(T) \ [[nodiscard]] T* operator[](size_t index) \ { \ return static_cast(m_resources[index].get()); \ } \ [[nodiscard]] T* at(size_t index) \ { \ return static_cast(m_resources[index].get()); \ } \ [[nodiscard]] const T* at(size_t index) const \ { \ return static_cast(m_resources.at(index).get()); \ } \ [[nodiscard]] T* first() \ { \ return static_cast(m_resources.first().get()); \ } \ [[nodiscard]] T* last() \ { \ return static_cast(m_resources.last().get()); \ } \ [[nodiscard]] T* find(QString id) \ { \ auto iter = std::find_if(m_resources.constBegin(), m_resources.constEnd(), \ [&](Resource::Ptr const& r) { return r->internal_id() == id; }); \ if (iter == m_resources.constEnd()) \ return nullptr; \ return static_cast((*iter).get()); \ } /* Template definition to avoid some code duplication */ template void ResourceFolderModel::applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources) { // see if the kept resources changed in some way { QSet kept_set = current_set; kept_set.intersect(new_set); for (auto const& kept : kept_set) { auto row_it = m_resources_index.constFind(kept); Q_ASSERT(row_it != m_resources_index.constEnd()); auto row = row_it.value(); auto& new_resource = new_resources[kept]; auto const& current_resource = m_resources.at(row); if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) { // no significant change, ignore... continue; } // If the resource is resolving, but something about it changed, we don't want to // continue the resolving. if (current_resource->isResolving()) { auto ticket = current_resource->resolutionTicket(); if (m_active_parse_tasks.contains(ticket)) { auto task = (*m_active_parse_tasks.find(ticket)).get(); task->abort(); } } m_resources[row].reset(new_resource); resolveResource(m_resources.at(row).get()); emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); } } // remove resources no longer present { QSet removed_set = current_set; removed_set.subtract(new_set); QList removed_rows; for (auto& removed : removed_set) removed_rows.append(m_resources_index[removed]); std::sort(removed_rows.begin(), removed_rows.end(), std::greater()); for (auto& removed_index : removed_rows) { auto removed_it = m_resources.begin() + removed_index; Q_ASSERT(removed_it != m_resources.end()); Q_ASSERT(removed_set.contains(removed_it->get()->internal_id())); if ((*removed_it)->isResolving()) { auto ticket = (*removed_it)->resolutionTicket(); if (m_active_parse_tasks.contains(ticket)) { auto task = (*m_active_parse_tasks.find(ticket)).get(); task->abort(); } } beginRemoveRows(QModelIndex(), removed_index, removed_index); m_resources.erase(removed_it); endRemoveRows(); } } // add new resources to the end { QSet added_set = new_set; added_set.subtract(current_set); // When you have a Qt build with assertions turned on, proceeding here will abort the application if (added_set.size() > 0) { beginInsertRows(QModelIndex(), m_resources.size(), m_resources.size() + added_set.size() - 1); for (auto& added : added_set) { auto res = new_resources[added]; m_resources.append(res); resolveResource(m_resources.last().get()); } endInsertRows(); } } // update index { m_resources_index.clear(); int idx = 0; for (auto const& mod : qAsConst(m_resources)) { m_resources_index[mod->internal_id()] = idx; idx++; } } }