1 /*
2 SPDX-FileCopyrightText: 2012 Aurélien Gâteau <agateau@kde.org>
3 SPDX-FileCopyrightText: 2014 Eike Hein <hein@kde.org>
4
5 SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8 #include "runnermodel.h"
9 #include "runnermatchesmodel.h"
10
11 #include <QSet>
12
13 #include <KLocalizedString>
14 #include <KRunner/AbstractRunner>
15 #include <KRunner/RunnerManager>
16
RunnerModel(QObject * parent)17 RunnerModel::RunnerModel(QObject *parent)
18 : QAbstractListModel(parent)
19 , m_favoritesModel(nullptr)
20 , m_appletInterface(nullptr)
21 , m_runnerManager(nullptr)
22 , m_mergeResults(false)
23 , m_deleteWhenEmpty(false)
24 {
25 m_queryTimer.setSingleShot(true);
26 m_queryTimer.setInterval(10);
27 connect(&m_queryTimer, &QTimer::timeout, this, &RunnerModel::startQuery);
28 }
29
~RunnerModel()30 RunnerModel::~RunnerModel()
31 {
32 }
33
roleNames() const34 QHash<int, QByteArray> RunnerModel::roleNames() const
35 {
36 return {{Qt::DisplayRole, "display"}};
37 }
38
favoritesModel() const39 AbstractModel *RunnerModel::favoritesModel() const
40 {
41 return m_favoritesModel;
42 }
43
setFavoritesModel(AbstractModel * model)44 void RunnerModel::setFavoritesModel(AbstractModel *model)
45 {
46 if (m_favoritesModel != model) {
47 m_favoritesModel = model;
48
49 clear();
50
51 if (!m_query.isEmpty()) {
52 m_queryTimer.start();
53 }
54
55 emit favoritesModelChanged();
56 }
57 }
58
appletInterface() const59 QObject *RunnerModel::appletInterface() const
60 {
61 return m_appletInterface;
62 }
63
setAppletInterface(QObject * appletInterface)64 void RunnerModel::setAppletInterface(QObject *appletInterface)
65 {
66 if (m_appletInterface != appletInterface) {
67 m_appletInterface = appletInterface;
68
69 clear();
70
71 if (!m_query.isEmpty()) {
72 m_queryTimer.start();
73 }
74
75 emit appletInterfaceChanged();
76 }
77 }
78
deleteWhenEmpty() const79 bool RunnerModel::deleteWhenEmpty() const
80 {
81 return m_deleteWhenEmpty;
82 }
83
setDeleteWhenEmpty(bool deleteWhenEmpty)84 void RunnerModel::setDeleteWhenEmpty(bool deleteWhenEmpty)
85 {
86 if (m_deleteWhenEmpty != deleteWhenEmpty) {
87 m_deleteWhenEmpty = deleteWhenEmpty;
88
89 clear();
90
91 if (!m_query.isEmpty()) {
92 m_queryTimer.start();
93 }
94
95 emit deleteWhenEmptyChanged();
96 }
97 }
98
mergeResults() const99 bool RunnerModel::mergeResults() const
100 {
101 return m_mergeResults;
102 }
103
setMergeResults(bool merge)104 void RunnerModel::setMergeResults(bool merge)
105 {
106 if (m_mergeResults != merge) {
107 m_mergeResults = merge;
108
109 clear();
110
111 if (!m_query.isEmpty()) {
112 m_queryTimer.start();
113 }
114
115 emit mergeResultsChanged();
116 }
117 }
118
data(const QModelIndex & index,int role) const119 QVariant RunnerModel::data(const QModelIndex &index, int role) const
120 {
121 if (!index.isValid() || index.row() >= m_models.count()) {
122 return QVariant();
123 }
124
125 if (role == Qt::DisplayRole) {
126 return m_models.at(index.row())->name();
127 }
128
129 return QVariant();
130 }
131
rowCount(const QModelIndex & parent) const132 int RunnerModel::rowCount(const QModelIndex &parent) const
133 {
134 return parent.isValid() ? 0 : m_models.count();
135 }
136
count() const137 int RunnerModel::count() const
138 {
139 return rowCount();
140 }
141
modelForRow(int row)142 QObject *RunnerModel::modelForRow(int row)
143 {
144 if (row < 0 || row >= m_models.count()) {
145 return nullptr;
146 }
147
148 return m_models.at(row);
149 }
150
runners() const151 QStringList RunnerModel::runners() const
152 {
153 return m_runners;
154 }
155
setRunners(const QStringList & runners)156 void RunnerModel::setRunners(const QStringList &runners)
157 {
158 if (QSet<QString>(runners.cbegin(), runners.cend()) != QSet<QString>(m_runners.cbegin(), m_runners.cend())) {
159 m_runners = runners;
160
161 if (m_runnerManager) {
162 m_runnerManager->setAllowedRunners(runners);
163 }
164
165 emit runnersChanged();
166 }
167 }
168
query() const169 QString RunnerModel::query() const
170 {
171 return m_query;
172 }
173
setQuery(const QString & query)174 void RunnerModel::setQuery(const QString &query)
175 {
176 if (m_query != query) {
177 m_query = query;
178
179 m_queryTimer.start();
180
181 emit queryChanged();
182 }
183 }
184
startQuery()185 void RunnerModel::startQuery()
186 {
187 if (m_query.isEmpty()) {
188 clear();
189 }
190
191 if (m_query.isEmpty() && m_runnerManager) {
192 return;
193 }
194
195 createManager();
196
197 m_runnerManager->launchQuery(m_query);
198 }
199
matchesChanged(const QList<Plasma::QueryMatch> & matches)200 void RunnerModel::matchesChanged(const QList<Plasma::QueryMatch> &matches)
201 {
202 // Group matches by runner.
203 // We do not use a QMultiHash here because it keeps values in LIFO order, while we want FIFO.
204 QHash<QString, QList<Plasma::QueryMatch>> matchesForRunner;
205
206 for (const Plasma::QueryMatch &match : matches) {
207 auto it = matchesForRunner.find(match.runner()->id());
208
209 if (it == matchesForRunner.end()) {
210 it = matchesForRunner.insert(match.runner()->id(), QList<Plasma::QueryMatch>());
211 }
212
213 it.value().append(match);
214 }
215
216 // Sort matches for all runners in descending order, note the reverse iterators. This allows the best
217 // match to win whilest preserving order between runners.
218 for (auto &list : matchesForRunner) {
219 std::sort(list.rbegin(), list.rend());
220 }
221
222 if (m_mergeResults) {
223 RunnerMatchesModel *matchesModel = nullptr;
224
225 if (m_models.isEmpty()) {
226 matchesModel = new RunnerMatchesModel(QString(), i18n("Search results"), m_runnerManager, this);
227
228 beginInsertRows(QModelIndex(), 0, 0);
229 m_models.append(matchesModel);
230 endInsertRows();
231 emit countChanged();
232 } else {
233 matchesModel = m_models.at(0);
234 }
235
236 QList<Plasma::QueryMatch> matches;
237 // To preserve the old behavior when allowing all runners we use static sorting
238 const static QStringList runnerIds = {
239 QStringLiteral("desktopsessions"),
240 QStringLiteral("services"),
241 QStringLiteral("places"),
242 QStringLiteral("PowerDevil"),
243 QStringLiteral("calculator"),
244 QStringLiteral("unitconverter"),
245 QStringLiteral("shell"),
246 QStringLiteral("bookmarks"),
247 QStringLiteral("recentdocuments"),
248 QStringLiteral("locations"),
249 };
250 if (m_runners.isEmpty()) {
251 const auto baloo = matchesForRunner.take(QStringLiteral("baloosearch"));
252 const auto appstream = matchesForRunner.take(QStringLiteral("krunner_appstream"));
253 for (const QString &runnerId : runnerIds) {
254 matches.append(matchesForRunner.take(runnerId));
255 }
256 for (const auto &match : matchesForRunner) {
257 matches.append(match);
258 }
259 matches.append(baloo);
260 matches.append(appstream);
261 } else {
262 for (const QString &runnerId : qAsConst(m_runners)) {
263 matches.append(matchesForRunner.take(runnerId));
264 }
265 }
266
267 matchesModel->setMatches(matches);
268
269 return;
270 }
271
272 // Assign matches to existing models. If there is no match for a model, delete it.
273 for (int row = m_models.count() - 1; row >= 0; --row) {
274 RunnerMatchesModel *matchesModel = m_models.at(row);
275 QList<Plasma::QueryMatch> matches = matchesForRunner.take(matchesModel->runnerId());
276
277 if (m_deleteWhenEmpty && matches.isEmpty()) {
278 beginRemoveRows(QModelIndex(), row, row);
279 m_models.removeAt(row);
280 delete matchesModel;
281 endRemoveRows();
282 emit countChanged();
283 } else {
284 matchesModel->setMatches(matches);
285 }
286 }
287
288 // At this point, matchesForRunner contains only matches for runners which
289 // do not have a model yet. Create new models for them.
290 if (!matchesForRunner.isEmpty()) {
291 auto it = matchesForRunner.constBegin();
292 auto end = matchesForRunner.constEnd();
293 int appendCount = 0;
294
295 for (; it != end; ++it) {
296 QList<Plasma::QueryMatch> matches = it.value();
297 Q_ASSERT(!matches.isEmpty());
298 RunnerMatchesModel *matchesModel = new RunnerMatchesModel(it.key(), matches.first().runner()->name(), m_runnerManager, this);
299 matchesModel->setMatches(matches);
300
301 if (it.key() == QLatin1String("services")) {
302 beginInsertRows(QModelIndex(), 0, 0);
303 m_models.prepend(matchesModel);
304 endInsertRows();
305 emit countChanged();
306 } else {
307 m_models.append(matchesModel);
308 ++appendCount;
309 }
310 }
311
312 if (appendCount > 0) {
313 beginInsertRows(QModelIndex(), rowCount() - appendCount, rowCount() - 1);
314 endInsertRows();
315 emit countChanged();
316 }
317 }
318 }
319
createManager()320 void RunnerModel::createManager()
321 {
322 if (!m_runnerManager) {
323 m_runnerManager = new Plasma::RunnerManager(QStringLiteral("krunnerrc"), this);
324 if (m_runners.isEmpty()) {
325 m_runnerManager->enableKNotifyPluginWatcher();
326 } else {
327 m_runnerManager->setAllowedRunners(m_runners);
328 }
329 connect(m_runnerManager, &Plasma::RunnerManager::matchesChanged, this, &RunnerModel::matchesChanged);
330 }
331 }
332
clear()333 void RunnerModel::clear()
334 {
335 if (m_runnerManager) {
336 m_runnerManager->reset();
337 m_runnerManager->matchSessionComplete();
338 }
339
340 if (m_models.isEmpty()) {
341 return;
342 }
343
344 beginResetModel();
345
346 qDeleteAll(m_models);
347 m_models.clear();
348
349 endResetModel();
350
351 emit countChanged();
352 }
353