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