KRunner

runnercontext.cpp
1 /*
2  SPDX-FileCopyrightText: 2006-2007 Aaron Seigo <[email protected]>
3 
4  SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "runnercontext.h"
8 
9 #include <cmath>
10 
11 #include <QDir>
12 #include <QFile>
13 #include <QFileInfo>
14 #include <QMimeDatabase>
15 #include <QReadWriteLock>
16 #include <QRegularExpression>
17 #include <QSharedData>
18 #include <QStandardPaths>
19 #include <QUrl>
20 
21 #include <KConfigGroup>
22 #include <KShell>
23 
24 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
25 #include <KProtocolInfo>
26 #endif
27 
28 #include "abstractrunner.h"
29 #include "krunner_debug.h"
30 #include "querymatch.h"
31 
32 #define LOCK_FOR_READ(d) d->lock.lockForRead();
33 #define LOCK_FOR_WRITE(d) d->lock.lockForWrite();
34 #define UNLOCK(d) d->lock.unlock();
35 
36 namespace Plasma
37 {
38 class RunnerContextPrivate : public QSharedData
39 {
40 public:
41  explicit RunnerContextPrivate(RunnerContext *context)
42  : QSharedData()
43  , type(RunnerContext::UnknownType)
44  , q(context)
45  {
46  }
47 
48  explicit RunnerContextPrivate(const RunnerContextPrivate &p)
49  : QSharedData()
50  , launchCounts(p.launchCounts)
51  , type(RunnerContext::None)
52  , q(p.q)
53  {
54  }
55 
56  ~RunnerContextPrivate()
57  {
58  }
59 
60 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
61  /**
62  * Determines type of query
63  &&
64  */
65  void determineType()
66  {
67  // NOTE! this method must NEVER be called from
68  // code that may be running in multiple threads
69  // with the same data.
70  type = RunnerContext::UnknownType;
72 
73  int space = path.indexOf(QLatin1Char(' '));
75  // it's a shell command if there's a space because that implies
76  // that it has arguments!
77  type = (space > 0) ? RunnerContext::ShellCommand : RunnerContext::Executable;
78  } else {
79  QUrl url = QUrl::fromUserInput(term);
80 
81  // QUrl::fromUserInput assigns http to everything if it cannot match it to
82  // anything else. We do not want that.
83  if (url.scheme() == QLatin1String("http")) {
84  if (!term.startsWith(QLatin1String("http"))) {
85  url.setScheme(QString());
86  }
87  }
88 
89  const bool hasProtocol = !url.scheme().isEmpty();
90  const bool isLocalProtocol = !hasProtocol || KProtocolInfo::protocolClass(url.scheme()) == QLatin1String(":local");
91  if ((hasProtocol && ((!isLocalProtocol && !url.host().isEmpty()) || (isLocalProtocol && url.scheme() != QLatin1String("file"))))
92  || term.startsWith(QLatin1String("\\\\"))) {
93  // we either have a network protocol with a host, so we can show matches for it
94  // or we have a non-file url that may be local so a host isn't required
95  // or we have an UNC path (\\foo\bar)
96  type = RunnerContext::NetworkLocation;
97  } else if (isLocalProtocol) {
98  // at this point in the game, we assume we have a path,
99  // but if a path doesn't have any slashes
100  // it's too ambiguous to be sure we're in a filesystem context
101  path = !url.scheme().isEmpty() ? QDir::cleanPath(url.toLocalFile()) : path;
102  if ((path.indexOf(QLatin1Char('/')) != -1 || path.indexOf(QLatin1Char('\\')) != -1)) {
103  QFileInfo info(path);
104  if (info.isSymLink()) {
105  path = info.canonicalFilePath();
106  info = QFileInfo(path);
107  }
108  if (info.isDir()) {
109  type = RunnerContext::Directory;
110  mimeType = QStringLiteral("inode/folder");
111  } else if (info.isFile()) {
112  type = RunnerContext::File;
113  QMimeDatabase db;
114  QMimeType mime = db.mimeTypeForFile(path);
115  if (!mime.isDefault()) {
116  mimeType = mime.name();
117  }
118  }
119  }
120  }
121  }
122  }
123 #endif
124 
125  void invalidate()
126  {
127  q = &s_dummyContext;
128  }
129 
130 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 81)
131  QueryMatch *findMatchById(const QString &id)
132  {
133  QueryMatch *match = nullptr;
134  lock.lockForRead();
135  for (auto &matchEntry : matches) {
136  if (matchEntry.id() == id) {
137  match = &matchEntry;
138  break;
139  }
140  }
141  lock.unlock();
142  return match;
143  }
144 #endif
145 
146  void addMatch(const QueryMatch &match)
147  {
148  if (match.runner() && match.runner()->hasUniqueResults()) {
149  if (uniqueIds.contains(match.id())) {
150  const QueryMatch &existentMatch = uniqueIds.value(match.id());
151  if (existentMatch.runner() && existentMatch.runner()->hasWeakResults()) {
152  // There is an existing match with the same ID and we are allowed to replace it
153  matches.removeOne(existentMatch);
154  matches.append(match);
155  }
156  } else {
157  // There is no existing match with the same id
158  uniqueIds.insert(match.id(), match);
159  matches.append(match);
160  }
161  } else {
162  // Runner has the unique results property not set
163  matches.append(match);
164  }
165  }
166 
167  QReadWriteLock lock;
168  QList<QueryMatch> matches;
169  QHash<QString, int> launchCounts;
170  QString term;
172  QStringList enabledCategories;
173  RunnerContext::Type type;
174  RunnerContext *q;
175  static RunnerContext s_dummyContext;
176  bool singleRunnerQueryMode = false;
177  bool shouldIgnoreCurrentMatchForHistory = false;
178  QMap<QString, QueryMatch> uniqueIds;
179  QString requestedText;
180  int requestedCursorPosition = 0;
181 };
182 
183 RunnerContext RunnerContextPrivate::s_dummyContext;
184 
185 RunnerContext::RunnerContext(QObject *parent)
186  : QObject(parent)
187  , d(new RunnerContextPrivate(this))
188 {
189 }
190 
191 // copy ctor
192 RunnerContext::RunnerContext(RunnerContext &other, QObject *parent)
193  : QObject(parent)
194 {
195  LOCK_FOR_READ(other.d)
196  d = other.d;
197  UNLOCK(other.d)
198 }
199 
200 RunnerContext::~RunnerContext()
201 {
202 }
203 
205 {
206  if (this->d == other.d) {
207  return *this;
208  }
209 
211  LOCK_FOR_WRITE(d)
212  LOCK_FOR_READ(other.d)
213  d = other.d;
214  UNLOCK(other.d)
215  UNLOCK(oldD)
216  return *this;
217 }
218 
220 {
221  LOCK_FOR_WRITE(d);
222  // We will detach if we are a copy of someone. But we will reset
223  // if we are the 'main' context others copied from. Resetting
224  // one RunnerContext makes all the copies obsolete.
225 
226  // We need to mark the q pointer of the detached RunnerContextPrivate
227  // as dirty on detach to avoid receiving results for old queries
228  d->invalidate();
229  UNLOCK(d);
230 
231  d.detach();
232 
233  // Now that we detached the d pointer we need to reset its q pointer
234 
235  d->q = this;
236 
237  // we still have to remove all the matches, since if the
238  // ref count was 1 (e.g. only the RunnerContext is using
239  // the dptr) then we won't get a copy made
240  d->matches.clear();
241  d->term.clear();
242  Q_EMIT matchesChanged();
243 
244  d->mimeType.clear();
245  d->uniqueIds.clear();
246  d->type = UnknownType;
247  d->singleRunnerQueryMode = false;
248  d->shouldIgnoreCurrentMatchForHistory = false;
249 }
250 
252 {
253  if (!this->query().isEmpty()) {
254  reset();
255  }
256 
257  if (term.isEmpty()) {
258  return;
259  }
260 
261  d->requestedText.clear(); // Invalidate this field whenever the query changes
262  d->term = term;
263 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
264  d->determineType();
265 #endif
266 }
267 
269 {
270  // the query term should never be set after
271  // a search starts. in fact, reset() ensures this
272  // and setQuery(QString) calls reset()
273  return d->term;
274 }
275 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
276 
278 {
279  d->enabledCategories = categories;
280 }
281 #endif
282 
283 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
285 {
286  return d->enabledCategories;
287 }
288 #endif
289 
290 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
291 RunnerContext::Type RunnerContext::type() const
292 {
293  return d->type;
294 }
295 #endif
296 
297 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
299 {
300  return d->mimeType;
301 }
302 #endif
303 
305 {
306  // if our qptr is dirty, we aren't useful anymore
307  LOCK_FOR_READ(d)
308  const bool valid = (d->q != &(d->s_dummyContext));
309  UNLOCK(d)
310  return valid;
311 }
312 
314 {
315  if (matches.isEmpty() || !isValid()) {
316  // Bail out if the query is empty or the qptr is dirty
317  return false;
318  }
319 
320  LOCK_FOR_WRITE(d)
321  for (QueryMatch match : matches) {
322  // Give previously launched matches a slight boost in relevance
323  // The boost smoothly saturates to 0.5;
324  if (int count = d->launchCounts.value(match.id())) {
325  match.setRelevance(match.relevance() + 0.5 * (1 - exp(-count * 0.3)));
326  }
327  d->addMatch(match);
328  }
329  UNLOCK(d);
330  // A copied searchContext may share the d pointer,
331  // we always want to sent the signal of the object that created
332  // the d pointer
333  Q_EMIT d->q->matchesChanged();
334 
335  return true;
336 }
337 
339 {
340  return addMatches({match});
341 }
342 
343 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 81)
345 {
346  bool success = false;
347  for (const QString &id : matchIdList) {
348  if (removeMatch(id)) {
349  success = true;
350  }
351  }
352 
353  return success;
354 }
355 #endif
356 
357 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 81)
359 {
360  if (!isValid()) {
361  return false;
362  }
363  LOCK_FOR_READ(d)
364  const QueryMatch *match = d->findMatchById(matchId);
365  UNLOCK(d)
366  if (!match) {
367  return false;
368  }
369  LOCK_FOR_WRITE(d)
370  d->matches.removeAll(*match);
371  UNLOCK(d)
372  Q_EMIT d->q->matchesChanged();
373 
374  return true;
375 }
376 #endif
377 
378 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 81)
380 {
381  if (!isValid()) {
382  return false;
383  }
384 
385  QList<QueryMatch> presentMatchList;
386 
387  LOCK_FOR_READ(d)
388  for (const QueryMatch &match : std::as_const(d->matches)) {
389  if (match.runner() == runner) {
390  presentMatchList << match;
391  }
392  }
393  UNLOCK(d)
394 
395  if (presentMatchList.isEmpty()) {
396  return false;
397  }
398 
399  LOCK_FOR_WRITE(d)
400  for (const QueryMatch &match : std::as_const(presentMatchList)) {
401  d->matches.removeAll(match);
402  }
403  UNLOCK(d)
404 
405  Q_EMIT d->q->matchesChanged();
406  return true;
407 }
408 #endif
409 
411 {
412  LOCK_FOR_READ(d)
413  QList<QueryMatch> matches = d->matches;
414  UNLOCK(d);
415  return matches;
416 }
417 
418 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 79)
420 {
421  LOCK_FOR_READ(d)
422  const QueryMatch *match = d->findMatchById(id);
423  UNLOCK(d)
424  return match ? *match : QueryMatch(nullptr);
425 }
426 #endif
427 
428 void RunnerContext::requestQueryStringUpdate(const QString &text, int cursorPosition) const
429 {
430  d->requestedText = text;
431  d->requestedCursorPosition = cursorPosition;
432 }
433 
435 {
436  d->singleRunnerQueryMode = enabled;
437 }
438 
440 {
441  return d->singleRunnerQueryMode;
442 }
443 
445 {
446  d->shouldIgnoreCurrentMatchForHistory = true;
447 }
448 
449 bool RunnerContext::shouldIgnoreCurrentMatchForHistory() const
450 {
451  return d->shouldIgnoreCurrentMatchForHistory;
452 }
453 
455 {
456  const QStringList cfgList = config.readEntry("LaunchCounts", QStringList());
457 
458  static const QRegularExpression re(QStringLiteral("(\\d*) (.+)"));
459  for (const QString &entry : cfgList) {
460  const QRegularExpressionMatch match = re.match(entry);
461  if (!match.hasMatch()) {
462  continue;
463  }
464  const int count = match.captured(1).toInt();
465  const QString id = match.captured(2);
466  d->launchCounts[id] = count;
467  }
468 }
469 
471 {
472  QStringList countList;
473 
474  typedef QHash<QString, int>::const_iterator Iterator;
475  Iterator end = d->launchCounts.constEnd();
476  for (Iterator i = d->launchCounts.constBegin(); i != end; ++i) {
477  countList << QStringLiteral("%2 %1").arg(i.key()).arg(i.value());
478  }
479 
480  config.writeEntry("LaunchCounts", countList);
481  config.sync();
482 }
483 
484 void RunnerContext::run(const QueryMatch &match)
485 {
486  ++d->launchCounts[match.id()];
487  match.run(*this);
488 }
489 
490 QString RunnerContext::requestedQueryString() const
491 {
492  return d->requestedText;
493 }
494 int RunnerContext::requestedCursorPosition() const
495 {
496  return d->requestedCursorPosition;
497 }
498 
499 } // Plasma namespace
500 
501 #include "moc_runnercontext.cpp"
QList< QueryMatch > matches() const
Retrieves all available matches for the current search term.
AbstractRunner * runner() const
Definition: querymatch.cpp:149
void setEnabledCategories(const QStringList &categories)
Sets the list of enabled categories.
Type type() const
The type of item the search term might refer to.
Q_EMITQ_EMIT
Type type(const QSqlDatabase &db)
QString scheme() const const
void ignoreCurrentMatchForHistory() const
Set this to true in the AbstractRunner::run method to prevent the entry from being saved to the histo...
A match returned by an AbstractRunner in response to a given RunnerContext.
Definition: querymatch.h:34
KCALUTILS_EXPORT QString mimeType()
An abstract base class for Plasma Runner plugins.
QueryMatch match(const QString &id) const
Retrieves a match by id.
void save(KConfigGroup &config)
void setScheme(const QString &scheme)
RunnerContext & operator=(const RunnerContext &other)
Assignment operator.
QString findExecutable(const QString &executableName, const QStringList &paths)
bool addMatches(const QList< QueryMatch > &matches)
Appends lists of matches to the list of matches.
void setRelevance(qreal relevance)
Sets the relevance of this action for the search it was created for.
Definition: querymatch.cpp:139
void requestQueryStringUpdate(const QString &text, int cursorPosition) const
Request that KRunner updates the query string and stasy open, even after running a match.
void setSingleRunnerQueryMode(bool enabled)
Sets single runner query mode.
QString mimeType() const
The mimetype that the search term refers to, if discoverable.
bool removeMatches(const QStringList matchIdList)
Removes lists of matches from the existing list of matches.
void run(const RunnerContext &context) const
Requests this match to activae using the given context.
Definition: querymatch.cpp:334
bool isEmpty() const const
QString id() const
Definition: querymatch.cpp:100
KCOREADDONS_EXPORT QString tildeExpand(const QString &path)
bool isEmpty() const const
QString toLocalFile() const const
void reset()
Resets the search term for this object.
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
KSharedConfigPtr config()
QRegularExpressionMatch match(const QString &subject, int offset, QRegularExpression::MatchType matchType, QRegularExpression::MatchOptions matchOptions) const const
void restore(const KConfigGroup &config)
Sets the launch counts for the associated match ids.
QStringList enabledCategories() const
A list of categories of which results should be returned.
QString cleanPath(const QString &path)
QString host(QUrl::ComponentFormattingOptions options) const const
QString path(const QString &relativePath)
QString left(int n) const const
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
qreal relevance() const
The relevance of this action to the search.
Definition: querymatch.cpp:144
bool removeMatch(const QString matchId)
Removes a match from the existing list of matches.
void run(const QueryMatch &match)
Run a match using the information from this context.
QMimeType mimeTypeForFile(const QString &fileName, QMimeDatabase::MatchMode mode) const const
void setQuery(const QString &term)
Sets the query term for this object and attempts to determine the type of the search.
The RunnerContext class provides information related to a search, including the search term,...
Definition: runnercontext.h:31
QString query() const
bool addMatch(const QueryMatch &match)
Appends a match to the existing list of matches.
bool singleRunnerQueryMode() const
QUrl fromUserInput(const QString &userInput)
static QString protocolClass(const QString &protocol)
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sat Dec 2 2023 03:51:00 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.