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