Skip to content

Commit 08836ca

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

File tree

8 files changed

+431
-2
lines changed

8 files changed

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