1 /*
2 SPDX-FileCopyrightText: 2010 Marco Martin <mart@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7 #include "datamodel.h"
8 #include "datasource.h"
9
10 #include <QQmlContext>
11 #include <QQmlEngine>
12 #include <QTimer>
13
14 namespace Plasma
15 {
SortFilterModel(QObject * parent)16 SortFilterModel::SortFilterModel(QObject *parent)
17 : QSortFilterProxyModel(parent)
18 {
19 setObjectName(QStringLiteral("SortFilterModel"));
20 setDynamicSortFilter(true);
21 connect(this, &QAbstractItemModel::rowsInserted, this, &SortFilterModel::countChanged);
22 connect(this, &QAbstractItemModel::rowsRemoved, this, &SortFilterModel::countChanged);
23 connect(this, &QAbstractItemModel::modelReset, this, &SortFilterModel::countChanged);
24 connect(this, &SortFilterModel::countChanged, this, &SortFilterModel::syncRoleNames);
25 }
26
~SortFilterModel()27 SortFilterModel::~SortFilterModel()
28 {
29 }
30
syncRoleNames()31 void SortFilterModel::syncRoleNames()
32 {
33 if (!sourceModel()) {
34 return;
35 }
36
37 m_roleIds.clear();
38 const QHash<int, QByteArray> rNames = roleNames();
39 m_roleIds.reserve(rNames.count());
40 for (auto i = rNames.constBegin(); i != rNames.constEnd(); ++i) {
41 m_roleIds[QString::fromUtf8(i.value())] = i.key();
42 }
43
44 setFilterRole(m_filterRole);
45 setSortRole(m_sortRole);
46 }
47
roleNames() const48 QHash<int, QByteArray> SortFilterModel::roleNames() const
49 {
50 if (sourceModel()) {
51 return sourceModel()->roleNames();
52 }
53 return {};
54 }
55
roleNameToId(const QString & name) const56 int SortFilterModel::roleNameToId(const QString &name) const
57 {
58 return m_roleIds.value(name, Qt::DisplayRole);
59 }
60
setModel(QAbstractItemModel * model)61 void SortFilterModel::setModel(QAbstractItemModel *model)
62 {
63 if (model == sourceModel()) {
64 return;
65 }
66
67 if (sourceModel()) {
68 disconnect(sourceModel(), &QAbstractItemModel::modelReset, this, &SortFilterModel::syncRoleNames);
69 }
70
71 QSortFilterProxyModel::setSourceModel(model);
72
73 if (model) {
74 connect(model, &QAbstractItemModel::modelReset, this, &SortFilterModel::syncRoleNames);
75 syncRoleNames();
76 }
77
78 Q_EMIT sourceModelChanged(model);
79 }
80
filterAcceptsRow(int source_row,const QModelIndex & source_parent) const81 bool SortFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
82 {
83 if (m_filterCallback.isCallable()) {
84 QJSValueList args;
85 args << QJSValue(source_row);
86
87 const QModelIndex idx = sourceModel()->index(source_row, filterKeyColumn(), source_parent);
88 QQmlEngine *engine = QQmlEngine::contextForObject(this)->engine();
89 args << engine->toScriptValue<QVariant>(idx.data(m_roleIds.value(m_filterRole)));
90
91 return const_cast<SortFilterModel *>(this)->m_filterCallback.call(args).toBool();
92 }
93
94 return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
95 }
96
setFilterRegExp(const QString & exp)97 void SortFilterModel::setFilterRegExp(const QString &exp)
98 {
99 if (exp == filterRegExp()) {
100 return;
101 }
102 QSortFilterProxyModel::setFilterRegExp(QRegExp(exp, Qt::CaseInsensitive));
103 Q_EMIT filterRegExpChanged(exp);
104 }
105
filterRegExp() const106 QString SortFilterModel::filterRegExp() const
107 {
108 return QSortFilterProxyModel::filterRegExp().pattern();
109 }
110
setFilterString(const QString & filterString)111 void SortFilterModel::setFilterString(const QString &filterString)
112 {
113 if (filterString == m_filterString) {
114 return;
115 }
116 m_filterString = filterString;
117 QSortFilterProxyModel::setFilterFixedString(filterString);
118 Q_EMIT filterStringChanged(filterString);
119 }
120
filterString() const121 QString SortFilterModel::filterString() const
122 {
123 return m_filterString;
124 }
125
filterCallback() const126 QJSValue SortFilterModel::filterCallback() const
127 {
128 return m_filterCallback;
129 }
130
setFilterCallback(const QJSValue & callback)131 void SortFilterModel::setFilterCallback(const QJSValue &callback)
132 {
133 if (m_filterCallback.strictlyEquals(callback)) {
134 return;
135 }
136
137 if (!callback.isNull() && !callback.isCallable()) {
138 return;
139 }
140
141 m_filterCallback = callback;
142 invalidateFilter();
143
144 Q_EMIT filterCallbackChanged(callback);
145 }
146
setFilterRole(const QString & role)147 void SortFilterModel::setFilterRole(const QString &role)
148 {
149 QSortFilterProxyModel::setFilterRole(roleNameToId(role));
150 m_filterRole = role;
151 }
152
filterRole() const153 QString SortFilterModel::filterRole() const
154 {
155 return m_filterRole;
156 }
157
setSortRole(const QString & role)158 void SortFilterModel::setSortRole(const QString &role)
159 {
160 m_sortRole = role;
161 if (role.isEmpty()) {
162 sort(-1, Qt::AscendingOrder);
163 } else if (sourceModel()) {
164 QSortFilterProxyModel::setSortRole(roleNameToId(role));
165 sort(sortColumn(), sortOrder());
166 }
167 }
168
sortRole() const169 QString SortFilterModel::sortRole() const
170 {
171 return m_sortRole;
172 }
173
setSortOrder(const Qt::SortOrder order)174 void SortFilterModel::setSortOrder(const Qt::SortOrder order)
175 {
176 if (order == sortOrder()) {
177 return;
178 }
179 sort(sortColumn(), order);
180 }
181
setSortColumn(int column)182 void SortFilterModel::setSortColumn(int column)
183 {
184 if (column == sortColumn()) {
185 return;
186 }
187 sort(column, sortOrder());
188 Q_EMIT sortColumnChanged();
189 }
190
get(int row) const191 QVariantMap SortFilterModel::get(int row) const
192 {
193 QModelIndex idx = index(row, 0);
194 QVariantMap hash;
195
196 const QHash<int, QByteArray> rNames = roleNames();
197 for (auto i = rNames.begin(); i != rNames.end(); ++i) {
198 hash[QString::fromUtf8(i.value())] = data(idx, i.key());
199 }
200
201 return hash;
202 }
203
mapRowToSource(int row) const204 int SortFilterModel::mapRowToSource(int row) const
205 {
206 QModelIndex idx = index(row, 0);
207 return mapToSource(idx).row();
208 }
209
mapRowFromSource(int row) const210 int SortFilterModel::mapRowFromSource(int row) const
211 {
212 if (!sourceModel()) {
213 qWarning() << "No source model defined!";
214 return -1;
215 }
216 QModelIndex idx = sourceModel()->index(row, 0);
217 return mapFromSource(idx).row();
218 }
219
DataModel(QObject * parent)220 DataModel::DataModel(QObject *parent)
221 : QAbstractItemModel(parent)
222 , m_dataSource(nullptr)
223 , m_maxRoleId(Qt::UserRole + 1)
224 {
225 // There is one reserved role name: DataEngineSource
226 m_roleNames[m_maxRoleId] = QByteArrayLiteral("DataEngineSource");
227 m_roleIds[QStringLiteral("DataEngineSource")] = m_maxRoleId;
228 ++m_maxRoleId;
229
230 setObjectName(QStringLiteral("DataModel"));
231 connect(this, &QAbstractItemModel::rowsInserted, this, &DataModel::countChanged);
232 connect(this, &QAbstractItemModel::rowsRemoved, this, &DataModel::countChanged);
233 connect(this, &QAbstractItemModel::modelReset, this, &DataModel::countChanged);
234 }
235
~DataModel()236 DataModel::~DataModel()
237 {
238 }
239
dataUpdated(const QString & sourceName,const QVariantMap & data)240 void DataModel::dataUpdated(const QString &sourceName, const QVariantMap &data)
241 {
242 if (!m_sourceFilter.isEmpty() && m_sourceFilterRE.isValid() && !m_sourceFilterRE.exactMatch(sourceName)) {
243 return;
244 }
245
246 if (m_keyRoleFilter.isEmpty()) {
247 // an item is represented by a source: keys are roles m_roleLevel == FirstLevel
248 QVariantList list;
249
250 if (!m_dataSource->data()->isEmpty()) {
251 const auto lst = m_dataSource->data()->keys();
252 for (const QString &key : lst) {
253 if (!m_sourceFilter.isEmpty() && m_sourceFilterRE.isValid() && !m_sourceFilterRE.exactMatch(key)) {
254 continue;
255 }
256 QVariant value = m_dataSource->data()->value(key);
257 if (value.isValid() && value.canConvert<Plasma::DataEngine::Data>()) {
258 Plasma::DataEngine::Data data = value.value<Plasma::DataEngine::Data>();
259 data[QStringLiteral("DataEngineSource")] = key;
260 list.append(data);
261 }
262 }
263 }
264 setItems(QString(), list);
265 } else {
266 // a key that matches the one we want exists and is a list of DataEngine::Data
267 if (data.contains(m_keyRoleFilter) && data.value(m_keyRoleFilter).canConvert<QVariantList>()) {
268 setItems(sourceName, data.value(m_keyRoleFilter).value<QVariantList>());
269 } else if (m_keyRoleFilterRE.isValid()) {
270 // try to match the key we want with a regular expression if set
271 QVariantList list;
272 QVariantMap::const_iterator i;
273 for (i = data.constBegin(); i != data.constEnd(); ++i) {
274 if (m_keyRoleFilterRE.exactMatch(i.key())) {
275 list.append(i.value());
276 }
277 }
278 setItems(sourceName, list);
279 }
280 }
281 }
282
setDataSource(QObject * object)283 void DataModel::setDataSource(QObject *object)
284 {
285 DataSource *source = qobject_cast<DataSource *>(object);
286 if (!source) {
287 qWarning() << "Error: DataSource type expected";
288 return;
289 }
290 if (m_dataSource == source) {
291 return;
292 }
293
294 if (m_dataSource) {
295 disconnect(m_dataSource, nullptr, this, nullptr);
296 }
297
298 m_dataSource = source;
299
300 const auto keys = m_dataSource->data()->keys();
301 for (const QString &key : keys) {
302 dataUpdated(key, m_dataSource->data()->value(key).value<Plasma::DataEngine::Data>());
303 }
304
305 connect(m_dataSource, &DataSource::newData, this, &DataModel::dataUpdated);
306 connect(m_dataSource, &DataSource::sourceRemoved, this, &DataModel::removeSource);
307 connect(m_dataSource, &DataSource::sourceDisconnected, this, &DataModel::removeSource);
308 }
309
dataSource() const310 QObject *DataModel::dataSource() const
311 {
312 return m_dataSource;
313 }
314
setKeyRoleFilter(const QString & key)315 void DataModel::setKeyRoleFilter(const QString &key)
316 {
317 // the "key role filter" can be used in one of three ways:
318 //
319 // 1) empty string -> all data is used, each source is one row in the model
320 // 2) matches a key in the data exactly -> only that key/value pair is used, and the value is
321 // treated as a collection where each item in the collection becomes a row in the model
322 // 3) regular expression -> matches zero or more keys in the data, and each matching key/value
323 // pair becomes a row in the model
324 if (m_keyRoleFilter == key) {
325 return;
326 }
327
328 m_keyRoleFilter = key;
329 m_keyRoleFilterRE = QRegExp(m_keyRoleFilter);
330 }
331
keyRoleFilter() const332 QString DataModel::keyRoleFilter() const
333 {
334 return m_keyRoleFilter;
335 }
336
setSourceFilter(const QString & key)337 void DataModel::setSourceFilter(const QString &key)
338 {
339 if (m_sourceFilter == key) {
340 return;
341 }
342
343 m_sourceFilter = key;
344 m_sourceFilterRE = QRegExp(key);
345 /*
346 FIXME: if the user changes the source filter, it won't immediately be reflected in the
347 available data
348 if (m_sourceFilterRE.isValid()) {
349 .. iterate through all items and weed out the ones that don't match ..
350 }
351 */
352 }
353
sourceFilter() const354 QString DataModel::sourceFilter() const
355 {
356 return m_sourceFilter;
357 }
358
setItems(const QString & sourceName,const QVariantList & list)359 void DataModel::setItems(const QString &sourceName, const QVariantList &list)
360 {
361 const int oldLength = m_items.value(sourceName).count();
362 const int delta = list.length() - oldLength;
363 const bool firstRun = m_items.isEmpty();
364
365 // At what row number the first item associated to this source starts
366 int sourceIndex = 0;
367 QMap<QString, QVector<QVariant>>::const_iterator i;
368 for (i = m_items.constBegin(); i != m_items.constEnd(); ++i) {
369 if (i.key() == sourceName) {
370 break;
371 }
372 sourceIndex += i.value().count();
373 }
374 // signal as inserted the rows at the end, all the other rows will signal a dataupdated.
375 // better than a model reset because doesn't cause deletion and re-creation of every list item on a qml ListView, repeaters etc.
376 // the first run it gets reset because otherwise setRoleNames gets broken
377 if (firstRun) {
378 beginResetModel();
379 } else if (delta > 0) {
380 beginInsertRows(QModelIndex(), sourceIndex + oldLength, sourceIndex + list.length() - 1);
381 } else if (delta < 0) {
382 beginRemoveRows(QModelIndex(), sourceIndex + list.length(), sourceIndex + oldLength - 1);
383 }
384 // convert to vector, so data() will be O(1)
385 m_items[sourceName] = list.toVector();
386
387 if (!list.isEmpty()) {
388 if (list.first().canConvert<QVariantMap>()) {
389 for (const QVariant &item : list) {
390 const QVariantMap &vh = item.value<QVariantMap>();
391 QMapIterator<QString, QVariant> it(vh);
392 while (it.hasNext()) {
393 it.next();
394 const QString &roleName = it.key();
395 if (!m_roleIds.contains(roleName)) {
396 ++m_maxRoleId;
397 m_roleNames[m_maxRoleId] = roleName.toLatin1();
398 m_roleIds[roleName] = m_maxRoleId;
399 }
400 }
401 }
402 } else {
403 for (const QVariant &item : list) {
404 const QVariantMap &vh = item.value<QVariantMap>();
405 QMapIterator<QString, QVariant> it(vh);
406 while (it.hasNext()) {
407 it.next();
408 const QString &roleName = it.key();
409 if (!m_roleIds.contains(roleName)) {
410 ++m_maxRoleId;
411 m_roleNames[m_maxRoleId] = roleName.toLatin1();
412 m_roleIds[roleName] = m_maxRoleId;
413 }
414 }
415 }
416 }
417 }
418
419 if (firstRun) {
420 endResetModel();
421 } else if (delta > 0) {
422 endInsertRows();
423 } else if (delta < 0) {
424 endRemoveRows();
425 }
426 Q_EMIT dataChanged(createIndex(sourceIndex, 0), createIndex(sourceIndex + qMin(list.length(), oldLength), 0));
427 }
428
roleNames() const429 QHash<int, QByteArray> DataModel::roleNames() const
430 {
431 return m_roleNames;
432 }
433
removeSource(const QString & sourceName)434 void DataModel::removeSource(const QString &sourceName)
435 {
436 // FIXME: find a way to remove only the proper things also in the case where sources are items
437
438 if (m_keyRoleFilter.isEmpty()) {
439 // source name in the map, linear scan
440 for (int i = 0; i < m_items.value(QString()).count(); ++i) {
441 if (m_items.value(QString())[i].value<QVariantMap>().value(QStringLiteral("DataEngineSource")) == sourceName) {
442 beginRemoveRows(QModelIndex(), i, i);
443 m_items[QString()].remove(i);
444 endRemoveRows();
445 break;
446 }
447 }
448 } else {
449 if (m_items.contains(sourceName)) {
450 // At what row number the first item associated to this source starts
451 int sourceIndex = 0;
452 for (auto i = m_items.constBegin(); i != m_items.constEnd(); ++i) {
453 if (i.key() == sourceName) {
454 break;
455 }
456 sourceIndex += i.value().count();
457 }
458
459 // source name as key of the map
460
461 int count = m_items.value(sourceName).count();
462 if (count > 0) {
463 beginRemoveRows(QModelIndex(), sourceIndex, sourceIndex + count - 1);
464 }
465 m_items.remove(sourceName);
466 if (count > 0) {
467 endRemoveRows();
468 }
469 }
470 }
471 }
472
data(const QModelIndex & index,int role) const473 QVariant DataModel::data(const QModelIndex &index, int role) const
474 {
475 if (!index.isValid() || index.column() > 0 || index.row() < 0 || index.row() >= countItems()) {
476 return QVariant();
477 }
478
479 int count = 0;
480 int actualRow = 0;
481 QString source;
482 QMap<QString, QVector<QVariant>>::const_iterator i;
483 for (i = m_items.constBegin(); i != m_items.constEnd(); ++i) {
484 const int oldCount = count;
485 count += i.value().count();
486
487 if (index.row() < count) {
488 source = i.key();
489 actualRow = index.row() - oldCount;
490 break;
491 }
492 }
493
494 // is it the reserved role: DataEngineSource ?
495 // also, if each source is an item DataEngineSource is a role between all the others, otherwise we know it from the role variable
496 if (!m_keyRoleFilter.isEmpty() && m_roleNames.value(role) == "DataEngineSource") {
497 return source;
498 } else {
499 return m_items.value(source).value(actualRow).value<QVariantMap>().value(QString::fromUtf8(m_roleNames.value(role)));
500 }
501 }
502
headerData(int section,Qt::Orientation orientation,int role) const503 QVariant DataModel::headerData(int section, Qt::Orientation orientation, int role) const
504 {
505 Q_UNUSED(section)
506 Q_UNUSED(orientation)
507 Q_UNUSED(role)
508
509 return QVariant();
510 }
511
index(int row,int column,const QModelIndex & parent) const512 QModelIndex DataModel::index(int row, int column, const QModelIndex &parent) const
513 {
514 if (parent.isValid() || column > 0 || row < 0 || row >= countItems()) {
515 return QModelIndex();
516 }
517
518 return createIndex(row, column);
519 }
520
parent(const QModelIndex & child) const521 QModelIndex DataModel::parent(const QModelIndex &child) const
522 {
523 Q_UNUSED(child)
524
525 return QModelIndex();
526 }
527
rowCount(const QModelIndex & parent) const528 int DataModel::rowCount(const QModelIndex &parent) const
529 {
530 // this is not a tree
531 // TODO: make it possible some day?
532 if (parent.isValid()) {
533 return 0;
534 }
535
536 return countItems();
537 }
538
columnCount(const QModelIndex & parent) const539 int DataModel::columnCount(const QModelIndex &parent) const
540 {
541 if (parent.isValid()) {
542 return 0;
543 }
544
545 return 1;
546 }
547
get(int row) const548 QVariantMap DataModel::get(int row) const
549 {
550 QModelIndex idx = index(row, 0);
551 QVariantMap map;
552
553 const QHash<int, QByteArray> rNames = roleNames();
554 for (auto i = rNames.constBegin(); i != rNames.constEnd(); ++i) {
555 map[QString::fromUtf8(i.value())] = data(idx, i.key());
556 }
557
558 return map;
559 }
560
561 }
562