1 /*
2 SPDX-FileCopyrightText: 2013-2015 Andreas Cord-Landwehr <cordlandwehr@kde.org>
3
4 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
5 */
6
7 #include "phrasemodel.h"
8 #include "artikulate_debug.h"
9 #include "core/icourse.h"
10 #include "core/unit.h"
11 #include <KLocalizedString>
12 #include <QSignalMapper>
13
PhraseModel(QObject * parent)14 PhraseModel::PhraseModel(QObject *parent)
15 : QAbstractItemModel(parent)
16 , m_course(nullptr)
17 , m_unitSignalMapper(new QSignalMapper)
18 , m_phraseSignalMapper(new QSignalMapper)
19 {
20 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
21 connect(m_unitSignalMapper, static_cast<void (QSignalMapper::*)(int)>(&QSignalMapper::mapped), this, &PhraseModel::onUnitChanged);
22 #else
23 connect(m_unitSignalMapper, &QSignalMapper::mappedInt, this, &PhraseModel::onUnitChanged);
24 #endif
25 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
26 connect(m_phraseSignalMapper, static_cast<void (QSignalMapper::*)(QObject *)>(&QSignalMapper::mapped), this, &PhraseModel::onPhraseChanged);
27 #else
28 connect(m_phraseSignalMapper, &QSignalMapper::mappedObject, this, &PhraseModel::onPhraseChanged);
29 #endif
30 }
31
roleNames() const32 QHash<int, QByteArray> PhraseModel::roleNames() const
33 {
34 QHash<int, QByteArray> roles;
35 roles[TextRole] = "text";
36 roles[DataRole] = "dataRole";
37
38 return roles;
39 }
40
setCourse(ICourse * course)41 void PhraseModel::setCourse(ICourse *course)
42 {
43 if (m_course == course) {
44 return;
45 }
46
47 beginResetModel();
48
49 if (m_course) {
50 m_course->disconnect(this);
51 for (auto unit : m_course->units()) {
52 unit->disconnect(this);
53 for (auto &phrase : unit->phrases()) {
54 phrase->disconnect(this);
55 }
56 }
57 }
58
59 m_course = course;
60 if (m_course) {
61 // connect to unit changes
62 connect(m_course, &ICourse::unitAboutToBeAdded, this, &PhraseModel::onUnitAboutToBeAdded);
63 connect(m_course, &ICourse::unitAdded, this, &PhraseModel::onUnitAdded);
64 connect(m_course, &ICourse::unitsAboutToBeRemoved, this, &PhraseModel::onUnitsAboutToBeRemoved);
65 connect(m_course, &ICourse::unitsRemoved, this, &PhraseModel::onUnitsRemoved);
66
67 // initial setting of signal mappings
68 for (auto unit : m_course->units()) {
69 // connect to phrase changes
70 connect(unit.get(), &Unit::phraseAboutToBeAdded, this, &PhraseModel::onPhraseAboutToBeAdded);
71 connect(unit.get(), &Unit::phraseAdded, this, &PhraseModel::onPhraseAdded);
72 connect(unit.get(), &Unit::phraseAboutToBeRemoved, this, &PhraseModel::onPhraseAboutToBeRemoved);
73 connect(unit.get(), &Unit::phraseRemoved, this, &PhraseModel::onPhrasesRemoved);
74 connect(unit.get(), &Unit::titleChanged, m_unitSignalMapper, static_cast<void (QSignalMapper::*)()>(&QSignalMapper::map));
75
76 // insert and connect all already existing phrases
77 int phrases = unit->phrases().count();
78 for (int i = 0; i < phrases; ++i) {
79 onPhraseAboutToBeAdded(unit->phrases().at(i), i);
80 endInsertRows();
81 }
82 }
83 updateUnitMappings();
84 updatePhraseMappings();
85 }
86
87 // emit done
88 endResetModel();
89 emit courseChanged();
90 }
91
course() const92 ICourse *PhraseModel::course() const
93 {
94 return m_course;
95 }
96
data(const QModelIndex & index,int role) const97 QVariant PhraseModel::data(const QModelIndex &index, int role) const
98 {
99 Q_ASSERT(m_course);
100
101 if (!index.isValid()) {
102 return QVariant();
103 }
104
105 if (!index.internalPointer()) {
106 if (!m_course || m_course->units().size() == 0) {
107 return QVariant();
108 }
109 auto unit = m_course->units().at(index.row());
110 switch (role) {
111 case TextRole:
112 return unit->title();
113 case DataRole:
114 return QVariant::fromValue<QObject *>(unit.get());
115 default:
116 return QVariant();
117 }
118 } else {
119 Unit *unit = static_cast<Unit *>(index.internalPointer());
120 switch (role) {
121 case TextRole:
122 return unit->phrases().at(index.row())->text();
123 case DataRole:
124 return QVariant::fromValue<QObject *>(unit->phrases().at(index.row()).get());
125 default:
126 return QVariant();
127 }
128 }
129 }
130
rowCount(const QModelIndex & parent) const131 int PhraseModel::rowCount(const QModelIndex &parent) const
132 {
133 if (!m_course) {
134 return 0;
135 }
136
137 // no valid index -> must be (invisible) root
138 if (!parent.isValid()) {
139 return m_course->units().count();
140 }
141
142 // internal pointer -> must be a phrase
143 if (parent.internalPointer()) {
144 return 0;
145 }
146
147 // else -> must be a unit
148 Unit *unit = m_course->units().at(parent.row()).get();
149 return unit->phrases().count();
150 }
151
columnCount(const QModelIndex & parent) const152 int PhraseModel::columnCount(const QModelIndex &parent) const
153 {
154 Q_UNUSED(parent);
155 return 1;
156 }
157
parent(const QModelIndex & child) const158 QModelIndex PhraseModel::parent(const QModelIndex &child) const
159 {
160 if (!child.internalPointer() || !m_course) {
161 return QModelIndex();
162 }
163 Unit *parent = static_cast<Unit *>(child.internalPointer());
164 for (int i = 0; i < m_course->units().count(); ++i) {
165 if (m_course->units().at(i).get() == parent) {
166 return createIndex(i, 0);
167 }
168 }
169 return QModelIndex();
170 }
171
index(int row,int column,const QModelIndex & parent) const172 QModelIndex PhraseModel::index(int row, int column, const QModelIndex &parent) const
173 {
174 if (!parent.isValid()) { // unit elements
175 return createIndex(row, column);
176 } else { // phrase elements
177 auto unit = m_course->units().at(parent.row());
178 if (unit) {
179 return createIndex(row, column, unit.get());
180 }
181 }
182 return QModelIndex();
183 }
184
indexPhrase(Phrase * phrase) const185 QModelIndex PhraseModel::indexPhrase(Phrase *phrase) const
186 {
187 if (!phrase) {
188 return QModelIndex();
189 }
190 auto unit = phrase->unit();
191 return createIndex(unit->phrases().indexOf(phrase->self()), 0, unit.get());
192 }
193
indexUnit(Unit * unit) const194 QModelIndex PhraseModel::indexUnit(Unit *unit) const
195 {
196 if (!unit || !m_course) {
197 return QModelIndex();
198 }
199 int uIndex {-1};
200 for (int i = 0; i < m_course->units().size(); ++i) {
201 if (m_course->units().at(i)->id() == unit->id()) {
202 uIndex = i;
203 break;
204 }
205 }
206 return createIndex(uIndex, 0);
207 }
208
isUnit(const QModelIndex & index) const209 bool PhraseModel::isUnit(const QModelIndex &index) const
210 {
211 return (index.internalPointer() == nullptr);
212 }
213
onPhraseAboutToBeAdded(std::shared_ptr<IPhrase> phrase,int index)214 void PhraseModel::onPhraseAboutToBeAdded(std::shared_ptr<IPhrase> phrase, int index)
215 {
216 int uIndex {-1};
217 for (int i = 0; i < m_course->units().size(); ++i) {
218 if (m_course->units().at(i)->id() == phrase->unit()->id()) {
219 uIndex = i;
220 break;
221 }
222 }
223 connect(phrase.get(), &IPhrase::textChanged, m_phraseSignalMapper, static_cast<void (QSignalMapper::*)()>(&QSignalMapper::map));
224 beginInsertRows(createIndex(uIndex, 0), index, index);
225 }
226
onPhraseAdded()227 void PhraseModel::onPhraseAdded()
228 {
229 endInsertRows();
230 updatePhraseMappings();
231 }
232
onPhraseAboutToBeRemoved(int index)233 void PhraseModel::onPhraseAboutToBeRemoved(int index)
234 {
235 Q_UNUSED(index)
236 // TODO better solution requires access to unit
237 // TODO remove connections from m_phraseSignalMapper
238 beginResetModel();
239 }
240
onPhrasesRemoved()241 void PhraseModel::onPhrasesRemoved()
242 {
243 endResetModel();
244 }
245
onPhraseChanged(QObject * phrase)246 void PhraseModel::onPhraseChanged(QObject *phrase)
247 {
248 Phrase *changedPhrase = qobject_cast<Phrase *>(phrase);
249 Q_ASSERT(changedPhrase);
250 QModelIndex index = indexPhrase(changedPhrase);
251 emit dataChanged(index, index);
252 }
253
onUnitAboutToBeAdded(std::shared_ptr<Unit> unit,int index)254 void PhraseModel::onUnitAboutToBeAdded(std::shared_ptr<Unit> unit, int index)
255 {
256 Q_UNUSED(unit)
257 beginInsertRows(QModelIndex(), index, index);
258 connect(unit.get(), &Unit::titleChanged, m_unitSignalMapper, static_cast<void (QSignalMapper::*)()>(&QSignalMapper::map));
259 }
260
onUnitAdded()261 void PhraseModel::onUnitAdded()
262 {
263 endInsertRows();
264 updateUnitMappings();
265 }
266
onUnitsAboutToBeRemoved(int first,int last)267 void PhraseModel::onUnitsAboutToBeRemoved(int first, int last)
268 {
269 for (int i = first; i <= last; ++i) {
270 auto unit = m_course->units().at(i);
271 disconnect(unit.get(), &Unit::titleChanged, m_unitSignalMapper, static_cast<void (QSignalMapper::*)()>(&QSignalMapper::map));
272 }
273 beginRemoveRows(QModelIndex(), first, last);
274 }
275
onUnitsRemoved()276 void PhraseModel::onUnitsRemoved()
277 {
278 endRemoveRows();
279 }
280
onUnitChanged(int index)281 void PhraseModel::onUnitChanged(int index)
282 {
283 emit dataChanged(createIndex(index, 0), createIndex(index, 0));
284 }
285
headerData(int section,Qt::Orientation orientation,int role) const286 QVariant PhraseModel::headerData(int section, Qt::Orientation orientation, int role) const
287 {
288 if (role != Qt::DisplayRole) {
289 return QVariant();
290 }
291 if (orientation == Qt::Vertical) {
292 return QVariant(section + 1);
293 }
294 return QVariant(i18nc("@title:column", "Phrase"));
295 }
296
isPhrase(const QModelIndex & index) const297 bool PhraseModel::isPhrase(const QModelIndex &index) const
298 {
299 if (index.internalPointer()) {
300 return true;
301 }
302 return false;
303 }
304
phrase(const QModelIndex & index) const305 IPhrase *PhraseModel::phrase(const QModelIndex &index) const
306 {
307 if (index.internalPointer()) {
308 Unit *unit = static_cast<Unit *>(index.internalPointer());
309 return unit->phrases().at(index.row()).get();
310 }
311 if (!m_course->units().at(index.row())->phrases().isEmpty()) {
312 return m_course->units().at(index.row())->phrases().first().get();
313 }
314 return nullptr;
315 }
316
unit(const QModelIndex & index) const317 Unit *PhraseModel::unit(const QModelIndex &index) const
318 {
319 return m_course->units().at(index.row()).get();
320 }
321
updateUnitMappings()322 void PhraseModel::updateUnitMappings()
323 {
324 int units = m_course->units().count();
325 for (int i = 0; i < units; ++i) {
326 m_unitSignalMapper->setMapping(m_course->units().at(i).get(), i);
327 }
328 }
329
updatePhraseMappings()330 void PhraseModel::updatePhraseMappings()
331 {
332 // TODO this might be quite costly for long units
333 // better, implement access based on index pairs
334 for (auto unit : m_course->units()) {
335 for (const auto &phrase : unit->phrases()) {
336 m_phraseSignalMapper->setMapping(phrase.get(), phrase.get());
337 }
338 }
339 }
340