| 1 | /* |
| 2 | * SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@broulik.de> |
| 3 | * |
| 4 | * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL |
| 5 | * |
| 6 | */ |
| 7 | |
| 8 | #include "runnerresultsmodel_p.h" |
| 9 | |
| 10 | #include <QSet> |
| 11 | |
| 12 | #include <KRunner/RunnerManager> |
| 13 | |
| 14 | #include "resultsmodel.h" |
| 15 | |
| 16 | namespace KRunner |
| 17 | { |
| 18 | RunnerResultsModel::RunnerResultsModel(const KConfigGroup &configGroup, const KConfigGroup &stateConfigGroup, QObject *parent) |
| 19 | : QAbstractItemModel(parent) |
| 20 | { |
| 21 | // Invalid groups are passed in to avoid unneeded overloads and such |
| 22 | setRunnerManager(configGroup.isValid() && stateConfigGroup.isValid() ? new RunnerManager(configGroup, stateConfigGroup, this) : new RunnerManager(this)); |
| 23 | } |
| 24 | |
| 25 | KRunner::QueryMatch RunnerResultsModel::fetchMatch(const QModelIndex &idx) const |
| 26 | { |
| 27 | const QString category = m_categories.value(i: int(idx.internalId() - 1)); |
| 28 | return m_matches.value(key: category).value(i: idx.row()); |
| 29 | } |
| 30 | |
| 31 | void RunnerResultsModel::onMatchesChanged(const QList<KRunner::QueryMatch> &matches) |
| 32 | { |
| 33 | // Build the list of new categories and matches |
| 34 | QSet<QString> newCategories; |
| 35 | // here we use QString as key since at this point we don't care about the order |
| 36 | // of categories but just what matches we have for each one. |
| 37 | // Below when we populate the actual m_matches we'll make sure to keep the order |
| 38 | // of existing categories to avoid pointless model changes. |
| 39 | QHash<QString /*category*/, QList<KRunner::QueryMatch>> newMatches; |
| 40 | for (const auto &match : matches) { |
| 41 | const QString category = match.matchCategory(); |
| 42 | newCategories.insert(value: category); |
| 43 | newMatches[category].append(t: match); |
| 44 | } |
| 45 | |
| 46 | // Get rid of all categories that are no longer present |
| 47 | auto it = m_categories.begin(); |
| 48 | while (it != m_categories.end()) { |
| 49 | const int categoryNumber = int(std::distance(first: m_categories.begin(), last: it)); |
| 50 | |
| 51 | if (!newCategories.contains(value: *it)) { |
| 52 | beginRemoveRows(parent: QModelIndex(), first: categoryNumber, last: categoryNumber); |
| 53 | m_matches.remove(key: *it); |
| 54 | it = m_categories.erase(pos: it); |
| 55 | endRemoveRows(); |
| 56 | } else { |
| 57 | ++it; |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | // Update the existing categories by adding/removing new/removed rows and |
| 62 | // updating changed ones |
| 63 | for (auto it = m_categories.constBegin(); it != m_categories.constEnd(); ++it) { |
| 64 | Q_ASSERT(newCategories.contains(*it)); |
| 65 | |
| 66 | const int categoryNumber = int(std::distance(first: m_categories.constBegin(), last: it)); |
| 67 | const QModelIndex categoryIdx = index(row: categoryNumber, column: 0); |
| 68 | |
| 69 | // don't use operator[] as to not insert an empty list |
| 70 | // TODO why? shouldn't m_categories and m_matches be in sync? |
| 71 | auto oldCategoryIt = m_matches.find(key: *it); |
| 72 | Q_ASSERT(oldCategoryIt != m_matches.end()); |
| 73 | |
| 74 | auto &oldMatchesInCategory = *oldCategoryIt; |
| 75 | const auto newMatchesInCategory = newMatches.value(key: *it); |
| 76 | |
| 77 | Q_ASSERT(!oldMatchesInCategory.isEmpty()); |
| 78 | Q_ASSERT(!newMatches.isEmpty()); |
| 79 | |
| 80 | // Emit a change for all existing matches if any of them changed |
| 81 | // TODO only emit a change for the ones that changed |
| 82 | bool emitDataChanged = false; |
| 83 | |
| 84 | const int oldCount = oldMatchesInCategory.count(); |
| 85 | const int newCount = newMatchesInCategory.count(); |
| 86 | |
| 87 | const int countCeiling = qMin(a: oldCount, b: newCount); |
| 88 | |
| 89 | for (int i = 0; i < countCeiling; ++i) { |
| 90 | auto &oldMatch = oldMatchesInCategory[i]; |
| 91 | if (oldMatch != newMatchesInCategory.at(i)) { |
| 92 | oldMatch = newMatchesInCategory.at(i); |
| 93 | emitDataChanged = true; |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | // Now that the source data has been updated, emit the data changes we noted down earlier |
| 98 | if (emitDataChanged) { |
| 99 | Q_EMIT dataChanged(topLeft: index(row: 0, column: 0, parent: categoryIdx), bottomRight: index(row: countCeiling - 1, column: 0, parent: categoryIdx)); |
| 100 | } |
| 101 | |
| 102 | // Signal insertions for any new items |
| 103 | if (newCount > oldCount) { |
| 104 | beginInsertRows(parent: categoryIdx, first: oldCount, last: newCount - 1); |
| 105 | oldMatchesInCategory = newMatchesInCategory; |
| 106 | endInsertRows(); |
| 107 | } else if (newCount < oldCount) { |
| 108 | beginRemoveRows(parent: categoryIdx, first: newCount, last: oldCount - 1); |
| 109 | oldMatchesInCategory = newMatchesInCategory; |
| 110 | endRemoveRows(); |
| 111 | } |
| 112 | |
| 113 | // Remove it from the "new" categories so in the next step we can add all genuinely new categories in one go |
| 114 | newCategories.remove(value: *it); |
| 115 | } |
| 116 | |
| 117 | // Finally add all the new categories |
| 118 | if (!newCategories.isEmpty()) { |
| 119 | beginInsertRows(parent: QModelIndex(), first: m_categories.count(), last: m_categories.count() + newCategories.count() - 1); |
| 120 | |
| 121 | for (const QString &newCategory : newCategories) { |
| 122 | const auto matchesInNewCategory = newMatches.value(key: newCategory); |
| 123 | |
| 124 | m_matches[newCategory] = matchesInNewCategory; |
| 125 | m_categories.append(t: newCategory); |
| 126 | } |
| 127 | |
| 128 | endInsertRows(); |
| 129 | } |
| 130 | |
| 131 | Q_ASSERT(m_categories.count() == m_matches.count()); |
| 132 | |
| 133 | m_hasMatches = !m_matches.isEmpty(); |
| 134 | |
| 135 | Q_EMIT matchesChanged(); |
| 136 | } |
| 137 | |
| 138 | QString RunnerResultsModel::queryString() const |
| 139 | { |
| 140 | return m_queryString; |
| 141 | } |
| 142 | |
| 143 | void RunnerResultsModel::setQueryString(const QString &queryString, const QString &runner) |
| 144 | { |
| 145 | // If our query and runner are the same we don't need to query again |
| 146 | if (m_queryString.trimmed() == queryString.trimmed() && m_prevRunner == runner) { |
| 147 | return; |
| 148 | } |
| 149 | |
| 150 | m_prevRunner = runner; |
| 151 | m_queryString = queryString; |
| 152 | m_hasMatches = false; |
| 153 | if (queryString.isEmpty()) { |
| 154 | clear(); |
| 155 | } else if (!queryString.trimmed().isEmpty()) { |
| 156 | m_manager->launchQuery(term: queryString, runnerId: runner); |
| 157 | } |
| 158 | Q_EMIT queryStringChanged(queryString); // NOLINT(readability-misleading-indentation) |
| 159 | } |
| 160 | |
| 161 | void RunnerResultsModel::clear() |
| 162 | { |
| 163 | m_manager->reset(); |
| 164 | m_manager->matchSessionComplete(); |
| 165 | |
| 166 | // When our session is over, the term is also no longer relevant |
| 167 | // If the same term is used again, the RunnerManager should be asked again |
| 168 | if (!m_queryString.isEmpty()) { |
| 169 | m_queryString.clear(); |
| 170 | Q_EMIT queryStringChanged(queryString: m_queryString); |
| 171 | } |
| 172 | |
| 173 | beginResetModel(); |
| 174 | m_categories.clear(); |
| 175 | m_matches.clear(); |
| 176 | endResetModel(); |
| 177 | |
| 178 | m_hasMatches = false; |
| 179 | } |
| 180 | |
| 181 | bool RunnerResultsModel::run(const QModelIndex &idx) |
| 182 | { |
| 183 | KRunner::QueryMatch match = fetchMatch(idx); |
| 184 | if (match.isValid() && match.isEnabled()) { |
| 185 | return m_manager->run(match); |
| 186 | } |
| 187 | return false; |
| 188 | } |
| 189 | |
| 190 | bool RunnerResultsModel::runAction(const QModelIndex &idx, int actionNumber) |
| 191 | { |
| 192 | KRunner::QueryMatch match = fetchMatch(idx); |
| 193 | if (!match.isValid() || !match.isEnabled()) { |
| 194 | return false; |
| 195 | } |
| 196 | |
| 197 | if (actionNumber < 0 || actionNumber >= match.actions().count()) { |
| 198 | return false; |
| 199 | } |
| 200 | |
| 201 | return m_manager->run(match, action: match.actions().at(i: actionNumber)); |
| 202 | } |
| 203 | |
| 204 | int RunnerResultsModel::columnCount(const QModelIndex &parent) const |
| 205 | { |
| 206 | Q_UNUSED(parent); |
| 207 | return 1; |
| 208 | } |
| 209 | |
| 210 | int RunnerResultsModel::rowCount(const QModelIndex &parent) const |
| 211 | { |
| 212 | if (parent.column() > 0) { |
| 213 | return 0; |
| 214 | } |
| 215 | |
| 216 | if (!parent.isValid()) { // root level |
| 217 | return m_categories.count(); |
| 218 | } |
| 219 | |
| 220 | if (parent.internalId()) { |
| 221 | return 0; |
| 222 | } |
| 223 | |
| 224 | const QString category = m_categories.value(i: parent.row()); |
| 225 | return m_matches.value(key: category).count(); |
| 226 | } |
| 227 | |
| 228 | QVariant RunnerResultsModel::data(const QModelIndex &index, int role) const |
| 229 | { |
| 230 | if (!index.isValid()) { |
| 231 | return QVariant(); |
| 232 | } |
| 233 | |
| 234 | if (index.internalId()) { // runner match |
| 235 | if (int(index.internalId() - 1) >= m_categories.count()) { |
| 236 | return QVariant(); |
| 237 | } |
| 238 | |
| 239 | KRunner::QueryMatch match = fetchMatch(idx: index); |
| 240 | if (!match.isValid()) { |
| 241 | return QVariant(); |
| 242 | } |
| 243 | |
| 244 | switch (role) { |
| 245 | case Qt::DisplayRole: |
| 246 | return match.text(); |
| 247 | case Qt::DecorationRole: |
| 248 | if (!match.iconName().isEmpty()) { |
| 249 | return match.iconName(); |
| 250 | } |
| 251 | return match.icon(); |
| 252 | case ResultsModel::CategoryRelevanceRole: |
| 253 | return match.categoryRelevance(); |
| 254 | case ResultsModel::RelevanceRole: |
| 255 | return match.relevance(); |
| 256 | case ResultsModel::IdRole: |
| 257 | return match.id(); |
| 258 | case ResultsModel::EnabledRole: |
| 259 | return match.isEnabled(); |
| 260 | case ResultsModel::CategoryRole: |
| 261 | return match.matchCategory(); |
| 262 | case ResultsModel::SubtextRole: |
| 263 | return match.subtext(); |
| 264 | case ResultsModel::UrlsRole: |
| 265 | return QVariant::fromValue(value: match.urls()); |
| 266 | case ResultsModel::MultiLineRole: |
| 267 | return match.isMultiLine(); |
| 268 | case ResultsModel::ActionsRole: { |
| 269 | const auto actions = match.actions(); |
| 270 | QVariantList actionsList; |
| 271 | actionsList.reserve(asize: actions.size()); |
| 272 | |
| 273 | for (const KRunner::Action &action : actions) { |
| 274 | actionsList.append(t: QVariant::fromValue(value: action)); |
| 275 | } |
| 276 | |
| 277 | return actionsList; |
| 278 | } |
| 279 | case ResultsModel::QueryMatchRole: |
| 280 | return QVariant::fromValue(value: match); |
| 281 | } |
| 282 | |
| 283 | return QVariant(); |
| 284 | } |
| 285 | |
| 286 | // category |
| 287 | if (index.row() >= m_categories.count()) { |
| 288 | return QVariant(); |
| 289 | } |
| 290 | |
| 291 | switch (role) { |
| 292 | case Qt::DisplayRole: |
| 293 | return m_categories.at(i: index.row()); |
| 294 | |
| 295 | case ResultsModel::FavoriteIndexRole: { |
| 296 | for (int i = 0; i < rowCount(parent: index); ++i) { |
| 297 | auto match = this->index(row: i, column: 0, parent: index).data(arole: ResultsModel::QueryMatchRole).value<KRunner::QueryMatch>(); |
| 298 | if (match.isValid()) { |
| 299 | const QString id = match.runner()->id(); |
| 300 | int idx = m_favoriteIds.indexOf(str: id); |
| 301 | return idx == -1 ? m_favoriteIds.size() : idx; |
| 302 | } |
| 303 | } |
| 304 | // Any match that is not a favorite will have a greater index than an actual favorite |
| 305 | return m_favoriteIds.size(); |
| 306 | } |
| 307 | // Returns the highest type/role within the group |
| 308 | case ResultsModel::CategoryRelevanceRole: { |
| 309 | int highestType = 0; |
| 310 | for (int i = 0; i < rowCount(parent: index); ++i) { |
| 311 | const int type = this->index(row: i, column: 0, parent: index).data(arole: ResultsModel::CategoryRelevanceRole).toInt(); |
| 312 | if (type > highestType) { |
| 313 | highestType = type; |
| 314 | } |
| 315 | } |
| 316 | return highestType; |
| 317 | } |
| 318 | case ResultsModel::RelevanceRole: { |
| 319 | qreal highestRelevance = 0.0; |
| 320 | for (int i = 0; i < rowCount(parent: index); ++i) { |
| 321 | const qreal relevance = this->index(row: i, column: 0, parent: index).data(arole: ResultsModel::RelevanceRole).toReal(); |
| 322 | if (relevance > highestRelevance) { |
| 323 | highestRelevance = relevance; |
| 324 | } |
| 325 | } |
| 326 | return highestRelevance; |
| 327 | } |
| 328 | } |
| 329 | |
| 330 | return QVariant(); |
| 331 | } |
| 332 | |
| 333 | QModelIndex RunnerResultsModel::index(int row, int column, const QModelIndex &parent) const |
| 334 | { |
| 335 | if (row < 0 || column != 0) { |
| 336 | return QModelIndex(); |
| 337 | } |
| 338 | |
| 339 | if (parent.isValid()) { |
| 340 | const QString category = m_categories.value(i: parent.row()); |
| 341 | const auto matches = m_matches.value(key: category); |
| 342 | if (row < matches.count()) { |
| 343 | return createIndex(arow: row, acolumn: column, aid: int(parent.row() + 1)); |
| 344 | } |
| 345 | |
| 346 | return QModelIndex(); |
| 347 | } |
| 348 | |
| 349 | if (row < m_categories.count()) { |
| 350 | return createIndex(arow: row, acolumn: column, adata: nullptr); |
| 351 | } |
| 352 | |
| 353 | return QModelIndex(); |
| 354 | } |
| 355 | |
| 356 | QModelIndex RunnerResultsModel::parent(const QModelIndex &child) const |
| 357 | { |
| 358 | if (child.internalId()) { |
| 359 | return createIndex(arow: int(child.internalId() - 1), acolumn: 0, adata: nullptr); |
| 360 | } |
| 361 | |
| 362 | return QModelIndex(); |
| 363 | } |
| 364 | |
| 365 | KRunner::RunnerManager *RunnerResultsModel::runnerManager() const |
| 366 | { |
| 367 | return m_manager; |
| 368 | } |
| 369 | |
| 370 | void RunnerResultsModel::setRunnerManager(KRunner::RunnerManager *manager) |
| 371 | { |
| 372 | disconnect(receiver: m_manager); |
| 373 | m_manager = manager; |
| 374 | |
| 375 | connect(sender: m_manager, signal: &RunnerManager::matchesChanged, context: this, slot: &RunnerResultsModel::onMatchesChanged); |
| 376 | connect(sender: m_manager, signal: &RunnerManager::requestUpdateQueryString, context: this, slot: &RunnerResultsModel::queryStringChangeRequested); |
| 377 | Q_EMIT runnerManagerChanged(); |
| 378 | } |
| 379 | } |
| 380 | |
| 381 | #include "moc_runnerresultsmodel_p.cpp" |
| 382 | |