Commit aee0f2e8 authored by Kai Uwe Broulik's avatar Kai Uwe Broulik 🍇
Browse files

Port TabsRunner to DBus runner

Removes quite some boilerplate code. The extension side is unchanged
and overall it functions the same albeit likely somewhat quicker as
the amount of DBus traffic is reduced by cutting the intermediary and
only sending the found tabs around.
parent 6599a4ef
......@@ -56,7 +56,6 @@ set(MOZILLA_DIR "${CMAKE_INSTALL_PREFIX}/lib/mozilla" CACHE STRING "Mozilla dire
add_feature_info(MOZILLA_DIR On "Mozilla directory is '${MOZILLA_DIR}'")
add_subdirectory(host)
add_subdirectory(tabsrunner)
add_subdirectory(reminder)
if(NOT DEFINED CHROME_EXTENSION_ID)
......
......@@ -17,7 +17,6 @@ set(HOST_SOURCES main.cpp
purposeplugin.cpp
)
qt5_add_dbus_adaptor(HOST_SOURCES ../dbus/org.kde.plasma.browser_integration.TabsRunner.xml tabsrunnerplugin.h TabsRunnerPlugin)
qt5_add_dbus_adaptor(HOST_SOURCES ../dbus/org.kde.plasma.browser_integration.Settings.xml settings.h Settings)
qt5_add_dbus_adaptor(HOST_SOURCES ../dbus/org.mpris.MediaPlayer2.xml mprisplugin.h MPrisPlugin mprisroot MPrisRoot)
......@@ -43,4 +42,5 @@ target_link_libraries(
install(TARGETS plasma-browser-integration-host ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
install(FILES plasma-runner-browsertabs.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins)
install(FILES plasma-runner-browserhistory.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins)
[Desktop Entry]
# ctxt: plasma runner
Name=Browser Tabs
Comment=Find and activate browser tabs
X-KDE-ServiceTypes=Plasma/Runner
Type=Service
Icon=internet-web-browser
X-KDE-PluginInfo-Author=Kai Uwe Broulik
X-KDE-PluginInfo-Email=kde@broulik.de
X-KDE-PluginInfo-Name=browsertabs
X-KDE-PluginInfo-Version=1.0
X-KDE-PluginInfo-License=LGPL
X-KDE-PluginInfo-EnabledByDefault=true
X-Plasma-AdvertiseSingleRunnerQueryMode=true
X-Plasma-API=DBus
X-Plasma-DBusRunner-Service=org.kde.plasma.browser_integration*
X-Plasma-DBusRunner-Path=/TabsRunner
X-Plasma-Request-Actions-Once=true
X-Plasma-Runner-Min-Letter-Count=3
X-Plasma-Runner-Syntaxes=:q:,
X-Plasma-Runner-Syntax-Descriptions=Finds open browser tabs whose title or URL match :q:
......@@ -26,86 +26,194 @@
#include "connection.h"
#include <QDBusConnection>
#include <QList>
#include <QHash>
#include <QImage>
#include <QJsonArray>
#include <QJsonObject>
#include <QVariant>
#include <QVariantHash>
#include "tabsrunneradaptor.h"
#include <KLocalizedString>
#include "settings.h"
static const auto s_actionIdMute = QLatin1String("MUTE");
static const auto s_actionIdUnmute = QLatin1String("UNMUTE");
TabsRunnerPlugin::TabsRunnerPlugin(QObject* parent) :
AbstractBrowserPlugin(QStringLiteral("tabsrunner"), 1, parent)
AbstractKRunnerPlugin(QStringLiteral("/TabsRunner"),
QStringLiteral("tabsrunner"),
1,
parent)
{
new TabsRunnerAdaptor(this);
}
bool TabsRunnerPlugin::onLoad()
{
return QDBusConnection::sessionBus().registerObject(QStringLiteral("/TabsRunner"), this);
}
bool TabsRunnerPlugin::onUnload()
RemoteActions TabsRunnerPlugin::Actions()
{
QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/TabsRunner"));
return true;
RemoteAction muteAction{
s_actionIdMute,
i18n("Mute Tab"),
QStringLiteral("audio-volume-muted")
};
RemoteAction unmuteAction{
s_actionIdUnmute,
i18n("Unmute Tab"),
QStringLiteral("audio-volume-high")
};
return {muteAction, unmuteAction};
}
// FIXME We really should enforce some kind of security policy, so only e.g. plasmashell and krunner
// may access your tabs
QList<QVariantHash> TabsRunnerPlugin::GetTabs()
RemoteMatches TabsRunnerPlugin::Match(const QString &searchTerm)
{
m_tabRequestMessages.append(message());
if (searchTerm.length() < 3) {
sendErrorReply(QDBusError::InvalidArgs, QStringLiteral("Search term too short"));
return {};
}
setDelayedReply(true);
sendData(QStringLiteral("getTabs"));
const bool runQuery = m_requests.isEmpty();
m_requests.insert(searchTerm, message());
if (runQuery) {
sendData(QStringLiteral("getTabs"));
}
return {};
}
void TabsRunnerPlugin::Activate(int tabId)
void TabsRunnerPlugin::Run(const QString &id, const QString &actionId)
{
sendData(QStringLiteral("activate"), {
{QStringLiteral("tabId"), tabId}
});
}
bool ok = false;
const int tabId = id.toInt(&ok);
if (!ok || tabId < 0) {
sendErrorReply(QDBusError::InvalidArgs, QStringLiteral("Invalid tab ID"));
return;
}
void TabsRunnerPlugin::SetMuted(int tabId, bool muted)
{
sendData(QStringLiteral("setMuted"), {
{QStringLiteral("tabId"), tabId},
{QStringLiteral("muted"), muted}
});
if (actionId.isEmpty()) {
sendData(QStringLiteral("activate"), {
{QStringLiteral("tabId"), tabId}
});
return;
}
if (actionId == s_actionIdMute || actionId == s_actionIdUnmute) {
sendData(QStringLiteral("setMuted"), {
{QStringLiteral("tabId"), tabId},
{QStringLiteral("muted"), actionId == s_actionIdMute}
});
return;
}
sendErrorReply(QDBusError::InvalidArgs, QStringLiteral("Unknown action ID"));
}
void TabsRunnerPlugin::handleData(const QString& event, const QJsonObject& json)
{
if (event == QLatin1String("gotTabs")) {
if (!m_tabRequestMessages.isEmpty()) {
const QJsonArray &tabs = json.value(QStringLiteral("tabs")).toArray();
QList<QVariant> tabsReply;
tabsReply.reserve(tabs.count());
for (auto it = tabs.constBegin(), end = tabs.constEnd(); it != end; ++it) {
const QJsonObject &tab = it->toObject();
tabsReply.append(tab.toVariantHash());
}
QList<QVariant> reply;
reply.append(QVariant(tabsReply));
for (const QDBusMessage &request : qAsConst(m_tabRequestMessages)) {
QDBusConnection::sessionBus().send(
// TODO why does it unwrap this? didn't we want a a(a{sv}) instead of a{sv}a{sv}a{sv}..? :/
request.createReply(QList<QVariant>{tabsReply})
);
const QJsonArray &tabs = json.value(QStringLiteral("tabs")).toArray();
for (auto it = m_requests.constBegin(), end = m_requests.constEnd(); it != end; ++it) {
const QString query = it.key();
const QDBusMessage request = it.value();
RemoteMatches matches;
for (auto jt = tabs.constBegin(), jend = tabs.constEnd(); jt != jend; ++jt) {
const QJsonObject tab = jt->toObject();
RemoteMatch match;
const int tabId = tab.value(QStringLiteral("id")).toInt();
const QString text = tab.value(QStringLiteral("title")).toString();
const QUrl url(tab.value(QStringLiteral("url")).toString());
QStringList actions;
qreal relevance = 0;
// someone was really busy here, typing the *exact* title or url :D
if (text.compare(query, Qt::CaseInsensitive) == 0
|| url.toString().compare(query, Qt::CaseInsensitive) == 0) {
match.type = Plasma::QueryMatch::ExactMatch;
relevance = 1;
} else {
match.type = Plasma::QueryMatch::PossibleMatch;
if (text.contains(query, Qt::CaseInsensitive)) {
relevance = 0.9;
if (text.startsWith(query, Qt::CaseInsensitive)) {
relevance += 0.1;
}
} else if (url.host().contains(query, Qt::CaseInsensitive)) {
relevance = 0.7;
if (url.host().startsWith(query, Qt::CaseInsensitive)) {
relevance += 0.1;
}
} else if (url.path().contains(query, Qt::CaseInsensitive)) {
relevance = 0.5;
if (url.path().startsWith(query, Qt::CaseInsensitive)) {
relevance += 0.1;
}
}
}
if (!relevance) {
continue;
}
match.id = QString::number(tabId);
match.text = text;
match.properties.insert(QStringLiteral("subtext"), url.toDisplayString());
match.relevance = relevance;
const bool audible = tab.value(QStringLiteral("audible")).toBool();
const QJsonObject mutedInfo = tab.value(QStringLiteral("mutedInfo")).toObject();
const bool muted = mutedInfo.value(QStringLiteral("muted")).toBool();
if (audible) {
if (muted) {
match.iconName = QStringLiteral("audio-volume-muted");
actions.append(s_actionIdUnmute);
} else {
match.iconName = QStringLiteral("audio-volume-high");
actions.append(s_actionIdMute);
}
} else {
match.iconName = Settings::self().environmentDescription().iconName;
const QString favIconData = tab.value(QStringLiteral("favIconData")).toString();
if (favIconData.startsWith(QLatin1String("data:"))) {
const int b64start = favIconData.indexOf(QLatin1Char(','));
if (b64start != -1) {
QByteArray b64 = favIconData.rightRef(favIconData.count() - b64start - 1).toLatin1();
QByteArray data = QByteArray::fromBase64(b64);
QImage image;
if (image.loadFromData(data)) {
const RemoteImage remoteImage = serializeImage(image);
match.properties.insert(QStringLiteral("icon-data"), QVariant::fromValue(remoteImage));
} else {
qWarning() << "Failed to load favicon image for" << match.id << match.text;
}
}
}
}
// Has to always be present so it knows we handle actions ourself
match.properties.insert(QStringLiteral("actions"), actions);
matches.append(match);
}
m_tabRequestMessages.clear();
QDBusConnection::sessionBus().send(
request.createReply(QVariant::fromValue(matches))
);
}
m_requests.clear();
}
}
......@@ -23,29 +23,27 @@
#pragma once
#include "abstractbrowserplugin.h"
#include "abstractkrunnerplugin.h"
#include <QDBusContext>
#include <QDBusMessage>
#include <QMultiHash>
class TabsRunnerPlugin : public AbstractBrowserPlugin, protected QDBusContext
class TabsRunnerPlugin : public AbstractKRunnerPlugin
{
Q_OBJECT
public:
explicit TabsRunnerPlugin(QObject *parent);
bool onLoad() override;
bool onUnload() override;
using AbstractBrowserPlugin::handleData;
void handleData(const QString &event, const QJsonObject &data) override;
// dbus-exported
QList<QHash<QString, QVariant>> GetTabs();
void Activate(int tabId);
void SetMuted(int tabId, bool muted);
// DBus API
RemoteActions Actions() override;
RemoteMatches Match(const QString &searchTerm) override;
void Run(const QString &id, const QString &actionId) override;
private:
QVector<QDBusMessage> m_tabRequestMessages;
QMultiHash<QString, QDBusMessage> m_requests;
};
add_definitions(-DTRANSLATION_DOMAIN=\"plasma_runner_browsertabs\")
set(krunner_browsertabs_SRCS
tabsrunner.cpp
)
add_library(krunner_browsertabs MODULE ${krunner_browsertabs_SRCS})
target_link_libraries(krunner_browsertabs
Qt5::DBus
KF5::I18n
KF5::Runner
)
kcoreaddons_desktop_to_json(krunner_browsertabs plasma-runner-browsertabs.desktop)
install(TARGETS krunner_browsertabs DESTINATION "${KDE_INSTALL_PLUGINDIR}/kf5/krunner")
#! /usr/bin/env bash
$XGETTEXT *.cpp -o $podir/plasma_runner_browsertabs.pot
[Desktop Entry]
Name=Browser Tabs
Name[ar]=ألسنة المتصفّح
Name[ast]=Llingüetes del restolador
Name[az]=Bələdçi Vərəqləri
Name[ca]=Pestanyes del navegador
Name[ca@valencia]=Pestanyes del navegador
Name[cs]=Karty prohlížeče
Name[da]=Browser-faneblade
Name[de]=Browser-Unterfenster
Name[el]=Καρτέλες περιηγητή
Name[en_GB]=Browser Tabs
Name[es]=Pestañas del navegador
Name[et]=Brauseri kaardid
Name[eu]=Arakatzailearen fitxak
Name[fi]=Selainvälilehdet
Name[fr]=Onglets du navigateur
Name[gl]=Separadores do navegador
Name[hu]=Böngészőlapok
Name[ia]=Etiquettas del Navigator
Name[id]=Browser Tabs
Name[it]=Schede del browser
Name[ko]=브라우저 탭
Name[lt]=Naršyklės kortelės
Name[nb]=Nettleserfaner
Name[nl]=Browsertabbladen
Name[nn]=Nettlesarfaner
Name[pl]=Karty przeglądarki
Name[pt]=Páginas do Navegador
Name[pt_BR]=Abas do navegador
Name[ro]=File de navigator
Name[ru]=Вкладки браузера
Name[sk]=Karty prehliadača
Name[sl]=Zavihki brskalnika
Name[sr]=Језичци прегледача
Name[sr@ijekavian]=Језичци прегледача
Name[sr@ijekavianlatin]=Jezičci pregledača
Name[sr@latin]=Jezičci pregledača
Name[sv]=Webbläsarflikar
Name[tg]=Варақаҳои намоишгар
Name[tr]=Tarayıcı Sekmeleri
Name[uk]=Вкладки навігатора
Name[x-test]=xxBrowser Tabsxx
Name[zh_CN]=浏览器标签
Name[zh_TW]=瀏覽器分頁
Comment=Find and activate browser tabs
Comment[ar]=جِد ألسنة المتصفّح ونشّطها
Comment[ast]=Alcuentra y activa llingüetes del restolador
Comment[az]=Bələdçi vərəqləri siyahısı və onlar arasında keçid
Comment[ca]=Cerca i activa les pestanyes del navegador
Comment[ca@valencia]=Busca i activa les pestanyes del navegador
Comment[cs]=Najít a aktivovat karty prohlížeče
Comment[da]=Find og aktivér browser-faneblade
Comment[de]=Browser-Unterfenster suchen und aktivieren
Comment[el]=Εύρεση και ενεργοποίηση καρτελών περιηγητή
Comment[en_GB]=Find and activate browser tabs
Comment[es]=Buscar y activar pestañas del navegador
Comment[et]=Brauseri kaartide otsimine ja aktiveerimine
Comment[eu]=Aurkitu eta aktibatu arakatzailearen fitxak
Comment[fi]=Etsi ja aktivoi selainvälilehtiä
Comment[fr]=Chercher et activer les onglets du navigateur
Comment[gl]=Atopar e activar separadores do navegador
Comment[hu]=Böngészőlapok keresése és aktiválása
Comment[ia]=Trova e activa etiquettas del navigator
Comment[id]=Temukan dan aktifkan tab-tab browser
Comment[it]=Cerca e attiva le schede del browser
Comment[ko]=브라우저 탭을 찾고 활성화
Comment[lt]=Rasti ir aktyvuoti naršyklės korteles
Comment[nb]=Finner og aktiverer nettleserfaner
Comment[nl]=Zoek en activeer browsertabbladen
Comment[nn]=Finn og aktiver nettlesarfaner
Comment[pl]=Wypisuje karty przeglądarki oraz przełącza pomiędzy nimi
Comment[pt]=Procurar e activar páginas do navegador
Comment[pt_BR]=Encontre e ative as abas do navegador
Comment[ro]=Găsește și activează file de navigator
Comment[ru]=Список вкладок браузера с возможностью переключения между ними
Comment[sk]=Nájsť a aktivovať karty prehliadača
Comment[sl]=Najdite in omogočite zavihke brskalnika
Comment[sr]=Нађите и активирајте језичке прегледача
Comment[sr@ijekavian]=Нађите и активирајте језичке прегледача
Comment[sr@ijekavianlatin]=Nađite i aktivirajte jezičke pregledača
Comment[sr@latin]=Nađite i aktivirajte jezičke pregledača
Comment[sv]=Sök efter och aktivera webbläsarflikar
Comment[tg]=Ёфтан ва фаъол кардани варақаҳои намоишгар
Comment[tr]=Tarayıcı sekmelerini bul ve etkinleştir
Comment[uk]=Пошук і задіяння вкладок навігатора
Comment[x-test]=xxFind and activate browser tabsxx
Comment[zh_CN]=找到并激活浏览器标签
Comment[zh_TW]=尋找並開啟瀏覽器分頁
X-KDE-ServiceTypes=Plasma/Runner
Type=Service
Icon=internet-web-browser
X-KDE-PluginInfo-Author=Kai Uwe Broulik
X-KDE-PluginInfo-Email=kde@privat.broulik.de
X-KDE-PluginInfo-Name=browsertabs
X-KDE-PluginInfo-Version=1.0
X-KDE-PluginInfo-License=LGPL
X-KDE-PluginInfo-EnabledByDefault=true
X-Plasma-AdvertiseSingleRunnerQueryMode=true
/*
* Copyright (C) 2017 Kai Uwe Broulik <kde@privat.broulik.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "tabsrunner.h"
#include <QMimeData>
#include <QUrl>
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <KLocalizedString>
static const QString s_muteTab = QStringLiteral("mute");
static const QString s_unmuteTab = QStringLiteral("unmute");
K_EXPORT_PLASMA_RUNNER_WITH_JSON(TabsRunner, "plasma-runner-browsertabs.json")
TabsRunner::TabsRunner(QObject *parent, const QVariantList &args)
: Plasma::AbstractRunner(parent, args)
{
Q_UNUSED(args)
setObjectName(QStringLiteral("BrowserTabs"));
setPriority(AbstractRunner::HighestPriority);
addSyntax(Plasma::RunnerSyntax(QStringLiteral(":q:"), i18n("Finds browser tabs whose title match :q:")));
// should we actually show the current state instead of what the button will do?
addAction(s_muteTab, QIcon::fromTheme(QStringLiteral("audio-volume-muted")), i18n("Mute Tab"));
addAction(s_unmuteTab, QIcon::fromTheme(QStringLiteral("audio-volume-high")), i18n("Unmute Tab"));
}
TabsRunner::~TabsRunner() = default;
void TabsRunner::match(Plasma::RunnerContext &context)
{
const QString &term = context.query();
if (term.length() < 3 && !context.singleRunnerQueryMode()) {
return;
}
// first look for all running hosts, there can be multiple browsers running
QDBusReply<QStringList> servicesReply = QDBusConnection::sessionBus().interface()->registeredServiceNames();
QStringList services;
if (servicesReply.isValid()) {
services = servicesReply.value();
}
for (const QString &service: services) {
if (!service.startsWith(QLatin1String("org.kde.plasma.browser_integration"))) {
continue;
}
QString browser = m_serviceToBrowser.value(service);
if (browser.isEmpty()) { // now ask what browser we're dealing with
// FIXME can we use our dbus xml for this?
QDBusMessage message = QDBusMessage::createMethodCall(service,
QStringLiteral("/Settings"),
QStringLiteral("org.freedesktop.DBus.Properties"),
QStringLiteral("Get"));
message.setArguments({
QStringLiteral("org.kde.plasma.browser_integration.Settings"),
QStringLiteral("Environment")
});
QDBusMessage reply = QDBusConnection::sessionBus().call(message);
if (reply.type() != QDBusMessage::ReplyMessage || reply.arguments().count() != 1) {
continue;
}
// what a long tail of calls...
browser = reply.arguments().at(0).value<QDBusVariant>().variant().toString();
m_serviceToBrowser.insert(service, browser);
}
QDBusMessage message =
QDBusMessage::createMethodCall(service,
QStringLiteral("/TabsRunner"),
QStringLiteral("org.kde.plasma.browser_integration.TabsRunner"),
QStringLiteral("GetTabs")
);
QDBusMessage reply = QDBusConnection::sessionBus().call(message);
// By the time the reply came in, the context might have already been invalidated
if (!context.isValid()) {
return;
}
if (reply.type() != QDBusMessage::ReplyMessage || reply.arguments().length() != 1) {
continue;
}
QList<Plasma::QueryMatch> matches;
auto arg = reply.arguments().at(0).value<QDBusArgument>();
auto tabvs = qdbus_cast<QList<QVariant>>(arg);
for (const QVariant &tabv : tabvs)
{
auto tab = qdbus_cast<QVariantHash>(tabv.value<QDBusArgument>());
// add browser name or window name or so to it maybe?
const QString &text = tab.value(QStringLiteral("title")).toString();
if (text.isEmpty()) { // shouldn't happen?
continue;
}
// will be used to raise the tab eventually
int tabId = tab.value(QStringLiteral("id")).toInt();
if (!tabId) {
continue;
}
const QUrl url(tab.value(QStringLiteral("url")).toString());
if (!url.isValid()) {
continue;
}
const bool incognito = tab.value(QStringLiteral("incognito")).toBool();
const bool audible = tab.value(QStringLiteral("audible")).toBool();
QVariantHash mutedInfo;
tab.value(QStringLiteral("mutedInfo")).value<QDBusArgument>() >> mutedInfo;
const bool muted = mutedInfo.value(QStringLiteral("muted")).toBool();
const QVariantHash tabData = {
{QStringLiteral("service"), service},
{QStringLiteral("tabId"), tabId},
{QStringLiteral("audible"), audible},
{QStringLiteral("muted"), muted},
{QStringLiteral("url"), url}
};
Plasma::QueryMatch match(this);
match.setText(text);
match.setSubtext(url.toDisplayString());
match.setData(tabData);
qreal relevance = 0;