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;