forked from Qortal/Brooklyn
388 lines
13 KiB
C++
388 lines
13 KiB
C++
/*
|
|
SPDX-FileCopyrightText: 2015 Marco Martin <mart@kde.org>
|
|
|
|
SPDX-License-Identifier: GPL-2.0-or-later
|
|
*/
|
|
|
|
#include "systemtray.h"
|
|
#include "debug.h"
|
|
|
|
#include "plasmoidregistry.h"
|
|
#include "sortedsystemtraymodel.h"
|
|
#include "systemtraymodel.h"
|
|
#include "systemtraysettings.h"
|
|
|
|
#include <QMenu>
|
|
#include <QQuickItem>
|
|
#include <QQuickWindow>
|
|
#include <QScreen>
|
|
#include <QTimer>
|
|
|
|
#include <Plasma/Applet>
|
|
#include <Plasma/PluginLoader>
|
|
#include <Plasma/ServiceJob>
|
|
|
|
#include <KAcceleratorManager>
|
|
#include <KActionCollection>
|
|
|
|
SystemTray::SystemTray(QObject *parent, const KPluginMetaData &data, const QVariantList &args)
|
|
: Plasma::Containment(parent, data, args)
|
|
, m_plasmoidModel(nullptr)
|
|
, m_statusNotifierModel(nullptr)
|
|
, m_systemTrayModel(nullptr)
|
|
, m_sortedSystemTrayModel(nullptr)
|
|
, m_configSystemTrayModel(nullptr)
|
|
{
|
|
setHasConfigurationInterface(true);
|
|
setContainmentType(Plasma::Types::CustomEmbeddedContainment);
|
|
setContainmentDisplayHints(Plasma::Types::ContainmentDrawsPlasmoidHeading | Plasma::Types::ContainmentForcesSquarePlasmoids);
|
|
}
|
|
|
|
SystemTray::~SystemTray()
|
|
{
|
|
}
|
|
|
|
void SystemTray::init()
|
|
{
|
|
Containment::init();
|
|
|
|
m_settings = new SystemTraySettings(configScheme(), this);
|
|
connect(m_settings, &SystemTraySettings::enabledPluginsChanged, this, &SystemTray::onEnabledAppletsChanged);
|
|
|
|
m_plasmoidRegistry = new PlasmoidRegistry(m_settings, this);
|
|
connect(m_plasmoidRegistry, &PlasmoidRegistry::plasmoidEnabled, this, &SystemTray::startApplet);
|
|
connect(m_plasmoidRegistry, &PlasmoidRegistry::plasmoidStopped, this, &SystemTray::stopApplet);
|
|
|
|
// we don't want to automatically propagate the activated signal from the Applet to the Containment
|
|
// even if SystemTray is of type Containment, it is de facto Applet and should act like one
|
|
connect(this, &Containment::appletAdded, this, [this](Plasma::Applet *applet) {
|
|
disconnect(applet, &Applet::activated, this, &Applet::activated);
|
|
});
|
|
}
|
|
|
|
void SystemTray::restoreContents(KConfigGroup &group)
|
|
{
|
|
if (!isContainment()) {
|
|
qCWarning(SYSTEM_TRAY) << "Loaded as an applet, this shouldn't have happened";
|
|
return;
|
|
}
|
|
|
|
KConfigGroup shortcutConfig(&group, "Shortcuts");
|
|
QString shortcutText = shortcutConfig.readEntryUntranslated("global", QString());
|
|
if (!shortcutText.isEmpty()) {
|
|
setGlobalShortcut(QKeySequence(shortcutText));
|
|
}
|
|
|
|
// cache known config group ids for applets
|
|
KConfigGroup cg = group.group("Applets");
|
|
for (const QString &group : cg.groupList()) {
|
|
KConfigGroup appletConfig(&cg, group);
|
|
QString plugin = appletConfig.readEntry("plugin");
|
|
if (!plugin.isEmpty()) {
|
|
m_configGroupIds[plugin] = group.toInt();
|
|
}
|
|
}
|
|
|
|
m_plasmoidRegistry->init();
|
|
}
|
|
|
|
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 realize 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();
|
|
}
|
|
};
|
|
|
|
QTimer::singleShot(0, appletInterface, ungrabMouseHack);
|
|
// end workaround
|
|
|
|
Q_EMIT applet->contextualActionsAboutToShow();
|
|
const auto contextActions = applet->contextualActions();
|
|
for (QAction *action : contextActions) {
|
|
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()));
|
|
}
|
|
|
|
KAcceleratorManager::manage(desktopMenu);
|
|
desktopMenu->winId();
|
|
desktopMenu->windowHandle()->setTransientParent(appletInterface->window());
|
|
desktopMenu->popup(pos.toPoint());
|
|
}
|
|
|
|
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->isEmpty()) {
|
|
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();
|
|
}
|
|
}
|
|
|
|
KAcceleratorManager::manage(menu);
|
|
menu->winId();
|
|
menu->windowHandle()->setTransientParent(statusNotifierIcon->window());
|
|
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;
|
|
}
|
|
|
|
bool SystemTray::isSystemTrayApplet(const QString &appletId)
|
|
{
|
|
if (m_plasmoidRegistry) {
|
|
return m_plasmoidRegistry->isSystemTrayApplet(appletId);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
SystemTrayModel *SystemTray::systemTrayModel()
|
|
{
|
|
if (!m_systemTrayModel) {
|
|
m_systemTrayModel = new SystemTrayModel(this);
|
|
|
|
m_plasmoidModel = new PlasmoidModel(m_settings, m_plasmoidRegistry, m_systemTrayModel);
|
|
connect(this, &SystemTray::appletAdded, m_plasmoidModel, &PlasmoidModel::addApplet);
|
|
connect(this, &SystemTray::appletRemoved, m_plasmoidModel, &PlasmoidModel::removeApplet);
|
|
for (auto applet : applets()) {
|
|
m_plasmoidModel->addApplet(applet);
|
|
}
|
|
|
|
m_statusNotifierModel = new StatusNotifierModel(m_settings, m_systemTrayModel);
|
|
|
|
m_systemTrayModel->addSourceModel(m_plasmoidModel);
|
|
m_systemTrayModel->addSourceModel(m_statusNotifierModel);
|
|
}
|
|
|
|
return m_systemTrayModel;
|
|
}
|
|
|
|
QAbstractItemModel *SystemTray::sortedSystemTrayModel()
|
|
{
|
|
if (!m_sortedSystemTrayModel) {
|
|
m_sortedSystemTrayModel = new SortedSystemTrayModel(SortedSystemTrayModel::SortingType::SystemTray, this);
|
|
m_sortedSystemTrayModel->setSourceModel(systemTrayModel());
|
|
}
|
|
return m_sortedSystemTrayModel;
|
|
}
|
|
|
|
QAbstractItemModel *SystemTray::configSystemTrayModel()
|
|
{
|
|
if (!m_configSystemTrayModel) {
|
|
m_configSystemTrayModel = new SortedSystemTrayModel(SortedSystemTrayModel::SortingType::ConfigurationPage, this);
|
|
m_configSystemTrayModel->setSourceModel(systemTrayModel());
|
|
}
|
|
return m_configSystemTrayModel;
|
|
}
|
|
|
|
void SystemTray::onEnabledAppletsChanged()
|
|
{
|
|
// remove all that are not allowed anymore
|
|
const auto appletsList = applets();
|
|
for (Plasma::Applet *applet : appletsList) {
|
|
// 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_settings->isEnabledPlugin(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();
|
|
m_configGroupIds.remove(task);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SystemTray::startApplet(const QString &pluginId)
|
|
{
|
|
const auto appletsList = applets();
|
|
for (Plasma::Applet *applet : appletsList) {
|
|
if (!applet->pluginMetaData().isValid()) {
|
|
continue;
|
|
}
|
|
|
|
// only allow one instance per applet
|
|
if (pluginId == 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
qCDebug(SYSTEM_TRAY) << "Adding applet:" << pluginId;
|
|
|
|
// known one, recycle the id to reuse old config
|
|
if (m_configGroupIds.contains(pluginId)) {
|
|
Applet *applet = Plasma::PluginLoader::self()->loadApplet(pluginId, m_configGroupIds.value(pluginId), QVariantList());
|
|
// this should never happen unless explicitly wrong config is hand-written or
|
|
//(more likely) a previously added applet is uninstalled
|
|
if (!applet) {
|
|
qCWarning(SYSTEM_TRAY) << "Unable to find applet" << pluginId;
|
|
return;
|
|
}
|
|
applet->setProperty("org.kde.plasma:force-create", true);
|
|
addApplet(applet);
|
|
// create a new one automatic id, new config group
|
|
} else {
|
|
Applet *applet = createApplet(pluginId, QVariantList() << "org.kde.plasma:force-create");
|
|
if (applet) {
|
|
m_configGroupIds[pluginId] = applet->id();
|
|
}
|
|
}
|
|
}
|
|
|
|
void SystemTray::stopApplet(const QString &pluginId)
|
|
{
|
|
const auto appletsList = applets();
|
|
for (Plasma::Applet *applet : appletsList) {
|
|
if (applet->pluginMetaData().isValid() && pluginId == 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
|
|
Q_EMIT appletDeleted(applet);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SystemTray::stackItemBefore(QQuickItem *newItem, QQuickItem *beforeItem)
|
|
{
|
|
if (!newItem || !beforeItem) {
|
|
return;
|
|
}
|
|
newItem->stackBefore(beforeItem);
|
|
}
|
|
|
|
void SystemTray::stackItemAfter(QQuickItem *newItem, QQuickItem *afterItem)
|
|
{
|
|
if (!newItem || !afterItem) {
|
|
return;
|
|
}
|
|
newItem->stackAfter(afterItem);
|
|
}
|
|
|
|
K_PLUGIN_CLASS_WITH_JSON(SystemTray, "package/metadata.json")
|
|
|
|
#include "systemtray.moc"
|