diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b76c7aa..3d91245 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -29,6 +29,7 @@ qt_add_library(quickshell-core STATIC model.cpp elapsedtimer.cpp desktopentry.cpp + objectrepeater.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/module.md b/src/core/module.md index 315eb25..73ede34 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -21,5 +21,6 @@ headers = [ "model.hpp", "elapsedtimer.hpp", "desktopentry.hpp", + "objectrepeater.hpp", ] ----- diff --git a/src/core/objectrepeater.cpp b/src/core/objectrepeater.cpp new file mode 100644 index 0000000..014a5e2 --- /dev/null +++ b/src/core/objectrepeater.cpp @@ -0,0 +1,191 @@ +#include "objectrepeater.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +QVariant ObjectRepeater::model() const { return this->mModel; } + +void ObjectRepeater::setModel(QVariant model) { + if (model == this->mModel) return; + + if (this->itemModel != nullptr) { + QObject::disconnect(this->itemModel, nullptr, this, nullptr); + } + + this->mModel = std::move(model); + emit this->modelChanged(); + this->reloadElements(); +} + +void ObjectRepeater::onModelDestroyed() { + this->mModel.clear(); + this->itemModel = nullptr; + emit this->modelChanged(); + this->reloadElements(); +} + +QQmlComponent* ObjectRepeater::delegate() const { return this->mDelegate; } + +void ObjectRepeater::setDelegate(QQmlComponent* delegate) { + if (delegate == this->mDelegate) return; + + if (this->mDelegate != nullptr) { + QObject::disconnect(this->mDelegate, nullptr, this, nullptr); + } + + this->mDelegate = delegate; + + if (delegate != nullptr) { + QObject::connect( + this->mDelegate, + &QObject::destroyed, + this, + &ObjectRepeater::onDelegateDestroyed + ); + } + + emit this->delegateChanged(); + this->reloadElements(); +} + +void ObjectRepeater::onDelegateDestroyed() { + this->mDelegate = nullptr; + emit this->delegateChanged(); + this->reloadElements(); +} + +void ObjectRepeater::reloadElements() { + for (auto i = this->valuesList.length() - 1; i >= 0; i--) { + this->removeComponent(i); + } + + if (this->mDelegate == nullptr || !this->mModel.isValid()) return; + + if (this->mModel.canConvert()) { + auto* model = this->mModel.value(); + this->itemModel = model; + + this->insertModelElements(model, 0, model->rowCount() - 1); // -1 is fine + + // clang-format off + QObject::connect(model, &QObject::destroyed, this, &ObjectRepeater::onModelDestroyed); + QObject::connect(model, &QAbstractItemModel::rowsInserted, this, &ObjectRepeater::onModelRowsInserted); + QObject::connect(model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &ObjectRepeater::onModelRowsAboutToBeRemoved); + QObject::connect(model, &QAbstractItemModel::rowsMoved, this, &ObjectRepeater::onModelRowsMoved); + QObject::connect(model, &QAbstractItemModel::modelAboutToBeReset, this, &ObjectRepeater::onModelAboutToBeReset); + // clang-format on + } else if (this->mModel.canConvert()) { + auto values = this->mModel.value(); + auto len = values.count(); + + for (auto i = 0; i != len; i++) { + this->insertComponent(i, {{"modelData", QVariant::fromValue(values.at(i))}}); + } + } else if (this->mModel.canConvert>()) { + auto values = this->mModel.value>(); + + for (auto& value: values) { + this->insertComponent(this->valuesList.length(), {{"modelData", value}}); + } + } else { + qCritical() << this + << "Cannot create components as the model is not compatible:" << this->mModel; + } +} + +void ObjectRepeater::insertModelElements(QAbstractItemModel* model, int first, int last) { + auto roles = model->roleNames(); + auto roleDataVec = QVector(); + for (auto id: roles.keys()) { + roleDataVec.push_back(QModelRoleData(id)); + } + + auto values = QModelRoleDataSpan(roleDataVec); + auto props = QVariantMap(); + + for (auto i = first; i != last + 1; i++) { + auto index = model->index(i, 0); + model->multiData(index, values); + + for (auto [id, name]: roles.asKeyValueRange()) { + props.insert(name, *values.dataForRole(id)); + } + + this->insertComponent(i, props); + + props.clear(); + } +} + +void ObjectRepeater::onModelRowsInserted(const QModelIndex& parent, int first, int last) { + if (parent != QModelIndex()) return; + + this->insertModelElements(this->itemModel, first, last); +} + +void ObjectRepeater::onModelRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last) { + if (parent != QModelIndex()) return; + + for (auto i = last; i != first - 1; i--) { + this->removeComponent(i); + } +} + +void ObjectRepeater::onModelRowsMoved( + const QModelIndex& sourceParent, + int sourceStart, + int sourceEnd, + const QModelIndex& destParent, + int destStart +) { + auto hasSource = sourceParent != QModelIndex(); + auto hasDest = destParent != QModelIndex(); + + if (!hasSource && !hasDest) return; + + if (hasSource) { + this->onModelRowsAboutToBeRemoved(sourceParent, sourceStart, sourceEnd); + } + + if (hasDest) { + this->onModelRowsInserted(destParent, destStart, destStart + (sourceEnd - sourceStart)); + } +} + +void ObjectRepeater::onModelAboutToBeReset() { + auto last = static_cast(this->valuesList.length() - 1); + this->onModelRowsAboutToBeRemoved(QModelIndex(), 0, last); // -1 is fine +} + +void ObjectRepeater::insertComponent(qsizetype index, const QVariantMap& properties) { + auto* context = QQmlEngine::contextForObject(this); + auto* instance = this->mDelegate->createWithInitialProperties(properties, context); + + if (instance == nullptr) { + qWarning().noquote() << this->mDelegate->errorString(); + qWarning() << this << "failed to create object for model data" << properties; + } else { + QQmlEngine::setObjectOwnership(instance, QQmlEngine::CppOwnership); + instance->setParent(this); + } + + this->insertObject(instance, index); +} + +void ObjectRepeater::removeComponent(qsizetype index) { + auto* instance = this->valuesList.at(index); + delete instance; + + this->removeAt(index); +} diff --git a/src/core/objectrepeater.hpp b/src/core/objectrepeater.hpp new file mode 100644 index 0000000..891666d --- /dev/null +++ b/src/core/objectrepeater.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "model.hpp" + +///! A Repeater / for loop / map for non Item derived objects. +/// The ObjectRepeater creates instances of the provided delegate for every entry in the +/// given model, similarly to a [Repeater] but for non visual types. +/// +/// [Repeater]: https://doc.qt.io/qt-6/qml-qtquick-repeater.html +class ObjectRepeater: public ObjectModel { + Q_OBJECT; + /// The model providing data to the ObjectRepeater. + /// + /// Currently accepted model types are QML `list` lists, javascript arrays, + /// and [QAbstractListModel] derived models, though only one column will be repeated + /// from the latter. + /// + /// Note: [ObjectModel] is a [QAbstractListModel] with a single column. + /// + /// [QAbstractListModel]: https://doc.qt.io/qt-6/qabstractlistmodel.html + /// [ObjectModel]: ../objectmodel + Q_PROPERTY(QVariant model READ model WRITE setModel NOTIFY modelChanged); + /// The delegate component to repeat. + /// + /// The delegate is given the same properties as in a Repeater, except `index` which + /// is not currently implemented. + /// + /// If the model is a `list` or javascript array, a `modelData` property will be + /// exposed containing the entry from the model. If the model is a [QAbstractListModel], + /// the roles from the model will be exposed. + /// + /// Note: [ObjectModel] has a single role named `modelData` for compatibility with normal lists. + /// + /// [QAbstractListModel]: https://doc.qt.io/qt-6/qabstractlistmodel.html + /// [ObjectModel]: ../objectmodel + Q_PROPERTY(QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY delegateChanged); + Q_CLASSINFO("DefaultProperty", "delegate"); + QML_ELEMENT; + +public: + explicit ObjectRepeater(QObject* parent = nullptr): ObjectModel(parent) {} + + [[nodiscard]] QVariant model() const; + void setModel(QVariant model); + + [[nodiscard]] QQmlComponent* delegate() const; + void setDelegate(QQmlComponent* delegate); + +signals: + void modelChanged(); + void delegateChanged(); + +private slots: + void onDelegateDestroyed(); + void onModelDestroyed(); + void onModelRowsInserted(const QModelIndex& parent, int first, int last); + void onModelRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last); + + void onModelRowsMoved( + const QModelIndex& sourceParent, + int sourceStart, + int sourceEnd, + const QModelIndex& destParent, + int destStart + ); + + void onModelAboutToBeReset(); + +private: + void reloadElements(); + void insertModelElements(QAbstractItemModel* model, int first, int last); + void insertComponent(qsizetype index, const QVariantMap& properties); + void removeComponent(qsizetype index); + + QVariant mModel; + QAbstractItemModel* itemModel = nullptr; + QQmlComponent* mDelegate = nullptr; +};