You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

583 lines
20 KiB

/***************************************************************************
* Copyright (C) 2015 Marco Martin <mart@kde.org> *
* *
* 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 2 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, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . *
***************************************************************************/
#include "systemtray.h"
#include "debug.h"
#include <QDebug>
#include <QProcess>
#include <QVersionNumber>
#include <QTimer>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusPendingCallWatcher>
#include <QMenu>
#include <QQuickItem>
#include <QQuickWindow>
#include <QRegExp>
#include <QScreen>
#include <QStandardItemModel>
#include <Plasma/PluginLoader>
#include <Plasma/ServiceJob>
#include <KActionCollection>
#include <KLocalizedString>
#include <plasma_version.h>
class PlasmoidModel: public QStandardItemModel
{
public:
PlasmoidModel(QObject *parent = 0)
: QStandardItemModel(parent)
{
}
QHash<int, QByteArray> roleNames() const override {
QHash<int, QByteArray> roles = QStandardItemModel::roleNames();
roles[Qt::UserRole+1] = "plugin";
return roles;
}
};
SystemTray::SystemTray(QObject *parent, const QVariantList &args)
: Plasma::Containment(parent, args),
m_availablePlasmoidsModel(nullptr)
{
setHasConfigurationInterface(true);
setContainmentType(Plasma::Types::CustomEmbeddedContainment);
}
SystemTray::~SystemTray()
{
}
void SystemTray::init()
{
Containment::init();
for (const auto &info: Plasma::PluginLoader::self()->listAppletMetaData(QString())) {
if (!info.isValid() || info.value(QStringLiteral("X-Plasma-NotificationArea")) != "true") {
continue;
}
m_systrayApplets[info.pluginId()] = KPluginInfo(info);
if (info.isEnabledByDefault()) {
m_defaultPlasmoids += info.pluginId();
}
const QString dbusactivation = info.value(QStringLiteral("X-Plasma-DBusActivationService"));
if (!dbusactivation.isEmpty()) {
qCDebug(SYSTEM_TRAY) << "ST Found DBus-able Applet: " << info.pluginId() << dbusactivation;
m_dbusActivatableTasks[info.pluginId()] = dbusactivation;
}
}
}
void SystemTray::newTask(const QString &task)
{
foreach (Plasma::Applet *applet, applets()) {
if (!applet->pluginMetaData().isValid()) {
continue;
}
//only allow one instance per applet
if (task == applet->pluginMetaData().pluginId()) {
//Applet::destroy doesn't delete the applet from Containment::applets in the same event
//potentially a dbus activated service being restarted can be added in this time.
if (!applet->destroyed()) {
return;
}
}
}
//known one, recycle the id to reuse old config
if (m_knownPlugins.contains(task)) {
Applet *applet = Plasma::PluginLoader::self()->loadApplet(task, m_knownPlugins.value(task), QVariantList());
//this should never happen unless explicitly wrong config is hand-written or
//(more likely) a previously added applet is uninstalled
if (!applet) {
qWarning() << "Unable to find applet" << task;
return;
}
applet->setProperty("org.kde.plasma:force-create", true);
addApplet(applet);
//create a new one automatic id, new config group
} else {
Applet * applet = createApplet(task, QVariantList() << "org.kde.plasma:force-create");
if (applet) {
m_knownPlugins[task] = applet->id();
}
}
}
void SystemTray::cleanupTask(const QString &task)
{
foreach (Plasma::Applet *applet, applets()) {
if (applet->pluginMetaData().isValid() && task == applet->pluginMetaData().pluginId()) {
//we are *not* cleaning the config here, because since is one
//of those automatically loaded/unloaded by dbus, we want to recycle
//the config the next time it's loaded, in case the user configured something here
applet->deleteLater();
//HACK: we need to remove the applet from Containment::applets() as soon as possible
//otherwise we may have disappearing applets for restarting dbus services
//this may be removed when we depend from a frameworks version in which appletDeleted is emitted as soon as deleteLater() is called
emit appletDeleted(applet);
}
}
}
void SystemTray::showPlasmoidMenu(QQuickItem *appletInterface, int x, int y)
{
if (!appletInterface) {
return;
}
Plasma::Applet *applet = appletInterface->property("_plasma_applet").value<Plasma::Applet*>();
QPointF pos = appletInterface->mapToScene(QPointF(x, y));
if (appletInterface->window() && appletInterface->window()->screen()) {
pos = appletInterface->window()->mapToGlobal(pos.toPoint());
} else {
pos = QPoint();
}
QMenu *desktopMenu = new QMenu;
connect(this, &QObject::destroyed, desktopMenu, &QMenu::close);
desktopMenu->setAttribute(Qt::WA_DeleteOnClose);
//this is a workaround where Qt will fail to realise a mouse has been released
// this happens if a window which does not accept focus spawns a new window that takes focus and X grab
// whilst the mouse is depressed
// https://bugreports.qt.io/browse/QTBUG-59044
// this causes the next click to go missing
//by releasing manually we avoid that situation
auto ungrabMouseHack = [appletInterface]() {
if (appletInterface->window() && appletInterface->window()->mouseGrabberItem()) {
appletInterface->window()->mouseGrabberItem()->ungrabMouse();
}
};
//pre 5.8.0 QQuickWindow code is "item->grabMouse(); sendEvent(item, mouseEvent)"
//post 5.8.0 QQuickWindow code is sendEvent(item, mouseEvent); item->grabMouse()
if (QVersionNumber::fromString(qVersion()) > QVersionNumber(5, 8, 0)) {
QTimer::singleShot(0, appletInterface, ungrabMouseHack);
}
else {
ungrabMouseHack();
}
//end workaround
emit applet->contextualActionsAboutToShow();
foreach (QAction *action, applet->contextualActions()) {
if (action) {
desktopMenu->addAction(action);
}
}
QAction *runAssociatedApplication = applet->actions()->action(QStringLiteral("run associated application"));
if (runAssociatedApplication && runAssociatedApplication->isEnabled()) {
desktopMenu->addAction(runAssociatedApplication);
}
if (applet->actions()->action(QStringLiteral("configure"))) {
desktopMenu->addAction(applet->actions()->action(QStringLiteral("configure")));
}
if (desktopMenu->isEmpty()) {
delete desktopMenu;
return;
}
desktopMenu->adjustSize();
if (QScreen *screen = appletInterface->window()->screen()) {
const QRect geo = screen->availableGeometry();
pos = QPoint(qBound(geo.left(), (int)pos.x(), geo.right() - desktopMenu->width()),
qBound(geo.top(), (int)pos.y(), geo.bottom() - desktopMenu->height()));
}
desktopMenu->popup(pos.toPoint());
}
QString SystemTray::plasmoidCategory(QQuickItem *appletInterface) const
{
if (!appletInterface) {
return "UnknownCategory";
}
Plasma::Applet *applet = appletInterface->property("_plasma_applet").value<Plasma::Applet*>();
if (!applet || !applet->pluginMetaData().isValid()) {
return "UnknownCategory";
}
const QString cat = applet->pluginMetaData().value(QStringLiteral("X-Plasma-NotificationAreaCategory"));
if (cat.isEmpty()) {
return "UnknownCategory";
}
return cat;
}
void SystemTray::showStatusNotifierContextMenu(KJob *job, QQuickItem *statusNotifierIcon)
{
if (QCoreApplication::closingDown() || !statusNotifierIcon) {
// apparently an edge case can be triggered due to the async nature of all this
// see: https://bugs.kde.org/show_bug.cgi?id=251977
return;
}
Plasma::ServiceJob *sjob = qobject_cast<Plasma::ServiceJob *>(job);
if (!sjob) {
return;
}
QMenu *menu = qobject_cast<QMenu *>(sjob->result().value<QObject *>());
if (menu) {
menu->adjustSize();
const auto parameters = sjob->parameters();
int x = parameters[QStringLiteral("x")].toInt();
int y = parameters[QStringLiteral("y")].toInt();
//try tofind the icon screen coordinates, and adjust the position as a poor
//man's popupPosition
QRect screenItemRect(statusNotifierIcon->mapToScene(QPointF(0, 0)).toPoint(), QSize(statusNotifierIcon->width(), statusNotifierIcon->height()));
if (statusNotifierIcon->window()) {
screenItemRect.moveTopLeft(statusNotifierIcon->window()->mapToGlobal(screenItemRect.topLeft()));
}
switch (location()) {
case Plasma::Types::LeftEdge:
x = screenItemRect.right();
y = screenItemRect.top();
break;
case Plasma::Types::RightEdge:
x = screenItemRect.left() - menu->width();
y = screenItemRect.top();
break;
case Plasma::Types::TopEdge:
x = screenItemRect.left();
y = screenItemRect.bottom();
break;
case Plasma::Types::BottomEdge:
x = screenItemRect.left();
y = screenItemRect.top() - menu->height();
break;
default:
x = screenItemRect.left();
if (screenItemRect.top() - menu->height() >= statusNotifierIcon->window()->screen()->geometry().top()) {
y = screenItemRect.top() - menu->height();
} else {
y = screenItemRect.bottom();
}
}
menu->popup(QPoint(x, y));
}
}
QPointF SystemTray::popupPosition(QQuickItem* visualParent, int x, int y)
{
if (!visualParent) {
return QPointF(0, 0);
}
QPointF pos = visualParent->mapToScene(QPointF(x, y));
if (visualParent->window() && visualParent->window()->screen()) {
pos = visualParent->window()->mapToGlobal(pos.toPoint());
} else {
return QPoint();
}
return pos;
}
void SystemTray::reorderItemBefore(QQuickItem* before, QQuickItem* after)
{
if (!before || !after) {
return;
}
before->setVisible(false);
before->setParentItem(after->parentItem());
before->stackBefore(after);
before->setVisible(true);
}
void SystemTray::reorderItemAfter(QQuickItem* after, QQuickItem* before)
{
if (!before || !after) {
return;
}
after->setVisible(false);
after->setParentItem(before->parentItem());
after->stackAfter(before);
after->setVisible(true);
}
bool SystemTray::isSystemTrayApplet(const QString &appletId)
{
return m_systrayApplets.contains(appletId);
}
void SystemTray::restoreContents(KConfigGroup &group)
{
Q_UNUSED(group);
//NOTE: RestoreContents shouldnn't do anything here because is too soon, so have an empty reimplementation
}
void SystemTray::restorePlasmoids()
{
if (!isContainment()) {
qCWarning(SYSTEM_TRAY) << "Loaded as an applet, this shouldn't have happened";
return;
}
//First: remove all that are not allowed anymore
foreach (Plasma::Applet *applet, applets()) {
//Here it should always be valid.
//for some reason it not always is.
if (!applet->pluginMetaData().isValid()) {
applet->config().parent().deleteGroup();
applet->deleteLater();
} else {
const QString task = applet->pluginMetaData().pluginId();
if (!m_allowedPlasmoids.contains(task)) {
//in those cases we do delete the applet config completely
//as they were explicitly disabled by the user
applet->config().parent().deleteGroup();
applet->deleteLater();
}
}
}
KConfigGroup cg = config();
cg = KConfigGroup(&cg, "Applets");
foreach (const QString &group, cg.groupList()) {
KConfigGroup appletConfig(&cg, group);
QString plugin = appletConfig.readEntry("plugin");
if (!plugin.isEmpty()) {
m_knownPlugins[plugin] = group.toInt();
}
}
QStringList ownApplets;
QMap<QString, KPluginInfo> sortedApplets;
for (auto it = m_systrayApplets.constBegin(); it != m_systrayApplets.constEnd(); ++it) {
const KPluginInfo &info = it.value();
if (m_allowedPlasmoids.contains(info.pluginName()) && !
m_dbusActivatableTasks.contains(info.pluginName())) {
//FIXME
// if we already have a plugin with this exact name in it, then check if it is the
// same plugin and skip it if it is indeed already listed
if (sortedApplets.contains(info.name())) {
bool dupe = false;
// it is possible (though poor form) to have multiple applets
// with the same visible name but different plugins, so we hve to check all values
foreach (const KPluginInfo &existingInfo, sortedApplets.values(info.name())) {
if (existingInfo.pluginName() == info.pluginName()) {
dupe = true;
break;
}
}
if (dupe) {
continue;
}
}
// insertMulti becase it is possible (though poor form) to have multiple applets
// with the same visible name but different plugins
sortedApplets.insertMulti(info.name(), info);
}
}
foreach (const KPluginInfo &info, sortedApplets) {
qCDebug(SYSTEM_TRAY) << " Adding applet: " << info.name();
if (m_allowedPlasmoids.contains(info.pluginName())) {
newTask(info.pluginName());
}
}
initDBusActivatables();
}
QStringList SystemTray::defaultPlasmoids() const
{
return m_defaultPlasmoids;
}
QAbstractItemModel* SystemTray::availablePlasmoids()
{
if (!m_availablePlasmoidsModel) {
m_availablePlasmoidsModel = new PlasmoidModel(this);
foreach (const KPluginInfo &info, m_systrayApplets) {
QString name = info.name();
const QString dbusactivation = info.property(QStringLiteral("X-Plasma-DBusActivationService")).toString();
if (!dbusactivation.isEmpty()) {
name += i18n(" (Automatic load)");
}
QStandardItem *item = new QStandardItem(QIcon::fromTheme(info.icon()), name);
item->setData(info.pluginName());
m_availablePlasmoidsModel->appendRow(item);
}
}
return m_availablePlasmoidsModel;
}
QStringList SystemTray::allowedPlasmoids() const
{
return m_allowedPlasmoids;
}
void SystemTray::setAllowedPlasmoids(const QStringList &allowed)
{
if (allowed == m_allowedPlasmoids) {
return;
}
m_allowedPlasmoids = allowed;
restorePlasmoids();
emit allowedPlasmoidsChanged();
}
void SystemTray::initDBusActivatables()
{
/* Loading and unloading Plasmoids when dbus services come and go
*
* This works as follows:
* - we collect a list of plugins and related services in m_dbusActivatableTasks
* - we query DBus for the list of services, async (initDBusActivatables())
* - we go over that list, adding tasks when a service and plugin match (serviceNameFetchFinished())
* - we start watching for new services, and do the same (serviceNameFetchFinished())
* - whenever a service is gone, we check whether to unload a Plasmoid (serviceUnregistered())
*/
QDBusPendingCall async = QDBusConnection::sessionBus().interface()->asyncCall(QStringLiteral("ListNames"));
QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this);
connect(callWatcher, &QDBusPendingCallWatcher::finished,
[=](QDBusPendingCallWatcher *callWatcher){
SystemTray::serviceNameFetchFinished(callWatcher, QDBusConnection::sessionBus());
});
QDBusPendingCall systemAsync = QDBusConnection::systemBus().interface()->asyncCall(QStringLiteral("ListNames"));
QDBusPendingCallWatcher *systemCallWatcher = new QDBusPendingCallWatcher(systemAsync, this);
connect(systemCallWatcher, &QDBusPendingCallWatcher::finished,
[=](QDBusPendingCallWatcher *callWatcher){
SystemTray::serviceNameFetchFinished(callWatcher, QDBusConnection::systemBus());
});
}
void SystemTray::serviceNameFetchFinished(QDBusPendingCallWatcher* watcher, const QDBusConnection &connection)
{
QDBusPendingReply<QStringList> propsReply = *watcher;
watcher->deleteLater();
if (propsReply.isError()) {
qCWarning(SYSTEM_TRAY) << "Could not get list of available D-Bus services";
} else {
foreach (const QString& serviceName, propsReply.value()) {
serviceRegistered(serviceName);
}
}
// Watch for new services
// We need to watch for all of new services here, since we want to "match" the names,
// not just compare them
// This makes mpris work, since it wants to match org.mpris.MediaPlayer2.dragonplayer
// against org.mpris.MediaPlayer2
// QDBusServiceWatcher is not capable for watching wildcard service right now
// See:
// https://bugreports.qt.io/browse/QTBUG-51683
// https://bugreports.qt.io/browse/QTBUG-33829
connect(connection.interface(), &QDBusConnectionInterface::serviceOwnerChanged, this, &SystemTray::serviceOwnerChanged);
}
void SystemTray::serviceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner)
{
if (oldOwner.isEmpty()) {
serviceRegistered(serviceName);
} else if (newOwner.isEmpty()) {
serviceUnregistered(serviceName);
}
}
void SystemTray::serviceRegistered(const QString &service)
{
//qCDebug(SYSTEM_TRAY) << "DBus service appeared:" << service;
for (auto it = m_dbusActivatableTasks.constBegin(), end = m_dbusActivatableTasks.constEnd(); it != end; ++it) {
const QString &plugin = it.key();
if (!m_allowedPlasmoids.contains(plugin)) {
continue;
}
const QString &pattern = it.value();
QRegExp rx(pattern);
rx.setPatternSyntax(QRegExp::Wildcard);
if (rx.exactMatch(service)) {
//qCDebug(SYSTEM_TRAY) << "ST : DBus service " << m_dbusActivatableTasks[plugin] << "appeared. Loading " << plugin;
newTask(plugin);
m_dbusServiceCounts[plugin]++;
}
}
}
void SystemTray::serviceUnregistered(const QString &service)
{
//qCDebug(SYSTEM_TRAY) << "DBus service disappeared:" << service;
for (auto it = m_dbusActivatableTasks.constBegin(), end = m_dbusActivatableTasks.constEnd(); it != end; ++it) {
const QString &plugin = it.key();
if (!m_allowedPlasmoids.contains(plugin)) {
continue;
}
const QString &pattern = it.value();
QRegExp rx(pattern);
rx.setPatternSyntax(QRegExp::Wildcard);
if (rx.exactMatch(service)) {
m_dbusServiceCounts[plugin]--;
Q_ASSERT(m_dbusServiceCounts[plugin] >= 0);
if (m_dbusServiceCounts[plugin] == 0) {
//qCDebug(SYSTEM_TRAY) << "ST : DBus service " << m_dbusActivatableTasks[plugin] << " disappeared. Unloading " << plugin;
cleanupTask(plugin);
}
}
}
}
K_EXPORT_PLASMA_APPLET_WITH_JSON(systemtray, SystemTray, "metadata.json")
#include "systemtray.moc"