core/objectrepeater: add ObjectRepeater
This commit is contained in:
parent
d8fa9e7bb3
commit
09d8a7a07d
|
@ -29,6 +29,7 @@ qt_add_library(quickshell-core STATIC
|
||||||
model.cpp
|
model.cpp
|
||||||
elapsedtimer.cpp
|
elapsedtimer.cpp
|
||||||
desktopentry.cpp
|
desktopentry.cpp
|
||||||
|
objectrepeater.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
|
set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}")
|
||||||
|
|
|
@ -21,5 +21,6 @@ headers = [
|
||||||
"model.hpp",
|
"model.hpp",
|
||||||
"elapsedtimer.hpp",
|
"elapsedtimer.hpp",
|
||||||
"desktopentry.hpp",
|
"desktopentry.hpp",
|
||||||
|
"objectrepeater.hpp",
|
||||||
]
|
]
|
||||||
-----
|
-----
|
||||||
|
|
191
src/core/objectrepeater.cpp
Normal file
191
src/core/objectrepeater.cpp
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
#include "objectrepeater.hpp"
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <qabstractitemmodel.h>
|
||||||
|
#include <qcontainerfwd.h>
|
||||||
|
#include <qhash.h>
|
||||||
|
#include <qlogging.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlcomponent.h>
|
||||||
|
#include <qqmlcontext.h>
|
||||||
|
#include <qqmlengine.h>
|
||||||
|
#include <qqmllist.h>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
#include <qtypes.h>
|
||||||
|
#include <qvariant.h>
|
||||||
|
|
||||||
|
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<QAbstractItemModel*>()) {
|
||||||
|
auto* model = this->mModel.value<QAbstractItemModel*>();
|
||||||
|
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<QQmlListReference>()) {
|
||||||
|
auto values = this->mModel.value<QQmlListReference>();
|
||||||
|
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<QVector<QVariant>>()) {
|
||||||
|
auto values = this->mModel.value<QVector<QVariant>>();
|
||||||
|
|
||||||
|
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<QModelRoleData>();
|
||||||
|
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<int>(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);
|
||||||
|
}
|
86
src/core/objectrepeater.hpp
Normal file
86
src/core/objectrepeater.hpp
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qabstractitemmodel.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlcomponent.h>
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
#include <qtmetamacros.h>
|
||||||
|
#include <qtypes.h>
|
||||||
|
#include <qvariant.h>
|
||||||
|
|
||||||
|
#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<QObject> {
|
||||||
|
Q_OBJECT;
|
||||||
|
/// The model providing data to the ObjectRepeater.
|
||||||
|
///
|
||||||
|
/// Currently accepted model types are QML `list<T>` 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<T>` 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;
|
||||||
|
};
|
Loading…
Reference in a new issue