Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
qt_list_model_adapter.hpp
Go to the documentation of this file.
1#pragma once
2
3// ObservableListModel<T> — Qt6 model adapter for any aria list source.
4//
5// Bridges the four observable list types onto `QAbstractListModel`:
6//
7// * `aria::ObservableList<T>`
8// * `aria::FilteredList<T>`
9// * `aria::SortedList<T>`
10// * `aria::MappedList<S, T>` (element type T = `Target`)
11//
12// All four expose the same `ListChange<T>` vocabulary and the same
13// read surface, so this adapter consumes them via the `aria::ListSource`
14// concept rather than through inheritance. The `Move` event is bridged
15// to Qt's `beginMoveRows` / `endMoveRows` so business code never has
16// to redraw a derived list on its own.
17//
18// Usage — with a domain ObservableList:
19//
20// ObservableListModel<Movie> model{vm.movies, roles, role_fn};
21//
22// Usage — with a derived (Filtered/Sorted/Mapped) list (recommended
23// pattern: keep the derived list owned by the ViewModel, hand a
24// reference to the model adapter):
25//
26// auto active = std::make_shared<aria::FilteredList<Movie>>(
27// vm.movies_shared(), [](const Movie& m){ return m.year >= 2000; });
28// ObservableListModel<Movie> model{*active, roles, role_fn};
29//
30// Construction snapshots and observes the source on its graph thread.
31// Subsequent owning events are queued to the model's Qt owner thread;
32// the model never reads the source while replaying them.
33
35#include "aria/list_source.hpp"
37#include "aria/subscription.hpp"
38
39#include <QAbstractListModel>
40#include <QByteArray>
41#include <QHash>
42#include <QMetaObject>
43#include <QModelIndex>
44#include <QPointer>
45#include <QThread>
46#include <QVariant>
47
48#include <deque>
49#include <functional>
50#include <limits>
51#include <mutex>
52#include <stdexcept>
53#include <memory>
54#include <type_traits>
55#include <utility>
56#include <vector>
57
58namespace aria::adapters::qt6 {
59
60template<typename T>
61class ObservableListModel : public QAbstractListModel {
62public:
63 using RoleMap = QHash<int, QByteArray>;
64 using RoleFn = std::function<QVariant(const T&, int role)>;
65
69 template<class L>
70 requires ::aria::ListSourceOf<L, T>
72 RoleMap roles,
73 RoleFn role_fn,
74 QObject* parent = nullptr)
75 : QAbstractListModel(parent),
76 roles_(std::move(roles)),
77 role_fn_(std::make_shared<RoleFn>(std::move(role_fn))),
78 snapshot_(source.snapshot()) {
79 check_size_(snapshot_.size());
80 delivery_->model = this;
81 std::weak_ptr<Delivery> weak = delivery_;
82 sub_ = source.observe([weak](const ::aria::ListChange<T>& change) {
83 if (auto delivery = weak.lock()) enqueue_(delivery, change);
84 });
85 }
86
88 Q_ASSERT(QThread::currentThread() == thread());
89 std::deque<::aria::ListChange<T>> retired;
90 {
91 std::lock_guard lock(delivery_->mutex);
92 delivery_->model = nullptr;
93 retired.swap(delivery_->pending);
94 }
95 // Both subscriptions and payloads can release application objects.
96 // Their destructors must run after the lifetime lock is released.
97 sub_.release();
98 }
99
100 int rowCount(const QModelIndex& parent = QModelIndex{}) const override {
101 if (parent.isValid()) return 0;
102 return static_cast<int>(snapshot_.size());
103 }
104
105 QVariant data(const QModelIndex& index,
106 int role = Qt::DisplayRole) const override {
107 if (!index.isValid() || index.model() != this || index.column() != 0) return {};
108 const auto row = index.row();
109 if (row < 0 || row >= rowCount()) return {};
110 auto item = snapshot_[static_cast<std::size_t>(row)];
111 if (!item) return {};
112 try {
113 // A role callback can release the model itself. Keep its target
114 // and the item alive independently until invocation returns.
115 auto project = role_fn_;
116 return (*project)(*item, role);
117 } catch (...) {
118 ::aria::report_callback_failure("qt.list_model.role", std::current_exception());
119 return {};
120 }
121 }
122
123 QHash<int, QByteArray> roleNames() const override {
124 return roles_;
125 }
126
129 void reload() {
130 Q_ASSERT(QThread::currentThread() == thread());
131 QPointer<ObservableListModel> alive(this);
132 beginResetModel();
133 if (alive) endResetModel();
134 }
135
136private:
137 struct Delivery {
138 std::mutex mutex;
139 ObservableListModel* model = nullptr;
140 std::deque<::aria::ListChange<T>> pending;
141 bool scheduled = false;
142 };
143
144 static void check_size_(std::size_t size) {
145 if (size > static_cast<std::size_t>(std::numeric_limits<int>::max()))
146 throw std::length_error("ObservableListModel: row count exceeds Qt's int range");
147 }
148
149 static void enqueue_(const std::shared_ptr<Delivery>& delivery,
150 const ::aria::ListChange<T>& change) {
151 std::unique_lock lock(delivery->mutex);
152 auto* model = delivery->model;
153 if (!model) return;
154 delivery->pending.push_back(change);
155 if (delivery->scheduled) return;
156 delivery->scheduled = true;
157 if (QThread::currentThread() == model->thread()) {
158 lock.unlock();
159 drain_(delivery);
160 } else {
161 // Lifetime lock prevents QObject destruction while registering
162 // the delivery. Qt drops the functor if its context dies later.
163 QMetaObject::invokeMethod(model, [weak = std::weak_ptr<Delivery>(delivery)] {
164 if (auto current = weak.lock()) drain_(current);
165 }, Qt::QueuedConnection);
166 }
167 }
168
169 static void drain_(const std::shared_ptr<Delivery>& delivery) noexcept {
170 for (;;) {
171 ::aria::ListChange<T> change{};
172 ObservableListModel* model;
173 {
174 std::lock_guard lock(delivery->mutex);
175 model = delivery->model;
176 if (!model || delivery->pending.empty()) {
177 delivery->scheduled = false;
178 return;
179 }
180 change = std::move(delivery->pending.front());
181 delivery->pending.pop_front();
182 }
183 // Queuing also serialises reentrant model notifications, so a
184 // second begin/end pair never interrupts the current change.
185 try { model->apply_change_(change); }
186 catch (...) {
187 ::aria::report_callback_failure("qt.list_model.change", std::current_exception());
188 }
189 }
190 }
191
192 void apply_change_(const ::aria::ListChange<T>& ch) {
193 Q_ASSERT(QThread::currentThread() == thread());
194 QPointer<ObservableListModel> alive(this);
195 switch (ch.kind) {
196 case ::aria::ListChangeKind::Insert: {
197 if (ch.index > snapshot_.size())
198 throw std::out_of_range("ObservableListModel: invalid insert index");
199 check_size_(snapshot_.size() + 1);
200 // Reserve before beginInsertRows: allocation failure must not
201 // leave Qt inside an unmatched structural notification pair.
202 if (snapshot_.size() == snapshot_.capacity()) {
203 const auto capacity = snapshot_.capacity();
204 constexpr auto maximum = static_cast<std::size_t>(std::numeric_limits<int>::max());
205 snapshot_.reserve(capacity > maximum / 2
206 ? maximum : (capacity == 0 ? 1 : capacity * 2));
207 }
208 auto row = static_cast<int>(ch.index);
209 beginInsertRows(QModelIndex{}, row, row);
210 if (!alive) return;
211 snapshot_.insert(snapshot_.begin() + static_cast<std::ptrdiff_t>(ch.index),
212 ch.item);
213 endInsertRows();
214 break;
215 }
216 case ::aria::ListChangeKind::Remove: {
217 auto row = static_cast<int>(ch.index);
218 if (ch.index >= snapshot_.size()) return;
219 beginRemoveRows(QModelIndex{}, row, row);
220 if (!alive) return;
221 snapshot_.erase(snapshot_.begin() + static_cast<std::ptrdiff_t>(ch.index));
222 endRemoveRows();
223 break;
224 }
225 case ::aria::ListChangeKind::Replace:
226 case ::aria::ListChangeKind::ItemChanged: {
227 if (ch.index >= snapshot_.size()) return;
228 auto retired = std::move(snapshot_[ch.index]);
229 snapshot_[ch.index] = ch.item;
230 auto idx = createIndex(static_cast<int>(ch.index), 0);
231 Q_EMIT dataChanged(idx, idx, roles_.keys());
232 break;
233 }
234 case ::aria::ListChangeKind::Move: {
235 // Translate (from, to) into Qt's beginMoveRows contract.
236 // Qt's `destinationChild` is the index in the FINAL layout
237 // where the moved row should appear *as if the source were
238 // still there* — i.e. for downward moves you pass `to + 1`.
239 // See QAbstractItemModel::beginMoveRows for details.
240 if (ch.from_index >= snapshot_.size()
241 || ch.index >= snapshot_.size()
242 || ch.from_index == ch.index) return;
243
244 const auto from = static_cast<int>(ch.from_index);
245 const auto to = static_cast<int>(ch.index);
246 const int dest = (to > from) ? to + 1 : to;
247
248 if (!beginMoveRows(QModelIndex{}, from, from, QModelIndex{}, dest) || !alive) return;
249 auto moved = snapshot_[ch.from_index];
250 snapshot_.erase(snapshot_.begin()
251 + static_cast<std::ptrdiff_t>(ch.from_index));
252 snapshot_.insert(snapshot_.begin()
253 + static_cast<std::ptrdiff_t>(ch.index),
254 std::move(moved));
255 endMoveRows();
256 break;
257 }
258 case ::aria::ListChangeKind::Reset: {
259 if (!ch.snapshot)
260 throw std::invalid_argument("ObservableListModel: Reset requires its snapshot");
261 check_size_(ch.snapshot->size());
262 auto next = *ch.snapshot;
263 beginResetModel();
264 if (!alive) return;
265 snapshot_.swap(next);
266 endResetModel();
267 break;
268 }
269 }
270 }
271
272 RoleMap roles_;
273 std::shared_ptr<RoleFn> role_fn_;
274 std::vector<std::shared_ptr<T>> snapshot_;
275 std::shared_ptr<Delivery> delivery_ = std::make_shared<Delivery>();
276 ::aria::Subscription sub_;
277};
278
279} // namespace aria::adapters::qt6
Definition qt_list_model_adapter.hpp:61
void reload()
Re-notify Qt views using the current event-maintained snapshot.
Definition qt_list_model_adapter.hpp:129
QHash< int, QByteArray > RoleMap
Definition qt_list_model_adapter.hpp:63
QHash< int, QByteArray > roleNames() const override
Definition qt_list_model_adapter.hpp:123
requires ::aria::ListSourceOf< L, T > ObservableListModel(L &source, RoleMap roles, RoleFn role_fn, QObject *parent=nullptr)
Generic constructor: accepts any list source whose element type matches T.
Definition qt_list_model_adapter.hpp:71
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
Definition qt_list_model_adapter.hpp:105
int rowCount(const QModelIndex &parent=QModelIndex{}) const override
Definition qt_list_model_adapter.hpp:100
std::function< QVariant(const T &, int role)> RoleFn
Definition qt_list_model_adapter.hpp:64
~ObservableListModel() override
Definition qt_list_model_adapter.hpp:87
Definition qt_adapter.hpp:12
void report_callback_failure(std::string_view category, std::exception_ptr exception, std::string_view message={}) noexcept
Report a callback failure.
Definition validation_key.hpp:110