File indexing completed on 2024-04-14 05:45:37

0001 /*
0002  * Copyright 2010-2012 Bart Kroon <bart@tarmack.eu>
0003  * Copyright 2012, 2013 Martin Sandsmark <martin.sandsmark@kde.org>
0004  * 
0005  * Redistribution and use in source and binary forms, with or without
0006  * modification, are permitted provided that the following conditions
0007  * are met:
0008  * 
0009  * 1. Redistributions of source code must retain the above copyright
0010  *   notice, this list of conditions and the following disclaimer.
0011  * 2. Redistributions in binary form must reproduce the above copyright
0012  *   notice, this list of conditions and the following disclaimer in the
0013  *   documentation and/or other materials provided with the distribution.
0014  * 
0015  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
0016  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
0017  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
0018  * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
0019  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
0020  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
0021  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
0022  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
0023  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
0024  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0025  */
0026 
0027 #include "Mangonel.h"
0028 
0029 #include <QCryptographicHash>
0030 #include <QGuiApplication>
0031 #include <QVBoxLayout>
0032 #include <QDesktopWidget>
0033 #include <QDBusInterface>
0034 #include <QIcon>
0035 #include <QMenu>
0036 #include <QTextDocument>
0037 #include <QClipboard>
0038 #include <QSettings>
0039 #include <QDateTime>
0040 #include <QQmlEngine>
0041 #include <QElapsedTimer>
0042 
0043 #include <KLocalizedString>
0044 #include <KNotification>
0045 #include <KNotifyConfigWidget>
0046 
0047 #ifdef KGLOBALACCEL_FOUND
0048 #include <KGlobalAccel>
0049 #endif
0050 
0051 #include "Config.h"
0052 //Include the providers.
0053 #include "providers/Applications.h"
0054 #include "providers/Paths.h"
0055 #include "providers/Shell.h"
0056 #include "providers/Calculator.h"
0057 #include "providers/Units.h"
0058 #include "globalshortcut/qglobalshortcut.h"
0059 
0060 #include <QDebug>
0061 
0062 #include <unistd.h>
0063 
0064 static const char *s_shortcutKey = "globalshortcut";
0065 
0066 static QHash<QString, Popularity> getRecursivePopularity(QSettings &settings, const QString &path = QString())
0067 {
0068     QHash<QString, Popularity> ret;
0069     for (const QString &key : settings.childGroups()) {
0070         settings.beginGroup(key);
0071         const QString program = path + key;
0072         Popularity pop;
0073         pop.count = settings.value("launches").toLongLong();
0074         pop.lastUse = settings.value("lastUse").toLongLong();
0075         pop.matchStrings = settings.value("matchStrings").toStringList();
0076 
0077         if (pop.count || pop.lastUse || !pop.matchStrings.isEmpty()) {
0078             ret.insert(program, pop);
0079         }
0080 
0081         for (const QString &child : settings.childGroups()) {
0082             settings.beginGroup(child);
0083             const QString childPath = (path.isEmpty() ? "/" : "") + path + key + "/" + child + "/";
0084             QHash<QString, Popularity> children = getRecursivePopularity(settings, childPath);
0085             QHash<QString, Popularity>::const_iterator i = children.constBegin();
0086             while (i != children.constEnd()) {
0087                 ret.insert(i.key(), i.value());
0088                 ++i;
0089             }
0090             settings.endGroup();
0091         }
0092 
0093         settings.endGroup();
0094     }
0095     return ret;
0096 }
0097 
0098 Mangonel::Mangonel()
0099 {
0100     // Setup our global shortcut.
0101     m_actionShow = new QAction(i18n("Show Mangonel"), this);
0102     m_actionShow->setObjectName(QString("show"));
0103 
0104 
0105     QSettings settings;
0106 
0107     QKeySequence shortcut = QKeySequence::fromString(
0108             settings.value(QLatin1String(s_shortcutKey)).toString()
0109         );
0110 
0111 #ifdef KGLOBALACCEL_FOUND
0112     if (shortcut.isEmpty()) {
0113         // KGlobalAccel is broken, so migrate config
0114         QList<QKeySequence> shortcuts = KGlobalAccel::self()->shortcut(m_actionShow);
0115         if (!shortcuts.isEmpty() && !shortcuts.first().isEmpty()) {
0116             qDebug() << "Migrating from kglobalaccel";
0117             KGlobalAccel::self()->removeAllShortcuts(m_actionShow);
0118             settings.setValue(QLatin1String(s_shortcutKey), shortcuts.first().toString());
0119         }
0120     }
0121 #endif
0122 
0123     if (shortcut.isEmpty()) {
0124         shortcut = QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_Space);
0125         settings.setValue(QLatin1String(s_shortcutKey), shortcut.toString());
0126     }
0127     m_shortcut = new QGlobalShortcut(shortcut, this);
0128     connect(m_shortcut, &QGlobalShortcut::activated, m_actionShow, &QAction::trigger);
0129     connect(m_actionShow, &QAction::triggered, this, &Mangonel::triggered);
0130 
0131     QString message = xi18nc("@info", "Press <shortcut>%1</shortcut> to show Mangonel.", m_shortcut->key().toString());
0132     KNotification::event(QLatin1String("startup"), message);
0133 
0134     m_history = settings.value("history").toStringList();
0135 
0136     // Instantiate the providers.
0137     m_providers["applications"] = new Applications(this);
0138     m_providers["paths"] = new Paths(this);
0139     m_providers["shell"] = new Shell(this);
0140     m_providers["Calculator"] = new Calculator(this);
0141     m_providers["Units"] = new Units(this);
0142 
0143     // Migrate old and broken
0144     if (settings.childGroups().contains("popularities")) {
0145         settings.beginGroup("popularities");
0146         QHash<QString, Popularity> children = getRecursivePopularity(settings);
0147         QHash<QString, Popularity>::const_iterator i = children.constBegin();
0148         while (i != children.constEnd()) {
0149             m_popularities.insert(i.key(), i.value());
0150             ++i;
0151         }
0152         settings.endGroup();
0153     }
0154 
0155     settings.beginGroup("popularitiesv2");
0156     for (const QString &key : settings.childGroups()) {
0157         settings.beginGroup(key);
0158         Popularity pop;
0159         const QString program = settings.value("program").toString();
0160         pop.count = settings.value("launches").toLongLong();
0161         pop.lastUse = settings.value("lastUse").toLongLong();
0162         pop.matchStrings = settings.value("matchStrings").toStringList();
0163         m_popularities.insert(program, pop);
0164         settings.endGroup();
0165     }
0166     settings.endGroup();
0167 }
0168 
0169 void Mangonel::storePopularities()
0170 {
0171     QSettings settings;
0172     settings.beginGroup("popularitiesv2");
0173     for (const QString &key : m_popularities.keys()) {
0174         // QSettings is dumb and annoying
0175         const QByteArray hashedProgram = QCryptographicHash::hash(key.toUtf8(), QCryptographicHash::Md5).toHex();
0176         settings.beginGroup(QString::fromLatin1(hashedProgram));
0177         settings.setValue("program", key);
0178         settings.setValue("launches", m_popularities[key].count);
0179         settings.setValue("lastUse", m_popularities[key].lastUse);
0180         settings.setValue("matchStrings", m_popularities[key].matchStrings);
0181         settings.endGroup();
0182     }
0183     settings.endGroup();
0184 
0185     // Remove legacy
0186     settings.beginGroup("popularities");
0187     settings.remove("");
0188 }
0189 
0190 Mangonel *Mangonel::instance()
0191 {
0192     static Mangonel s_instance;
0193     return &s_instance;
0194 }
0195 
0196 QList<QObject *> Mangonel::setQuery(const QString &query)
0197 {
0198     m_currentQuery = query;
0199 
0200     if (query.isEmpty()) {
0201         return {};
0202     }
0203 
0204     const qint64 currentSecsSinceEpoch = QDateTime::currentSecsSinceEpoch();
0205 
0206     QElapsedTimer timer;
0207 
0208     m_current = -1;
0209     QList<ProviderResult*> newResults;
0210     for (Provider* provider : m_providers) {
0211         timer.restart();
0212         QList<ProviderResult*> list = provider->getResults(query);
0213         if (timer.elapsed() > 30) {
0214             qWarning() << provider << "spent" << timer.elapsed() << "ms on" << query;
0215         }
0216         for (ProviderResult *app : list) {
0217             if (!app) {
0218                 qWarning() << "got null app from" << provider;
0219                 continue;
0220             }
0221             if (app->name.isEmpty()) {
0222                 qWarning() << "empty name!" << app->name << app->program << app->completion;
0223                 continue;
0224             }
0225             QQmlEngine::setObjectOwnership(app, QQmlEngine::JavaScriptOwnership);
0226             app->setParent(this);
0227 
0228             if (!app->isCalculation) {
0229                 if (m_popularities.contains(app->program)) {
0230                     const Popularity &popularity = m_popularities[app->program];
0231                     app->priority = currentSecsSinceEpoch - popularity.lastUse;
0232                     app->priority -= (3600 * 360) * popularity.count;
0233                 }
0234             }
0235 
0236             newResults.append(app);
0237         }
0238     }
0239 
0240     std::sort(newResults.begin(), newResults.end(), [this](ProviderResult *a, ProviderResult *b) {
0241             Q_ASSERT(a);
0242             Q_ASSERT(b);
0243 
0244             const bool aContains = a->name.contains(m_currentQuery, Qt::CaseInsensitive) ||
0245                                     a->program.contains(m_currentQuery, Qt::CaseInsensitive);
0246             const bool bContains = b->name.contains(m_currentQuery, Qt::CaseInsensitive) ||
0247                                     b->program.contains(m_currentQuery, Qt::CaseInsensitive);
0248             if (aContains != bContains) {
0249                 return aContains;
0250             }
0251 
0252             const bool aHasPopularity = m_popularities.contains(a->program);
0253             const bool bHasPopularity = m_popularities.contains(b->program);
0254             if (aHasPopularity != bHasPopularity) {
0255                 return aHasPopularity;
0256             }
0257 
0258             if (aHasPopularity && bHasPopularity) {
0259                 const Popularity &aPopularity = m_popularities[a->program];
0260                 const Popularity &bPopularity = m_popularities[b->program];
0261 
0262                 const bool aHasMatchStrings = aPopularity.matchStrings.contains(m_currentQuery);
0263                 const bool bHasMatchStrings = bPopularity.matchStrings.contains(m_currentQuery);
0264                 if (aHasMatchStrings != bHasMatchStrings) {
0265                     return aHasMatchStrings;
0266                 }
0267 
0268                 if (aPopularity.count != bPopularity.count) {
0269                     return aPopularity.count > bPopularity.count;
0270                 }
0271 
0272                 if (aPopularity.lastUse != bPopularity.lastUse) {
0273                     return aPopularity.lastUse > bPopularity.lastUse;
0274                 }
0275             }
0276 
0277             bool aStartMatch = a->name.startsWith(m_currentQuery, Qt::CaseInsensitive);
0278             bool bStartMatch = b->name.startsWith(m_currentQuery, Qt::CaseInsensitive);
0279             if (aStartMatch != bStartMatch) {
0280                 return aStartMatch;
0281             }
0282 
0283             aStartMatch = a->program.startsWith(m_currentQuery, Qt::CaseInsensitive);
0284             bStartMatch = b->program.startsWith(m_currentQuery, Qt::CaseInsensitive);
0285             if (aStartMatch != bStartMatch) {
0286                 return aStartMatch;
0287             }
0288 
0289             if (a->isCalculation != b->isCalculation) {
0290                 return a->isCalculation;
0291             }
0292 
0293             if (a->priority != b->priority) {
0294                 return a->priority < b->priority;
0295             }
0296 
0297             if (a->name != b->name) {
0298                 return a->name > b->name;
0299             }
0300 
0301             // They are 100% equal
0302             return false;
0303     });
0304 
0305     QList<QObject*> ret;
0306     for (ProviderResult *result : newResults) {
0307         ret.append(result);
0308     }
0309 
0310     return ret;
0311 }
0312 
0313 void Mangonel::launch(QObject *selectedObject)
0314 {
0315     ProviderResult *selected = qobject_cast<ProviderResult*>(selectedObject);
0316     if (!selected) {
0317         qWarning() << "Trying to launch null pointer";
0318         return;
0319     }
0320 
0321     addToHistory(m_currentQuery);
0322     selected->launch();
0323 
0324     if (selected->isCalculation) {
0325         return;
0326     }
0327 
0328     Popularity pop;
0329     const QString exec = selected->program;
0330 
0331     if (m_popularities.contains(exec)) {
0332         pop = m_popularities[exec];
0333         pop.lastUse = QDateTime::currentSecsSinceEpoch();
0334 
0335         // Cap it, so history doesn't haunt forever
0336         pop.count = std::min(pop.count + 1, qint64(10));
0337 
0338         if (pop.matchStrings.contains(m_currentQuery)) {
0339             pop.matchStrings.removeAll(m_currentQuery);
0340         }
0341     } else {
0342         pop.lastUse = QDateTime::currentSecsSinceEpoch();
0343         pop.count = 0;
0344     }
0345     pop.matchStrings.prepend(m_currentQuery);
0346 
0347     m_popularities[exec] = pop;
0348 
0349     storePopularities();
0350 }
0351 
0352 void Mangonel::showConfig()
0353 {
0354     ConfigDialog* dialog = new ConfigDialog;
0355     dialog->setHotkey(m_shortcut->key());
0356     connect(dialog, SIGNAL(hotkeyChanged(QKeySequence)), this, SLOT(setHotkey(QKeySequence)));
0357     dialog->exec();
0358 }
0359 
0360 void Mangonel::setHotkey(const QKeySequence& hotkey)
0361 {
0362     QSettings settings;
0363     settings.setValue(QLatin1String(s_shortcutKey), hotkey.toString());
0364 
0365     m_shortcut->setKey(hotkey);
0366 }
0367 
0368 void Mangonel::configureNotifications()
0369 {
0370     KNotifyConfigWidget::configure();
0371 }
0372 
0373 QString Mangonel::selectionClipboardContent()
0374 {
0375     return QGuiApplication::clipboard()->text(QClipboard::Selection);
0376 }
0377 
0378 void Mangonel::addToHistory(const QString &text)
0379 {
0380     m_history.prepend(text);
0381     m_history.removeDuplicates();
0382     emit historyChanged();
0383 
0384     // Store history of session.
0385     QSettings settings;
0386     settings.setValue("history", m_history);
0387 }
0388 
0389 // kate: indent-mode cstyle; space-indent on; indent-width 4;