Skip to content

Commit d3258f6

Browse files
committed
core/scriptmodel: add expression model for unique lists
1 parent 2f194b7 commit d3258f6

File tree

8 files changed

+426
-2
lines changed

8 files changed

+426
-2
lines changed

.clang-tidy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Checks: >
1818
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
1919
-cppcoreguidelines-avoid-do-while,
2020
-cppcoreguidelines-pro-type-reinterpret-cast,
21+
-cppcoreguidelines-pro-type-vararg,
2122
google-global-names-in-headers,
2223
google-readability-casting,
2324
google-runtime-int,

src/core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ qt_add_library(quickshell-core STATIC
3636
instanceinfo.cpp
3737
common.cpp
3838
iconprovider.cpp
39+
scriptmodel.cpp
3940
)
4041

4142
qt_add_qml_module(quickshell-core

src/core/model.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <qabstractitemmodel.h>
44
#include <qhash.h>
5+
#include <qnamespace.h>
56
#include <qobject.h>
67
#include <qqmllist.h>
78
#include <qtmetamacros.h>
@@ -14,11 +15,13 @@ qint32 UntypedObjectModel::rowCount(const QModelIndex& parent) const {
1415
}
1516

1617
QVariant UntypedObjectModel::data(const QModelIndex& index, qint32 role) const {
17-
if (role != 0) return QVariant();
18+
if (role != Qt::UserRole) return QVariant();
1819
return QVariant::fromValue(this->valuesList.at(index.row()));
1920
}
2021

21-
QHash<int, QByteArray> UntypedObjectModel::roleNames() const { return {{0, "modelData"}}; }
22+
QHash<int, QByteArray> UntypedObjectModel::roleNames() const {
23+
return {{Qt::UserRole, "modelData"}};
24+
}
2225

2326
QQmlListProperty<QObject> UntypedObjectModel::values() {
2427
return QQmlListProperty<QObject>(

src/core/scriptmodel.cpp

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#include "scriptmodel.hpp"
2+
#include <algorithm>
3+
#include <iterator>
4+
5+
#include <qabstractitemmodel.h>
6+
#include <qcontainerfwd.h>
7+
#include <qlist.h>
8+
#include <qnamespace.h>
9+
#include <qtmetamacros.h>
10+
#include <qtypes.h>
11+
#include <qvariant.h>
12+
13+
void ScriptModel::updateValuesUnique(const QVariantList& newValues) {
14+
this->mValues.reserve(newValues.size());
15+
16+
auto iter = this->mValues.begin();
17+
auto newIter = newValues.begin();
18+
19+
while (true) {
20+
if (newIter == newValues.end()) {
21+
if (iter == this->mValues.end()) break;
22+
23+
auto startIndex = static_cast<qint32>(newValues.length());
24+
auto endIndex = static_cast<qint32>(this->mValues.length() - 1);
25+
26+
this->beginRemoveRows(QModelIndex(), startIndex, endIndex);
27+
this->mValues.erase(iter, this->mValues.end());
28+
this->endRemoveRows();
29+
30+
break;
31+
} else if (iter == this->mValues.end()) {
32+
// Prior branch ensures length is at least 1.
33+
auto startIndex = static_cast<qint32>(this->mValues.length());
34+
auto endIndex = static_cast<qint32>(newValues.length() - 1);
35+
36+
this->beginInsertRows(QModelIndex(), startIndex, endIndex);
37+
this->mValues.append(newValues.sliced(startIndex));
38+
this->endInsertRows();
39+
40+
break;
41+
} else if (*newIter != *iter) {
42+
auto oldIter = std::find(iter, this->mValues.end(), *newIter);
43+
44+
if (oldIter != this->mValues.end()) {
45+
if (std::find(newIter, newValues.end(), *iter) == newValues.end()) {
46+
// Remove any entries we would otherwise move around that aren't in the new list.
47+
auto startIter = iter;
48+
49+
do {
50+
++iter;
51+
} while (iter != this->mValues.end()
52+
&& std::find(newIter, newValues.end(), *iter) == newValues.end());
53+
54+
auto index = static_cast<qint32>(std::distance(this->mValues.begin(), iter));
55+
auto startIndex = static_cast<qint32>(std::distance(this->mValues.begin(), startIter));
56+
57+
this->beginRemoveRows(QModelIndex(), startIndex, index - 1);
58+
iter = this->mValues.erase(startIter, iter);
59+
this->endRemoveRows();
60+
} else {
61+
// Advance iters to capture a whole move sequence as a single operation if possible.
62+
auto oldStartIter = oldIter;
63+
do {
64+
++oldIter;
65+
++newIter;
66+
} while (oldIter != this->mValues.end() && newIter != newValues.end()
67+
&& *oldIter == *newIter);
68+
69+
auto index = static_cast<qint32>(std::distance(this->mValues.begin(), iter));
70+
auto oldStartIndex =
71+
static_cast<qint32>(std::distance(this->mValues.begin(), oldStartIter));
72+
auto oldIndex = static_cast<qint32>(std::distance(this->mValues.begin(), oldIter));
73+
auto len = oldIndex - oldStartIndex;
74+
75+
this->beginMoveRows(QModelIndex(), oldStartIndex, oldIndex - 1, QModelIndex(), index);
76+
77+
// While it is possible to optimize this further, it is currently not worth the time.
78+
for (auto i = 0; i != len; i++) {
79+
this->mValues.move(oldStartIndex + i, index + i);
80+
}
81+
82+
iter = this->mValues.begin() + (index + len);
83+
this->endMoveRows();
84+
}
85+
} else {
86+
auto startNewIter = newIter;
87+
88+
do {
89+
newIter++;
90+
} while (newIter != newValues.end()
91+
&& std::find(iter, this->mValues.end(), *newIter) == this->mValues.end());
92+
93+
auto index = static_cast<qint32>(std::distance(this->mValues.begin(), iter));
94+
auto newIndex = static_cast<qint32>(std::distance(newValues.begin(), newIter));
95+
auto startNewIndex = static_cast<qint32>(std::distance(newValues.begin(), startNewIter));
96+
auto len = newIndex - startNewIndex;
97+
98+
this->beginInsertRows(QModelIndex(), index, index + len - 1);
99+
this->mValues.resizeForOverwrite(this->mValues.length() + len);
100+
iter = this->mValues.begin() + index; // invalidated
101+
std::move_backward(iter, this->mValues.end() - len, this->mValues.end());
102+
iter = std::copy(startNewIter, newIter, iter);
103+
this->endInsertRows();
104+
}
105+
} else {
106+
++iter;
107+
++newIter;
108+
}
109+
}
110+
}
111+
112+
void ScriptModel::setValues(const QVariantList& newValues) {
113+
if (newValues == this->mValues) return;
114+
this->updateValuesUnique(newValues);
115+
emit this->valuesChanged();
116+
}
117+
118+
qint32 ScriptModel::rowCount(const QModelIndex& parent) const {
119+
if (parent != QModelIndex()) return 0;
120+
return static_cast<qint32>(this->mValues.length());
121+
}
122+
123+
QVariant ScriptModel::data(const QModelIndex& index, qint32 role) const {
124+
if (role != Qt::UserRole) return QVariant();
125+
return this->mValues.at(index.row());
126+
}
127+
128+
QHash<int, QByteArray> ScriptModel::roleNames() const { return {{Qt::UserRole, "modelData"}}; }

src/core/scriptmodel.hpp

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#pragma once
2+
3+
#include <qabstractitemmodel.h>
4+
#include <qcontainerfwd.h>
5+
#include <qproperty.h>
6+
#include <qqmlintegration.h>
7+
#include <qtmetamacros.h>
8+
9+
///! QML model reflecting a javascript expression
10+
/// ScriptModel is a QML [Data Model] that generates model operations based on changes
11+
/// to a javascript expression attached to @@values.
12+
///
13+
/// ### When should I use this
14+
/// ScriptModel should be used when you would otherwise use a javascript expression as a model,
15+
/// [QAbstractItemModel] is accepted, and the data is likely to change over the lifetime of the program.
16+
///
17+
/// When directly using a javascript expression as a model, types like @@QtQuick.Repeater or @@QtQuick.ListView
18+
/// will destroy all created delegates, and re-create the entire list. In the case of @@QtQuick.ListView this
19+
/// will also prevent animations from working. If you wrap your expression with ScriptModel, only new items
20+
/// will be created, and ListView animations will work as expected.
21+
///
22+
/// ### Example
23+
/// ```qml
24+
/// // Will cause all delegates to be re-created every time filterText changes.
25+
/// @@QtQuick.Repeater {
26+
/// model: myList.filter(entry => entry.name.startsWith(filterText))
27+
/// delegate: // ...
28+
/// }
29+
///
30+
/// // Will add and remove delegates only when required.
31+
/// @@QtQuick.Repeater {
32+
/// model: ScriptModel {
33+
/// values: myList.filter(entry => entry.name.startsWith(filterText))
34+
/// }
35+
///
36+
/// delegate: // ...
37+
/// }
38+
/// ```
39+
class ScriptModel: public QAbstractListModel {
40+
Q_OBJECT;
41+
/// The list of values to reflect in the model.
42+
/// > [!WARNING] ScriptModel currently only works with lists of *unique* values.
43+
/// > There must not be any duplicates in the given list, or behavior of the model is undefined.
44+
///
45+
/// > [!TIP] @@ObjectModel$s supplied by Quickshell types will only contain unique values,
46+
/// > and can be used like so:
47+
/// >
48+
/// > ```qml
49+
/// > ScriptModel {
50+
/// > values: DesktopEntries.applications.values.filter(...)
51+
/// > }
52+
/// > ```
53+
/// >
54+
/// > Note that we are using @@DesktopEntries.values because it will cause @@ScriptModel.values
55+
/// > to receive an update on change.
56+
Q_PROPERTY(QVariantList values READ values WRITE setValues NOTIFY valuesChanged);
57+
QML_ELEMENT;
58+
59+
public:
60+
[[nodiscard]] const QVariantList& values() const { return this->mValues; }
61+
void setValues(const QVariantList& newValues);
62+
63+
[[nodiscard]] qint32 rowCount(const QModelIndex& parent) const override;
64+
[[nodiscard]] QVariant data(const QModelIndex& index, qint32 role) const override;
65+
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
66+
67+
signals:
68+
void valuesChanged();
69+
70+
private:
71+
QVariantList mValues;
72+
73+
void updateValuesUnique(const QVariantList& newValues);
74+
};

src/core/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ endfunction()
66

77
qs_test(transformwatcher transformwatcher.cpp)
88
qs_test(ringbuffer ringbuf.cpp)
9+
qs_test(scriptmodel scriptmodel.cpp)

0 commit comments

Comments
 (0)