sourcesmodel.cpp 11.6 KB
Newer Older
Vishesh Handa's avatar
Vishesh Handa committed
1
/*
2
3
 * This file is part of the KDE Milou Project
 * Copyright (C) 2013  Vishesh Handa <me@vhanda.in>
Vishesh Handa's avatar
Vishesh Handa committed
4
 *
5
6
7
8
9
10
11
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) version 3, or any
 * later version accepted by the membership of KDE e.V. (or its
 * successor approved by the membership of KDE e.V.), which shall
 * act as a proxy defined in Section 6 of version 3 of the license.
Vishesh Handa's avatar
Vishesh Handa committed
12
 *
13
 * This library is distributed in the hope that it will be useful,
Vishesh Handa's avatar
Vishesh Handa committed
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
Vishesh Handa's avatar
Vishesh Handa committed
17
 *
18
19
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
Vishesh Handa's avatar
Vishesh Handa committed
20
21
22
23
 *
 */

#include "sourcesmodel.h"
24

25
#include <KConfig>
26
27
#include <KDirWatch>
#include <KSharedConfig>
Vishesh Handa's avatar
Vishesh Handa committed
28

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
29
#include <QAction>
Laurent Montel's avatar
Laurent Montel committed
30
#include <QModelIndex>
31
#include <QMimeData>
Laurent Montel's avatar
Laurent Montel committed
32
33
#include <QSet>

34
35
using namespace Milou;

Vishesh Handa's avatar
Vishesh Handa committed
36
SourcesModel::SourcesModel(QObject* parent)
37
38
    : QAbstractListModel(parent)
    , m_size(0)
Vishesh Handa's avatar
Vishesh Handa committed
39
{
Vishesh Handa's avatar
Vishesh Handa committed
40
41
42
    m_manager = new Plasma::RunnerManager(this);
    connect(m_manager, SIGNAL(matchesChanged(QList<Plasma::QueryMatch>)),
            this, SLOT(slotMatchesChanged(QList<Plasma::QueryMatch>)));
43
44

    KDirWatch* watch = KDirWatch::self();
45
46
    connect(watch, &KDirWatch::created, this, &SourcesModel::slotSettingsFileChanged);
    connect(watch, &KDirWatch::dirty, this, &SourcesModel::slotSettingsFileChanged);
47
    watch->addFile(QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("krunnerrc")));
48
49
50
51

    m_resetTimer.setSingleShot(true);
    m_resetTimer.setInterval(500);
    connect(&m_resetTimer, SIGNAL(timeout()), this, SLOT(slotResetTimeout()));
Vishesh Handa's avatar
Vishesh Handa committed
52
53
54
55
56
57
}

SourcesModel::~SourcesModel()
{
}

58
59
60
61
QHash<int, QByteArray> SourcesModel::roleNames() const
{
    QHash<int, QByteArray> roles = QAbstractListModel::roleNames();
    roles.insert(TypeRole, "type");
62
    roles.insert(SubtextRole, "subtext");
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
63
    roles.insert(ActionsRole, "actions");
64
    roles.insert(DuplicateRole, "isDuplicate");
65
66
67
68
69
70
71
    roles.insert(PreviewTypeRole, "previewType");
    roles.insert(PreviewUrlRole, "previewUrl");
    roles.insert(PreviewLabelRole, "previewLabel");

    return roles;
}

Vishesh Handa's avatar
Vishesh Handa committed
72
Plasma::QueryMatch SourcesModel::fetchMatch(int row) const
73
{
Vishesh Handa's avatar
Vishesh Handa committed
74
    foreach (const QString& type, m_types) {
75
        const TypeData data = m_matches.value(type);
76
        if (row < data.shown.size()) {
Vishesh Handa's avatar
Vishesh Handa committed
77
            return data.shown[row];
78
79
        }
        else {
80
            row -= data.shown.size();
81
82
83
84
            if (row < 0) {
                break;
            }
        }
85
86
    }

Friedrich W. H. Kossebau's avatar
Friedrich W. H. Kossebau committed
87
    return Plasma::QueryMatch(nullptr);
Vishesh Handa's avatar
Vishesh Handa committed
88
89
}

Vishesh Handa's avatar
Vishesh Handa committed
90
91
92
93
94
QVariant SourcesModel::data(const QModelIndex& index, int role) const
{
    if (!index.isValid())
        return QVariant();

95
    if (index.row() >= m_size)
Vishesh Handa's avatar
Vishesh Handa committed
96
97
        return QVariant();

Vishesh Handa's avatar
Vishesh Handa committed
98
99
    Plasma::QueryMatch m = fetchMatch(index.row());
    Q_ASSERT(m.runner());
100

Vishesh Handa's avatar
Vishesh Handa committed
101
102
    switch(role) {
        case Qt::DisplayRole:
Vishesh Handa's avatar
Vishesh Handa committed
103
            return m.text();
Vishesh Handa's avatar
Vishesh Handa committed
104

Vishesh Handa's avatar
Vishesh Handa committed
105
        case Qt::DecorationRole:
106
107
108
109
            if (!m.iconName().isEmpty()) {
                return m.iconName();
            }

110
            return m.icon();
Vishesh Handa's avatar
Vishesh Handa committed
111
112

        case TypeRole:
Vishesh Handa's avatar
Vishesh Handa committed
113
            return m.matchCategory();
114

115
116
117
        case SubtextRole:
            return m.subtext();

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
        case ActionsRole: {
            const auto &actions = m_manager->actionsForMatch(m);
            if (actions.isEmpty()) {
                return QVariantList();
            }

            QVariantList actionsList;
            actionsList.reserve(actions.size());

            for (QAction *action : actions) {
                actionsList.append(QVariant::fromValue(action));
            }

            return actionsList;
        }
133
134
        case DuplicateRole:
            return m_duplicates.value(m.text());
135

Vishesh Handa's avatar
Vishesh Handa committed
136
            /*
137
138
139
140
141
        case PreviewTypeRole:
            return m.previewType();

        case PreviewUrlRole:
            return m.previewUrl();
142
143
144

        case PreviewLabelRole:
            return m.previewLabel();
Vishesh Handa's avatar
Vishesh Handa committed
145
            */
Vishesh Handa's avatar
Vishesh Handa committed
146
147
148
149
150
151
152
153
154
155
    }

    return QVariant();
}

int SourcesModel::rowCount(const QModelIndex& parent) const
{
    if (parent.isValid())
        return 0;

156
    return m_size;
Vishesh Handa's avatar
Vishesh Handa committed
157
158
}

159
QString SourcesModel::queryString() const
Vishesh Handa's avatar
Vishesh Handa committed
160
161
162
163
{
    return m_queryString;
}

164
165
166
167
168
int SourcesModel::queryLimit() const
{
    return m_queryLimit;
}

169
170
171
172
173
174
175
QString SourcesModel::runner() const
{
    return m_runner;
}

void SourcesModel::setRunner(const QString& runner)
{
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
    if (m_runner != runner) {
        m_runner = runner;

        m_manager->setSingleModeRunnerId(m_runner);
        m_manager->setSingleMode(!m_runner.isEmpty());

        emit runnerChanged();
    }
}

QString SourcesModel::runnerName() const
{
    auto *singleRunner = m_manager->singleModeRunner();
    if (!singleRunner) {
        return QString();
    }

    return singleRunner->name();
}

QIcon SourcesModel::runnerIcon() const
{
    auto *singleRunner = m_manager->singleModeRunner();
    if (!singleRunner) {
        return QIcon();
    }

    return singleRunner->icon();
204
205
}

206
207
208
void SourcesModel::setQueryLimit(int limit)
{
    m_queryLimit = limit;
Vishesh Handa's avatar
Vishesh Handa committed
209
    /*
210
211
    foreach (AbstractSource* source, m_sources)
        source->setQueryLimit(limit);
Vishesh Handa's avatar
Vishesh Handa committed
212
    */
213
214
}

Vishesh Handa's avatar
Vishesh Handa committed
215
216
217
218
219
220
void SourcesModel::setQueryString(const QString& str)
{
    if (str.trimmed() == m_queryString.trimmed()) {
        return;
    }

221
    m_queryString = str;
222
223
224
225
    if (m_queryString.isEmpty()) {
        clear();
        return;
    }
226

227
228
229
230
231
    // We avoid clearing the model instantly, and instead wait for the results
    // to show up, and only then do we clear the model. In the event
    // where there are no results, we wait for a predefined time before
    // clearing the model
    m_resetTimer.start();
232
233
234

    m_modelPopulated = false;
    m_manager->launchQuery(m_queryString, m_runner);
235
236
237
238
239
}

void SourcesModel::slotResetTimeout()
{
    if (!m_modelPopulated) {
240
241
242
243
244
245
        // The old items are still shown, get rid of them
        beginResetModel();
        m_matches.clear();
        m_size = 0;
        m_duplicates.clear();
        endResetModel();
246
    }
Vishesh Handa's avatar
Vishesh Handa committed
247
248
}

249
void SourcesModel::slotMatchesChanged(const QList<Plasma::QueryMatch>& l)
Vishesh Handa's avatar
Vishesh Handa committed
250
{
251
252
253
254
255
256
    // We do reset handling ourselves, so ignore clears if the reset timer
    // is supposed to handle them (see setQueryString)
    if(l.length() == 0 && m_resetTimer.isActive() && !m_modelPopulated) {
        return;
    }

257
258
259
260
    beginResetModel();
    m_matches.clear();
    m_size = 0;
    m_types.clear();
261
    m_duplicates.clear();
262

263
    QList<Plasma::QueryMatch> list(l);
264
    std::sort(list.begin(), list.end());
265

266
267
    for (auto it = list.crbegin(), end = list.crend(); it != end; ++it) {
        slotMatchAdded(*it);
Vishesh Handa's avatar
Vishesh Handa committed
268
    }
269
270
271
272
273

    // Sort the result types. We give the results which contain the query
    // text in the user visible string a higher preference than the ones
    // that do not
    // The rest are given the same preference as given by the runners.
274
275
276
    const QString simplifiedQuery = m_queryString.simplified();
    const auto words = simplifiedQuery.splitRef(QLatin1Char(' '), QString::SkipEmptyParts);

277
    QSet<QString> higherTypes;
278
    foreach (const QString &type, m_types) {
279
        const TypeData td = m_matches.value(type);
280
281

        for (const Plasma::QueryMatch &match : td.shown) {
282
283
284
            const QString text = match.text().simplified();
            bool containsAll = true;

285
286
            for (const auto &word : words) {
                if (!text.contains(word, Qt::CaseInsensitive)) {
287
288
289
290
291
292
293
294
295
                    containsAll = false;
                    break;
                }
            }

            // Maybe we should be giving it a higher type based on the number of matched
            // words in the text?
            if (containsAll) {
                higherTypes << match.matchCategory();
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
            }
        }
    }

    auto sortFunc = [&](const QString& l, const QString& r) {
        bool lHigher = higherTypes.contains(l);
        bool rHigher = higherTypes.contains(r);

        if (lHigher == rHigher) {
            return false;
        }
        else {
            return lHigher;
        }
    };
311
    std::stable_sort(m_types.begin(), m_types.end(), sortFunc);
312

313
    m_modelPopulated = true;
314
    endResetModel();
Vishesh Handa's avatar
Vishesh Handa committed
315
316
}

317
318
319
320
//
// Tries to make sure that all the types have the same number
// of visible items
//
Vishesh Handa's avatar
Vishesh Handa committed
321
void SourcesModel::slotMatchAdded(const Plasma::QueryMatch& m)
Vishesh Handa's avatar
Vishesh Handa committed
322
{
323
324
325
    if (m_queryString.isEmpty())
        return;

326
    QString matchType = m.matchCategory();
327

328
    if (!m_types.contains(matchType)) {
329
        m_types << matchType;
330
    }
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349

    if (m_size == m_queryLimit) {
        int maxShownItems = 0;
        QString maxShownType;
        foreach (const QString& type, m_types) {
            TypeData data = m_matches.value(type);
            if (data.shown.size() >= maxShownItems) {
                maxShownItems = data.shown.size();
                maxShownType = type;
            }
        }

        if (maxShownType == matchType) {
            m_matches[matchType].hidden.append(m);
            return;
        }

        // Remove the last shown row from maxShownType
        // and add it to matchType
Vishesh Handa's avatar
Vishesh Handa committed
350
        Plasma::QueryMatch transferMatch = m_matches[maxShownType].shown.takeLast();
351
        m_matches[maxShownType].hidden.append(transferMatch);
352
        m_size--;
353
        m_duplicates[transferMatch.text()]--;
354
355
    }

356
357
358
    m_matches[matchType].shown.append(m);
    m_size++;
    m_duplicates[m.text()]++;
Vishesh Handa's avatar
Vishesh Handa committed
359
360
}

361
362
363
364
365
366
367
368
369
void SourcesModel::slotSettingsFileChanged(const QString &path)
{
    if (!path.endsWith(QLatin1String("krunnerrc"))) {
        return;
    }

    reloadConfiguration();
}

Vishesh Handa's avatar
Vishesh Handa committed
370
371
372
373
void SourcesModel::clear()
{
    beginResetModel();
    m_matches.clear();
374
    m_size = 0;
375
    m_duplicates.clear();
376
    m_queryString.clear();
Vishesh Handa's avatar
Vishesh Handa committed
377
    m_manager->reset();
378
    m_manager->matchSessionComplete();
Vishesh Handa's avatar
Vishesh Handa committed
379
380
381
    endResetModel();
}

382
bool SourcesModel::run(int index)
383
{
Vishesh Handa's avatar
Vishesh Handa committed
384
    Plasma::QueryMatch match = fetchMatch(index);
385
386
387
388
389
390
391
392
393
    Q_ASSERT(match.runner());

    if (match.type() == Plasma::QueryMatch::InformationalMatch) {
        QString info = match.data().toString();
        int editPos = info.length();

        if (!info.isEmpty()) {
            // FIXME: pretty lame way to decide if this is a query prototype
            // Copied from kde4 krunner interface.cpp
Friedrich W. H. Kossebau's avatar
Friedrich W. H. Kossebau committed
394
            if (match.runner() == nullptr) {
395
396
397
398
399
400
401
402
403
404
405
406
407
408
                // lame way of checking to see if this is a Help Button generated match!
                int index = info.indexOf(QStringLiteral(":q:"));

                if (index != -1) {
                    editPos = index;
                    info.replace(QStringLiteral(":q:"), QString());
                }
            }

            emit updateSearchTerm(info, editPos);
            return false;
        }
    }

Vishesh Handa's avatar
Vishesh Handa committed
409
    m_manager->run(match);
410
    return true;
411
}
412

Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
bool SourcesModel::runAction(int index, int actionIndex)
{
    Plasma::QueryMatch match = fetchMatch(index);
    Q_ASSERT(match.runner());

    const auto &actions = m_manager->actionsForMatch(match);
    if (actionIndex < 0 || actionIndex >= actions.count()) {
        return false;
    }

    QAction *action = actions.at(actionIndex);
    match.setSelectedAction(action);
    m_manager->run(match);
    return true;
}

429
430
void SourcesModel::reloadConfiguration()
{
431
    KSharedConfig::openConfig(QStringLiteral("krunnerrc"))->reparseConfiguration();
432
433
    m_manager->reloadConfiguration();
}
434
435
436
437
438
439
440
441
442
443
444
445

QMimeData *SourcesModel::getMimeData(int index) const
{
    Plasma::QueryMatch match = fetchMatch(index);
    Q_ASSERT(match.runner());

    // we're returning a parent-less QObject from a Q_INVOKABLE
    // which means the QML engine will take care of deleting it eventually
    QMimeData *mimeData = m_manager->mimeDataForMatch(match);

    return mimeData;
}