Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
qt_combo_box_binding.hpp
Go to the documentation of this file.
1#pragma once
2
4#include "aria/property.hpp"
6
7#include <QAbstractItemModel>
8#include <QComboBox>
9#include <QObject>
10#include <QPointer>
11#include <QThread>
12#include <QVariant>
13
14#include <functional>
15#include <memory>
16#include <optional>
17#include <utility>
18
19namespace aria::adapters::qt6 {
20
21namespace detail {
22
23template<typename Key, typename KeyFromData>
24class ComboBoxSelectionBinding final : public QObject {
25public:
26 ComboBoxSelectionBinding(Property<std::optional<Key>>& selected,
27 QComboBox& combo, KeyFromData key_from_data,
28 int role)
29 : selected_(&selected), combo_(&combo), model_(combo.model()),
30 key_from_data_(std::move(key_from_data)), role_(role) {}
31
32 void start(std::weak_ptr<ComboBoxSelectionBinding> weak) {
33 // activated identifies user input. currentIndexChanged also fires
34 // while Qt inserts/removes rows and chooses a fallback selection;
35 // that automatic choice must never overwrite the ViewModel's ID.
36 QObject::connect(combo_, &QComboBox::activated, this,
37 [weak](int index) {
38 if (auto state = weak.lock()) {
39 state->at_callback_boundary([&] { state->activate(index); });
40 }
41 });
42 QObject::connect(combo_, &QObject::destroyed, this, [weak] {
43 if (auto state = weak.lock()) state->stop();
44 });
45 QObject::connect(model_, &QObject::destroyed, this, [weak] {
46 if (auto state = weak.lock()) state->stop();
47 });
48
49 const auto refresh = [weak] {
50 if (auto state = weak.lock()) {
51 state->at_callback_boundary([&] { state->synchronize(true); });
52 }
53 };
54 QObject::connect(model_, &QAbstractItemModel::rowsInserted, this, refresh);
55 QObject::connect(model_, &QAbstractItemModel::rowsRemoved, this, refresh);
56 QObject::connect(model_, &QAbstractItemModel::rowsMoved, this, refresh);
57 QObject::connect(model_, &QAbstractItemModel::dataChanged, this, refresh);
58 QObject::connect(model_, &QAbstractItemModel::modelReset, this, refresh);
59 QObject::connect(model_, &QAbstractItemModel::layoutChanged, this, refresh);
60
61 selected_sub_ = selected_->on_changed([weak](const std::optional<Key>&) {
62 if (auto state = weak.lock()) {
63 state->at_callback_boundary([&] { state->synchronize(false); });
64 }
65 });
66 synchronize(false);
67 }
68
69 void stop() noexcept {
70 // Clear the widget first: a callback already in flight may retain
71 // this state until it returns, but must perform no further work.
72 combo_.clear();
73 model_.clear();
74 selected_sub_.release();
75 }
76
77private:
78 template<typename Callback>
79 static void at_callback_boundary(Callback&& callback) noexcept {
80 try {
81 std::forward<Callback>(callback)();
82 } catch (...) {
83 aria::report_callback_failure("qt.combo_box_selection",
84 std::current_exception());
85 }
86 }
87
88 void activate(int index) {
89 if (!combo_ || !model_) return;
90 std::optional<Key> next;
91 if (index >= 0 && index < combo_->count()) {
92 next = std::invoke(key_from_data_, combo_->itemData(index, role_));
93 }
94 resolved_key_ = next;
95 selected_->set(std::move(next));
96 }
97
98 void synchronize(bool model_changed) {
99 if (!combo_ || !model_) return;
100 const auto key = selected_->peek();
101 int row = -1;
102 if (key) {
103 for (int i = 0; i < combo_->count(); ++i) {
104 if (std::invoke(key_from_data_, combo_->itemData(i, role_)) == *key) {
105 row = i;
106 break;
107 }
108 }
109 }
110
111 const bool removed = model_changed && key && resolved_key_ == key && row < 0;
112 resolved_key_ = row >= 0 ? key : std::nullopt;
113 if (removed) {
114 selected_->set(std::nullopt);
115 // Observers may choose a fallback ID, or notifications may be
116 // batched. Apply the latest Property value in either case.
117 synchronize(false);
118 return;
119 }
120 combo_->setCurrentIndex(row);
121 }
122
123 Property<std::optional<Key>>* selected_;
124 QPointer<QComboBox> combo_;
125 QPointer<QAbstractItemModel> model_;
126 KeyFromData key_from_data_;
127 int role_;
128 std::optional<Key> resolved_key_;
129 Subscription selected_sub_;
130};
131
132} // namespace detail
133
159template<typename Key, typename KeyFromData>
161 Property<std::optional<Key>>& selected, QComboBox& combo,
162 KeyFromData key_from_data, int role = Qt::UserRole) {
163 Q_ASSERT(QThread::currentThread() == combo.thread());
164 Q_ASSERT(combo.model() && combo.model()->thread() == combo.thread());
165 // An empty combo creates its popup lazily. That creation reconnects
166 // Qt's model handlers, so complete it before installing our end-of-
167 // change handlers; Qt's default selection must settle before we align.
168 (void)combo.view();
169 using State = detail::ComboBoxSelectionBinding<Key, KeyFromData>;
170 auto state = std::make_shared<State>(selected, combo, std::move(key_from_data), role);
171 state->start(state);
172 return Subscription{std::function<void()>{[state] { state->stop(); }}};
173}
174
175} // namespace aria::adapters::qt6
RAII handle to a single subscription.
Definition subscription.hpp:44
Definition property.hpp:103
Definition qt_adapter.hpp:12
Subscription bind_combo_box_selection(Property< std::optional< Key > > &selected, QComboBox &combo, KeyFromData key_from_data, int role=Qt::UserRole)
Bind a ViewModel's stable selected ID to a QComboBox's existing model.
Definition qt_combo_box_binding.hpp:160
void report_callback_failure(std::string_view category, std::exception_ptr exception, std::string_view message={}) noexcept
Report a callback failure.