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