File indexing completed on 2024-04-28 16:51:34

0001 /*
0002     SPDX-FileCopyrightText: 2017 Kai Uwe Broulik <kde@privat.broulik.de>
0003     SPDX-FileCopyrightText: 2017 David Edmundson <davidedmundson@kde.org>
0004 
0005     SPDX-License-Identifier: MIT
0006 */
0007 
0008 #include "tabsrunnerplugin.h"
0009 
0010 #include "connection.h"
0011 
0012 #include <QDBusConnection>
0013 #include <QGuiApplication>
0014 #include <QHash>
0015 #include <QIcon>
0016 #include <QImage>
0017 #include <QJsonArray>
0018 #include <QJsonObject>
0019 #include <QVariant>
0020 
0021 #include <KApplicationTrader>
0022 #include <KLocalizedString>
0023 
0024 #include "settings.h"
0025 
0026 static const auto s_actionIdMute = QLatin1String("MUTE");
0027 static const auto s_actionIdUnmute = QLatin1String("UNMUTE");
0028 
0029 TabsRunnerPlugin::TabsRunnerPlugin(QObject *parent)
0030     : AbstractKRunnerPlugin(QStringLiteral("/TabsRunner"), QStringLiteral("tabsrunner"), 1, parent)
0031 {
0032 }
0033 
0034 RemoteActions TabsRunnerPlugin::Actions()
0035 {
0036     RemoteAction muteAction{
0037         s_actionIdMute,
0038         i18n("Mute Tab"),
0039         QStringLiteral("audio-volume-muted"),
0040     };
0041     RemoteAction unmuteAction{
0042         s_actionIdUnmute,
0043         i18n("Unmute Tab"),
0044         QStringLiteral("audio-volume-high"),
0045     };
0046 
0047     return {muteAction, unmuteAction};
0048 }
0049 
0050 RemoteMatches TabsRunnerPlugin::Match(const QString &searchTerm)
0051 {
0052     if (searchTerm.length() < 3) {
0053         sendErrorReply(QDBusError::InvalidArgs, QStringLiteral("Search term too short"));
0054         return {};
0055     }
0056 
0057     setDelayedReply(true);
0058 
0059     const bool runQuery = m_requests.isEmpty();
0060 
0061     m_requests.insert(searchTerm, message());
0062 
0063     if (runQuery) {
0064         sendData(QStringLiteral("getTabs"));
0065     }
0066 
0067     return {};
0068 }
0069 
0070 void TabsRunnerPlugin::Run(const QString &id, const QString &actionId)
0071 {
0072     bool ok = false;
0073     const int tabId = id.toInt(&ok);
0074     if (!ok || tabId < 0) {
0075         sendErrorReply(QDBusError::InvalidArgs, QStringLiteral("Invalid tab ID"));
0076         return;
0077     }
0078 
0079     if (actionId.isEmpty()) {
0080         sendData(QStringLiteral("activate"),
0081                  {
0082                      {QStringLiteral("tabId"), tabId},
0083                  });
0084         return;
0085     }
0086 
0087     if (actionId == s_actionIdMute || actionId == s_actionIdUnmute) {
0088         sendData(QStringLiteral("setMuted"),
0089                  {
0090                      {QStringLiteral("tabId"), tabId},
0091                      {QStringLiteral("muted"), actionId == s_actionIdMute},
0092                  });
0093         return;
0094     }
0095 
0096     sendErrorReply(QDBusError::InvalidArgs, QStringLiteral("Unknown action ID"));
0097 }
0098 
0099 void TabsRunnerPlugin::handleData(const QString &event, const QJsonObject &json)
0100 {
0101     if (event == QLatin1String("gotTabs")) {
0102         const QJsonArray &tabs = json.value(QStringLiteral("tabs")).toArray();
0103 
0104         for (auto it = m_requests.constBegin(), end = m_requests.constEnd(); it != end; ++it) {
0105             const QString query = it.key();
0106             const QDBusMessage request = it.value();
0107 
0108             RemoteMatches matches;
0109 
0110             for (auto jt = tabs.constBegin(), jend = tabs.constEnd(); jt != jend; ++jt) {
0111                 const QJsonObject tab = jt->toObject();
0112 
0113                 RemoteMatch match;
0114 
0115                 const int tabId = tab.value(QStringLiteral("id")).toInt();
0116                 const QString text = tab.value(QStringLiteral("title")).toString();
0117                 const QUrl url(tab.value(QStringLiteral("url")).toString());
0118 
0119                 QStringList actions;
0120 
0121                 qreal relevance = 0;
0122                 // someone was really busy here, typing the *exact* title or url :D
0123                 if (text.compare(query, Qt::CaseInsensitive) == 0 || url.toString().compare(query, Qt::CaseInsensitive) == 0) {
0124                     match.type = Plasma::QueryMatch::ExactMatch;
0125                     relevance = 1;
0126                 } else {
0127                     match.type = Plasma::QueryMatch::PossibleMatch;
0128 
0129                     if (KApplicationTrader::isSubsequence(query, text, Qt::CaseInsensitive)) {
0130                         relevance = 0.85;
0131                         if (text.contains(query, Qt::CaseInsensitive)) {
0132                             relevance += 0.05;
0133                             if (text.startsWith(query, Qt::CaseInsensitive)) {
0134                                 relevance += 0.05;
0135                             }
0136                         }
0137                     } else if (url.host().contains(query, Qt::CaseInsensitive)) {
0138                         relevance = 0.7;
0139                         if (url.host().startsWith(query, Qt::CaseInsensitive)) {
0140                             relevance += 0.1;
0141                         }
0142                     } else if (url.path().contains(query, Qt::CaseInsensitive)) {
0143                         relevance = 0.5;
0144                         if (url.path().startsWith(query, Qt::CaseInsensitive)) {
0145                             relevance += 0.1;
0146                         }
0147                     }
0148                 }
0149 
0150                 if (!relevance) {
0151                     continue;
0152                 }
0153 
0154                 match.id = QString::number(tabId);
0155                 match.text = text;
0156                 match.properties.insert(QStringLiteral("subtext"), url.toDisplayString());
0157                 match.relevance = relevance;
0158 
0159                 QUrl urlWithoutPassword = url;
0160                 urlWithoutPassword.setPassword({});
0161                 match.properties.insert(QStringLiteral("urls"), QUrl::toStringList(QList<QUrl>{urlWithoutPassword}));
0162 
0163                 const bool audible = tab.value(QStringLiteral("audible")).toBool();
0164 
0165                 const QJsonObject mutedInfo = tab.value(QStringLiteral("mutedInfo")).toObject();
0166                 const bool muted = mutedInfo.value(QStringLiteral("muted")).toBool();
0167 
0168                 if (audible) {
0169                     if (muted) {
0170                         match.iconName = QStringLiteral("audio-volume-muted");
0171                         actions.append(s_actionIdUnmute);
0172                     } else {
0173                         match.iconName = QStringLiteral("audio-volume-high");
0174                         actions.append(s_actionIdMute);
0175                     }
0176                 } else {
0177                     match.iconName = qApp->windowIcon().name();
0178 
0179                     const QImage favIcon = imageFromDataUrl(tab.value(QStringLiteral("favIconData")).toString());
0180                     if (!favIcon.isNull()) {
0181                         const RemoteImage remoteImage = serializeImage(favIcon);
0182                         match.properties.insert(QStringLiteral("icon-data"), QVariant::fromValue(remoteImage));
0183                     }
0184                 }
0185 
0186                 // Has to always be present so it knows we handle actions ourself
0187                 match.properties.insert(QStringLiteral("actions"), actions);
0188 
0189                 matches.append(match);
0190             }
0191 
0192             QDBusConnection::sessionBus().send(request.createReply(QVariant::fromValue(matches)));
0193         }
0194 
0195         m_requests.clear();
0196     }
0197 }