1 | /* |
2 | * Copyright 2006-2007 Aaron Seigo <aseigo@kde.org> |
3 | * |
4 | * This program is free software; you can redistribute it and/or modify |
5 | * it under the terms of the GNU Library General Public License as |
6 | * published by the Free Software Foundation; either version 2, or |
7 | * (at your option) any later version. |
8 | * |
9 | * This program is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | * GNU General Public License for more details |
13 | * |
14 | * You should have received a copy of the GNU Library General Public |
15 | * License along with this program; if not, write to the |
16 | * Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | */ |
19 | |
20 | #include "runnercontext.h" |
21 | |
22 | #include <cmath> |
23 | |
24 | #include <QReadWriteLock> |
25 | |
26 | #include <QDir> |
27 | #include <QFile> |
28 | #include <QFileInfo> |
29 | #include <QSharedData> |
30 | |
31 | #include <kcompletion.h> |
32 | #include <kconfiggroup.h> |
33 | #include <kdebug.h> |
34 | #include <kmimetype.h> |
35 | #include <kshell.h> |
36 | #include <kstandarddirs.h> |
37 | #include <kurl.h> |
38 | #include <kprotocolinfo.h> |
39 | |
40 | #include "abstractrunner.h" |
41 | #include "querymatch.h" |
42 | |
43 | //#define LOCK_FOR_READ(d) if (d->policy == Shared) { d->lock.lockForRead(); } |
44 | //#define LOCK_FOR_WRITE(d) if (d->policy == Shared) { d->lock.lockForWrite(); } |
45 | //#define UNLOCK(d) if (d->policy == Shared) { d->lock.unlock(); } |
46 | |
47 | #define LOCK_FOR_READ(d) d->lock.lockForRead(); |
48 | #define LOCK_FOR_WRITE(d) d->lock.lockForWrite(); |
49 | #define UNLOCK(d) d->lock.unlock(); |
50 | |
51 | namespace Plasma |
52 | { |
53 | |
54 | /* |
55 | Corrects the case of the last component in a path (e.g. /usr/liB -> /usr/lib) |
56 | path: The path to be processed. |
57 | correctCasePath: The corrected-case path |
58 | mustBeDir: Tells whether the last component is a folder or doesn't matter |
59 | Returns true on success and false on error, in case of error, correctCasePath is not modified |
60 | */ |
61 | bool correctLastComponentCase(const QString &path, QString &correctCasePath, const bool mustBeDir) |
62 | { |
63 | //kDebug() << "Correcting " << path; |
64 | |
65 | // If the file already exists then no need to search for it. |
66 | if (QFile::exists(path)) { |
67 | correctCasePath = path; |
68 | //kDebug() << "Correct path is" << correctCasePath; |
69 | return true; |
70 | } |
71 | |
72 | const QFileInfo pathInfo(path); |
73 | |
74 | const QDir fileDir = pathInfo.dir(); |
75 | //kDebug() << "Directory is" << fileDir; |
76 | |
77 | const QString filename = pathInfo.fileName(); |
78 | //kDebug() << "Filename is" << filename; |
79 | |
80 | //kDebug() << "searching for a" << (mustBeDir ? "directory" : "directory/file"); |
81 | |
82 | const QStringList matchingFilenames = fileDir.entryList(QStringList(filename), |
83 | mustBeDir ? QDir::Dirs : QDir::NoFilter); |
84 | |
85 | if (matchingFilenames.empty()) { |
86 | //kDebug() << "No matches found!!\n"; |
87 | return false; |
88 | } else { |
89 | /*if (matchingFilenames.size() > 1) { |
90 | kDebug() << "Found multiple matches!!\n"; |
91 | }*/ |
92 | |
93 | if (fileDir.path().endsWith(QDir::separator())) { |
94 | correctCasePath = fileDir.path() + matchingFilenames[0]; |
95 | } else { |
96 | correctCasePath = fileDir.path() + QDir::separator() + matchingFilenames[0]; |
97 | } |
98 | |
99 | //kDebug() << "Correct path is" << correctCasePath; |
100 | return true; |
101 | } |
102 | } |
103 | |
104 | /* |
105 | Corrects the case of a path (e.g. /uSr/loCAL/bIN -> /usr/local/bin) |
106 | path: The path to be processed. |
107 | corrected: The corrected-case path |
108 | Returns true on success and false on error, in case of error, corrected is not modified |
109 | */ |
110 | bool correctPathCase(const QString& path, QString &corrected) |
111 | { |
112 | // early exit check |
113 | if (QFile::exists(path)) { |
114 | corrected = path; |
115 | return true; |
116 | } |
117 | |
118 | // path components |
119 | QStringList components = QString(path).split(QDir::separator()); |
120 | |
121 | if (components.size() < 1) { |
122 | return false; |
123 | } |
124 | |
125 | const bool mustBeDir = components.back().isEmpty(); |
126 | |
127 | //kDebug() << "Components are" << components; |
128 | |
129 | if (mustBeDir) { |
130 | components.pop_back(); |
131 | } |
132 | |
133 | if (components.isEmpty()) { |
134 | return true; |
135 | } |
136 | |
137 | QString correctPath; |
138 | const unsigned initialComponents = components.size(); |
139 | for (unsigned i = 0; i < initialComponents - 1; i ++) { |
140 | const QString tmp = components[0] + QDir::separator() + components[1]; |
141 | |
142 | if (!correctLastComponentCase(tmp, correctPath, components.size() > 2 || mustBeDir)) { |
143 | //kDebug() << "search was not successful"; |
144 | return false; |
145 | } |
146 | |
147 | components.removeFirst(); |
148 | components[0] = correctPath; |
149 | } |
150 | |
151 | corrected = correctPath; |
152 | return true; |
153 | } |
154 | |
155 | class RunnerContextPrivate : public QSharedData |
156 | { |
157 | public: |
158 | RunnerContextPrivate(RunnerContext *context) |
159 | : QSharedData(), |
160 | type(RunnerContext::UnknownType), |
161 | q(context), |
162 | singleRunnerQueryMode(false) |
163 | { |
164 | } |
165 | |
166 | RunnerContextPrivate(const RunnerContextPrivate &p) |
167 | : QSharedData(), |
168 | launchCounts(p.launchCounts), |
169 | type(RunnerContext::None), |
170 | q(p.q), |
171 | singleRunnerQueryMode(false) |
172 | { |
173 | //kDebug() << "¿¿¿¿¿¿¿¿¿¿¿¿¿¿¿¿¿boo yeah" << type; |
174 | } |
175 | |
176 | ~RunnerContextPrivate() |
177 | { |
178 | } |
179 | |
180 | /** |
181 | * Determines type of query |
182 | && |
183 | */ |
184 | void determineType() |
185 | { |
186 | // NOTE! this method must NEVER be called from |
187 | // code that may be running in multiple threads |
188 | // with the same data. |
189 | type = RunnerContext::UnknownType; |
190 | QString path = QDir::cleanPath(KShell::tildeExpand(term)); |
191 | |
192 | int space = path.indexOf(' '); |
193 | if (!KStandardDirs::findExe(path.left(space)).isEmpty()) { |
194 | // it's a shell command if there's a space because that implies |
195 | // that it has arguments! |
196 | type = (space > 0) ? RunnerContext::ShellCommand : |
197 | RunnerContext::Executable; |
198 | } else { |
199 | KUrl url(term); |
200 | // check for a normal URL first |
201 | //kDebug() << url << KProtocolInfo::protocolClass(url.protocol()) << url.hasHost() << |
202 | // url.host() << url.isLocalFile() << path << path.indexOf('/'); |
203 | const bool hasProtocol = !url.protocol().isEmpty(); |
204 | const bool isLocalProtocol = KProtocolInfo::protocolClass(url.protocol()) == ":local" ; |
205 | if (hasProtocol && |
206 | ((!isLocalProtocol && url.hasHost()) || |
207 | (isLocalProtocol && url.protocol() != "file" ))) { |
208 | // we either have a network protocol with a host, so we can show matches for it |
209 | // or we have a non-file url that may be local so a host isn't required |
210 | type = RunnerContext::NetworkLocation; |
211 | } else if (isLocalProtocol) { |
212 | // at this point in the game, we assume we have a path, |
213 | // but if a path doesn't have any slashes |
214 | // it's too ambiguous to be sure we're in a filesystem context |
215 | path = QDir::cleanPath(url.toLocalFile()); |
216 | //kDebug( )<< "slash check" << path; |
217 | if (hasProtocol || ((path.indexOf('/') != -1 || path.indexOf('\\') != -1))) { |
218 | QString correctCasePath; |
219 | if (correctPathCase(path, correctCasePath)) { |
220 | path = correctCasePath; |
221 | QFileInfo info(path); |
222 | //kDebug( )<< "correct cas epath is" << correctCasePath << info.isSymLink() << |
223 | // info.isDir() << info.isFile(); |
224 | |
225 | if (info.isSymLink()) { |
226 | path = info.canonicalFilePath(); |
227 | info = QFileInfo(path); |
228 | } |
229 | if (info.isDir()) { |
230 | type = RunnerContext::Directory; |
231 | mimeType = "inode/folder" ; |
232 | } else if (info.isFile()) { |
233 | type = RunnerContext::File; |
234 | KMimeType::Ptr mimeTypePtr = KMimeType::findByPath(path); |
235 | if (mimeTypePtr) { |
236 | mimeType = mimeTypePtr->name(); |
237 | } |
238 | } |
239 | } |
240 | } |
241 | } |
242 | } |
243 | |
244 | //kDebug() << "term2type" << term << type; |
245 | } |
246 | |
247 | void invalidate() |
248 | { |
249 | q = &s_dummyContext; |
250 | } |
251 | |
252 | QReadWriteLock lock; |
253 | QList<QueryMatch> matches; |
254 | QMap<QString, const QueryMatch*> matchesById; |
255 | QHash<QString, int> launchCounts; |
256 | QString term; |
257 | QString mimeType; |
258 | RunnerContext::Type type; |
259 | RunnerContext * q; |
260 | static RunnerContext s_dummyContext; |
261 | bool singleRunnerQueryMode; |
262 | }; |
263 | |
264 | RunnerContext RunnerContextPrivate::s_dummyContext; |
265 | |
266 | RunnerContext::RunnerContext(QObject *parent) |
267 | : QObject(parent), |
268 | d(new RunnerContextPrivate(this)) |
269 | { |
270 | } |
271 | |
272 | //copy ctor |
273 | RunnerContext::RunnerContext(RunnerContext &other, QObject *parent) |
274 | : QObject(parent) |
275 | { |
276 | LOCK_FOR_READ(other.d) |
277 | d = other.d; |
278 | UNLOCK(other.d) |
279 | } |
280 | |
281 | RunnerContext::~RunnerContext() |
282 | { |
283 | } |
284 | |
285 | RunnerContext &RunnerContext::operator=(const RunnerContext &other) |
286 | { |
287 | if (this->d == other.d) { |
288 | return *this; |
289 | } |
290 | |
291 | QExplicitlySharedDataPointer<Plasma::RunnerContextPrivate> oldD = d; |
292 | LOCK_FOR_WRITE(d) |
293 | LOCK_FOR_READ(other.d) |
294 | d = other.d; |
295 | UNLOCK(other.d) |
296 | UNLOCK(oldD) |
297 | return *this; |
298 | } |
299 | |
300 | void RunnerContext::reset() |
301 | { |
302 | LOCK_FOR_WRITE(d); |
303 | // We will detach if we are a copy of someone. But we will reset |
304 | // if we are the 'main' context others copied from. Resetting |
305 | // one RunnerContext makes all the copies obsolete. |
306 | |
307 | // We need to mark the q pointer of the detached RunnerContextPrivate |
308 | // as dirty on detach to avoid receiving results for old queries |
309 | d->invalidate(); |
310 | UNLOCK(d); |
311 | |
312 | d.detach(); |
313 | |
314 | // Now that we detached the d pointer we need to reset its q pointer |
315 | |
316 | d->q = this; |
317 | |
318 | // we still have to remove all the matches, since if the |
319 | // ref count was 1 (e.g. only the RunnerContext is using |
320 | // the dptr) then we won't get a copy made |
321 | if (!d->matches.isEmpty()) { |
322 | d->matchesById.clear(); |
323 | d->matches.clear(); |
324 | emit matchesChanged(); |
325 | } |
326 | |
327 | d->term.clear(); |
328 | d->mimeType.clear(); |
329 | d->type = UnknownType; |
330 | d->singleRunnerQueryMode = false; |
331 | //kDebug() << "match count" << d->matches.count(); |
332 | } |
333 | |
334 | void RunnerContext::setQuery(const QString &term) |
335 | { |
336 | reset(); |
337 | |
338 | if (term.isEmpty()) { |
339 | return; |
340 | } |
341 | |
342 | d->term = term; |
343 | d->determineType(); |
344 | } |
345 | |
346 | QString RunnerContext::query() const |
347 | { |
348 | // the query term should never be set after |
349 | // a search starts. in fact, reset() ensures this |
350 | // and setQuery(QString) calls reset() |
351 | return d->term; |
352 | } |
353 | |
354 | RunnerContext::Type RunnerContext::type() const |
355 | { |
356 | return d->type; |
357 | } |
358 | |
359 | QString RunnerContext::mimeType() const |
360 | { |
361 | return d->mimeType; |
362 | } |
363 | |
364 | bool RunnerContext::isValid() const |
365 | { |
366 | // if our qptr is dirty, we aren't useful anymore |
367 | LOCK_FOR_READ(d) |
368 | const bool valid = (d->q != &(d->s_dummyContext)); |
369 | UNLOCK(d) |
370 | return valid; |
371 | } |
372 | |
373 | bool RunnerContext::addMatches(const QString &term, const QList<QueryMatch> &matches) |
374 | { |
375 | Q_UNUSED(term) |
376 | |
377 | if (matches.isEmpty() || !isValid()) { |
378 | //Bail out if the query is empty or the qptr is dirty |
379 | return false; |
380 | } |
381 | |
382 | LOCK_FOR_WRITE(d) |
383 | foreach (QueryMatch match, matches) { |
384 | // Give previously launched matches a slight boost in relevance |
385 | // The boost smoothly saturates to 0.5; |
386 | if (int count = d->launchCounts.value(match.id())) { |
387 | match.setRelevance(match.relevance() + 0.5 * (1-exp(-count*0.3))); |
388 | } |
389 | |
390 | d->matches.append(match); |
391 | #ifndef NDEBUG |
392 | if (d->matchesById.contains(match.id())) { |
393 | kDebug() << "Duplicate match id " << match.id() << "from" << match.runner()->name(); |
394 | } |
395 | #endif |
396 | d->matchesById.insert(match.id(), &d->matches.at(d->matches.size() - 1)); |
397 | } |
398 | UNLOCK(d); |
399 | //kDebug()<< "add matches"; |
400 | // A copied searchContext may share the d pointer, |
401 | // we always want to sent the signal of the object that created |
402 | // the d pointer |
403 | emit d->q->matchesChanged(); |
404 | |
405 | return true; |
406 | } |
407 | |
408 | bool RunnerContext::addMatch(const QString &term, const QueryMatch &match) |
409 | { |
410 | Q_UNUSED(term) |
411 | |
412 | if (!isValid()) { |
413 | // Bail out if the qptr is dirty |
414 | return false; |
415 | } |
416 | |
417 | QueryMatch m(match); // match must be non-const to modify relevance |
418 | |
419 | LOCK_FOR_WRITE(d) |
420 | |
421 | if (int count = d->launchCounts.value(m.id())) { |
422 | m.setRelevance(m.relevance() + 0.05 * count); |
423 | } |
424 | |
425 | d->matches.append(m); |
426 | d->matchesById.insert(m.id(), &d->matches.at(d->matches.size() - 1)); |
427 | UNLOCK(d); |
428 | //kDebug()<< "added match" << match->text(); |
429 | emit d->q->matchesChanged(); |
430 | |
431 | return true; |
432 | } |
433 | |
434 | bool RunnerContext::removeMatches(const QStringList matchIdList) |
435 | { |
436 | if (!isValid()) { |
437 | return false; |
438 | } |
439 | |
440 | QStringList presentMatchIdList; |
441 | QList<const QueryMatch*> presentMatchList; |
442 | |
443 | LOCK_FOR_READ(d) |
444 | foreach(const QString &matchId, matchIdList) { |
445 | const QueryMatch* match = d->matchesById.value(matchId, 0); |
446 | if (match) { |
447 | presentMatchList << match; |
448 | presentMatchIdList << matchId; |
449 | } |
450 | } |
451 | UNLOCK(d) |
452 | |
453 | if (presentMatchIdList.isEmpty()) { |
454 | return false; |
455 | } |
456 | |
457 | LOCK_FOR_WRITE(d) |
458 | foreach(const QueryMatch *match, presentMatchList) { |
459 | d->matches.removeAll(*match); |
460 | } |
461 | foreach(const QString &matchId, presentMatchIdList) { |
462 | d->matchesById.remove(matchId); |
463 | } |
464 | UNLOCK(d) |
465 | |
466 | emit d->q->matchesChanged(); |
467 | |
468 | return true; |
469 | } |
470 | |
471 | bool RunnerContext::removeMatch(const QString matchId) |
472 | { |
473 | if (!isValid()) { |
474 | return false; |
475 | } |
476 | LOCK_FOR_READ(d) |
477 | const QueryMatch* match = d->matchesById.value(matchId, 0); |
478 | UNLOCK(d) |
479 | if (!match) { |
480 | return false; |
481 | } |
482 | LOCK_FOR_WRITE(d) |
483 | d->matches.removeAll(*match); |
484 | d->matchesById.remove(matchId); |
485 | UNLOCK(d) |
486 | emit d->q->matchesChanged(); |
487 | |
488 | return true; |
489 | } |
490 | |
491 | bool RunnerContext::removeMatches(Plasma::AbstractRunner *runner) |
492 | { |
493 | if (!isValid()) { |
494 | return false; |
495 | } |
496 | |
497 | QList<QueryMatch> presentMatchList; |
498 | |
499 | LOCK_FOR_READ(d) |
500 | foreach(const QueryMatch &match, d->matches) { |
501 | if (match.runner() == runner) { |
502 | presentMatchList << match; |
503 | } |
504 | } |
505 | UNLOCK(d) |
506 | |
507 | if (presentMatchList.isEmpty()) { |
508 | return false; |
509 | } |
510 | |
511 | LOCK_FOR_WRITE(d) |
512 | foreach (const QueryMatch &match, presentMatchList) { |
513 | d->matchesById.remove(match.id()); |
514 | d->matches.removeAll(match); |
515 | } |
516 | UNLOCK(d) |
517 | |
518 | emit d->q->matchesChanged(); |
519 | return true; |
520 | } |
521 | |
522 | QList<QueryMatch> RunnerContext::matches() const |
523 | { |
524 | LOCK_FOR_READ(d) |
525 | QList<QueryMatch> matches = d->matches; |
526 | UNLOCK(d); |
527 | return matches; |
528 | } |
529 | |
530 | QueryMatch RunnerContext::match(const QString &id) const |
531 | { |
532 | LOCK_FOR_READ(d) |
533 | const QueryMatch *match = d->matchesById.value(id, 0); |
534 | UNLOCK(d) |
535 | |
536 | if (match) { |
537 | return *match; |
538 | } |
539 | |
540 | return QueryMatch(0); |
541 | } |
542 | |
543 | void RunnerContext::setSingleRunnerQueryMode(bool enabled) |
544 | { |
545 | d->singleRunnerQueryMode = enabled; |
546 | } |
547 | |
548 | bool RunnerContext::singleRunnerQueryMode() const |
549 | { |
550 | return d->singleRunnerQueryMode; |
551 | } |
552 | |
553 | void RunnerContext::restore(const KConfigGroup &config) |
554 | { |
555 | const QStringList cfgList = config.readEntry("LaunchCounts" , QStringList()); |
556 | |
557 | const QRegExp r("(\\d*) (.*)" ); |
558 | foreach (const QString& entry, cfgList) { |
559 | r.indexIn(entry); |
560 | int count = r.cap(1).toInt(); |
561 | QString id = r.cap(2); |
562 | d->launchCounts[id] = count; |
563 | } |
564 | } |
565 | |
566 | void RunnerContext::save(KConfigGroup &config) |
567 | { |
568 | QStringList countList; |
569 | |
570 | typedef QHash<QString, int>::const_iterator Iterator; |
571 | Iterator end = d->launchCounts.constEnd(); |
572 | for (Iterator i = d->launchCounts.constBegin(); i != end; ++i) { |
573 | countList << QString("%2 %1" ).arg(i.key()).arg(i.value()); |
574 | } |
575 | |
576 | config.writeEntry("LaunchCounts" , countList); |
577 | config.sync(); |
578 | } |
579 | |
580 | void RunnerContext::run(const QueryMatch &match) |
581 | { |
582 | ++d->launchCounts[match.id()]; |
583 | match.run(*this); |
584 | } |
585 | |
586 | } // Plasma namespace |
587 | |
588 | #include "runnercontext.moc" |
589 | |