diff --git a/applets/systemtray/package/contents/ui/HiddenItemsView.qml b/applets/systemtray/package/contents/ui/HiddenItemsView.qml index bea02c317..3793aac8f 100644 --- a/applets/systemtray/package/contents/ui/HiddenItemsView.qml +++ b/applets/systemtray/package/contents/ui/HiddenItemsView.qml @@ -1,71 +1,72 @@ /* * Copyright 2016 Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, 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 Library General Public License for more details * * You should have received a copy of the GNU Library 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. */ import QtQuick 2.1 import QtQuick.Layouts 1.1 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.extras 2.0 as PlasmaExtras PlasmaExtras.ScrollArea { id: hiddenTasksView visible: !activeApplet || activeApplet.parent.parent == hiddenTasksColumn width: activeApplet ? iconColumnWidth : parent.width property alias layout: hiddenTasksColumn //Useful to align stuff to the column of icons, both in expanded and shrink modes property int iconColumnWidth: root.hiddenItemSize + highlight.marginHints.left + highlight.marginHints.right verticalScrollBarPolicy: activeApplet ? Qt.ScrollBarAlwaysOff : Qt.ScrollBarAsNeeded Flickable { contentWidth: width contentHeight: hiddenTasksColumn.height MouseArea { width: parent.width height: hiddenTasksColumn.height drag.filterChildren: true hoverEnabled: true onExited: hiddenTasksColumn.hoveredItem = null; CurrentItemHighLight { target: root.activeApplet && root.activeApplet.parent.parent == hiddenTasksColumn ? root.activeApplet.parent : null location: PlasmaCore.Types.LeftEdge } PlasmaComponents.Highlight { id: highlight visible: hiddenTasksColumn.hoveredItem != null && !root.activeApplet y: hiddenTasksColumn.hoveredItem ? hiddenTasksColumn.hoveredItem.y : 0 width: hiddenTasksColumn.hoveredItem ? hiddenTasksColumn.hoveredItem.width : 0 height: hiddenTasksColumn.hoveredItem ? hiddenTasksColumn.hoveredItem.height : 0 } Column { id: hiddenTasksColumn + spacing: units.smallSpacing width: parent.width property Item hoveredItem property alias marginHints: highlight.marginHints - + objectName: "hiddenTasksColumn" } } } } diff --git a/applets/systemtray/package/contents/ui/items/AbstractItem.qml b/applets/systemtray/package/contents/ui/items/AbstractItem.qml index dfa604f0d..7b84a04a5 100644 --- a/applets/systemtray/package/contents/ui/items/AbstractItem.qml +++ b/applets/systemtray/package/contents/ui/items/AbstractItem.qml @@ -1,129 +1,115 @@ /* * Copyright 2016 Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, 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 Library General Public License for more details * * You should have received a copy of the GNU Library 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. */ import QtQuick 2.1 +import QtQml.Models 2.2 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents PlasmaCore.ToolTipArea { id: abstractItem height: hidden ? root.hiddenItemSize + marginHints.top + marginHints.bottom : root.itemSize + marginHints.top + marginHints.bottom width: labelVisible ? parent.width : height + marginHints.left + marginHints.right property string itemId property string category property alias text: label.text property bool hidden: parent.objectName == "hiddenTasksColumn" property QtObject marginHints: parent.marginHints property bool labelVisible: abstractItem.hidden && !root.activeApplet property Item iconItem //PlasmaCore.Types.ItemStatus property int status - - //FIXME: Qt 5.6 - property bool repositioningInProgress: false + property QtObject model signal clicked(var mouse) signal wheel(var wheel) property bool forcedHidden: plasmoid.configuration.hiddenItems.indexOf(itemId) !== -1 property bool forcedShown: plasmoid.configuration.showAllItems || plasmoid.configuration.shownItems.indexOf(itemId) !== -1 property bool categoryShown: shownCategories.indexOf(category) != -1; /* subclasses need to assign to this tiiltip properties mainText: subText: icon: */ - location: if (abstractItem.parent && abstractItem.parent.objectName == "hiddenTasksColumn") { return PlasmaCore.Types.RightEdge; } else { return abstractItem.location; } - function updateVisibility() { - if (!categoryShown) { - abstractItem.parent = invisibleEntriesContainer; - } else if (forcedShown || !(forcedHidden || status == PlasmaCore.Types.PassiveStatus)) { - visibleLayout.addItem(abstractItem); - } else { - abstractItem.parent = hiddenLayout; - abstractItem.x = 0; - } - } - - //BEGIN CONNECTIONS - onStatusChanged: updateVisibility() + onStatusChanged: updateItemVisibility(abstractItem); onContainsMouseChanged: { if (hidden && containsMouse) { root.hiddenLayout.hoveredItem = abstractItem } } - Component.onCompleted: updateVisibility() - onForcedHiddenChanged: updateVisibility() - onForcedShownChanged: updateVisibility() - onCategoryShownChanged: updateVisibility() + Component.onCompleted: updateItemVisibility(abstractItem); + onForcedHiddenChanged: updateItemVisibility(abstractItem); + onForcedShownChanged: updateItemVisibility(abstractItem); + onCategoryShownChanged: updateItemVisibility(abstractItem); //dangerous but needed due how repeater reparents - onParentChanged: updateVisibility() + onParentChanged: updateItemVisibility(abstractItem); //END CONNECTIONS PulseAnimation { targetItem: abstractItem running: (abstractItem.status === PlasmaCore.Types.NeedsAttentionStatus || abstractItem.status === PlasmaCore.Types.RequiresAttentionStatus ) && units.longDuration > 0 } MouseArea { id: mouseArea anchors.fill: abstractItem hoverEnabled: true drag.filterChildren: true acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton onClicked: abstractItem.clicked(mouse) onWheel: abstractItem.wheel(wheel) } PlasmaComponents.Label { id: label opacity: labelVisible ? 1 : 0 x: iconItem ? iconItem.width + units.smallSpacing : 0 visible: abstractItem.hidden Behavior on opacity { NumberAnimation { duration: units.longDuration easing.type: Easing.InOutQuad } } anchors { verticalCenter: parent.verticalCenter } } } diff --git a/applets/systemtray/package/contents/ui/items/StatusNotifierItem.qml b/applets/systemtray/package/contents/ui/items/StatusNotifierItem.qml index 8f67bc39b..115e1fbea 100644 --- a/applets/systemtray/package/contents/ui/items/StatusNotifierItem.qml +++ b/applets/systemtray/package/contents/ui/items/StatusNotifierItem.qml @@ -1,118 +1,116 @@ /* * Copyright 2016 Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, 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 Library General Public License for more details * * You should have received a copy of the GNU Library 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. */ import QtQuick 2.1 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents AbstractItem { id: taskIcon itemId: Id text: Title mainText: ToolTipTitle != "" ? ToolTipTitle : Title subText: ToolTipSubTitle icon: ToolTipIcon != "" ? ToolTipIcon : plasmoid.nativeInterface.resolveIcon(IconName != "" ? IconName : Icon, IconThemePath) textFormat: Text.AutoText category: Category status: { switch (Status) { case "Active": return PlasmaCore.Types.ActiveStatus; case "NeedsAttention": return PlasmaCore.Types.NeedsAttentionStatus; //just assume passive default: return PlasmaCore.Types.PassiveStatus; } } iconItem: iconItem - Component.onCompleted: taskIcon.parent = visibleLayout - PlasmaCore.IconItem { id: iconItem source: plasmoid.nativeInterface.resolveIcon(IconName != "" ? IconName : Icon, IconThemePath) width: Math.min(parent.width, parent.height) height: width active: taskIcon.containsMouse anchors { left: parent.left verticalCenter: parent.verticalCenter } } onClicked: { var pos = plasmoid.nativeInterface.popupPosition(taskIcon, 0, 0); switch (mouse.button) { case Qt.LeftButton: { var service = statusNotifierSource.serviceForSource(DataEngineSource); var operation = service.operationDescription("Activate"); operation.x = pos.x; operation.y = pos.y; service.startOperationCall(operation); break; } case Qt.RightButton: { var service = statusNotifierSource.serviceForSource(DataEngineSource); var operation = service.operationDescription("ContextMenu"); operation.x = pos.x; operation.y = pos.y; var job = service.startOperationCall(operation); job.finished.connect(function () { plasmoid.nativeInterface.showStatusNotifierContextMenu(job, taskIcon); }); break; } case Qt.MiddleButton: var service = statusNotifierSource.serviceForSource(DataEngineSource); var operation = service.operationDescription("SecondaryActivate"); operation.x = pos.x; operation.y = pos.y; service.startOperationCall(operation); break; break; } } onWheel: { //don't send activateVertScroll with a delta of 0, some clients seem to break (kmix) if (wheel.angleDelta.y !== 0) { var service = statusNotifierSource.serviceForSource(DataEngineSource); var operation = service.operationDescription("Scroll"); operation.delta =wheel.angleDelta.y; operation.direction = "Vertical"; service.startOperationCall(operation); } if (wheel.angleDelta.x !== 0) { var service = statusNotifierSource.serviceForSource(DataEngineSource); var operation = service.operationDescription("Scroll"); operation.delta =wheel.angleDelta.x; operation.direction = "Horizontal"; service.startOperationCall(operation); } } } diff --git a/applets/systemtray/package/contents/ui/main.qml b/applets/systemtray/package/contents/ui/main.qml index 31bc6ed21..dd0910861 100644 --- a/applets/systemtray/package/contents/ui/main.qml +++ b/applets/systemtray/package/contents/ui/main.qml @@ -1,272 +1,281 @@ /* * Copyright 2011 Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, 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 Library General Public License for more details * * You should have received a copy of the GNU Library 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. */ import QtQuick 2.5 import QtQuick.Layouts 1.1 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.plasmoid 2.0 import "items" MouseArea { id: root Layout.minimumWidth: vertical ? units.iconSizes.small : tasksRow.implicitWidth + expander.implicitWidth + units.smallSpacing Layout.minimumHeight: vertical ? tasksRow.implicitHeight+ expander.implicitHeight + units.smallSpacing : units.smallSpacing property bool vertical: plasmoid.formFactor == PlasmaCore.Types.Vertical property int itemSize: Math.min(Math.min(width, height), units.iconSizes.smallMedium) property int hiddenItemSize: units.iconSizes.smallMedium property alias expanded: dialog.visible property Item activeApplet property alias visibleLayout: tasksRow property alias hiddenLayout: expandedRepresentation.hiddenLayout property alias statusNotifierModel: statusNotifierModel - function addApplet(applet, x, y) { + function updateItemVisibility(item) { + + //Invisible + if (!item.categoryShown) { + if (item.parent == invisibleEntriesContainer) { + return; + } + + item.parent = invisibleEntriesContainer; + + //visible + } else if (item.forcedShown || !(item.forcedHidden || item.status == PlasmaCore.Types.PassiveStatus)) { + + if (visibleLayout.children.length == 0) { + item.parent = visibleLayout; + //notifications is always the first + } else if (visibleLayout.children[0].itemId == "org.kde.plasma.notifications" && + item.itemId != "org.kde.plasma.notifications") { + plasmoid.nativeInterface.reorderItemAfter(item, visibleLayout.children[0]); + } else { + plasmoid.nativeInterface.reorderItemBefore(item, visibleLayout.children[0]); + } + + //hidden + } else { + + if (hiddenLayout.children.length == 0) { + item.parent = hiddenLayout; + //notifications is always the first + } else if (hiddenLayout.children[0].itemId == "org.kde.plasma.notifications" && + item.itemId != "org.kde.plasma.notifications") { + plasmoid.nativeInterface.reorderItemAfter(item, hiddenLayout.children[0]); + } else { + plasmoid.nativeInterface.reorderItemBefore(item, hiddenLayout.children[0]); + } + item.x = 0; + } + } + + Containment.onAppletAdded: { print("Applet created:" + applet.title) var component = Qt.createComponent("items/PlasmoidItem.qml") - var plasmoidContainer = component.createObject((applet.status == PlasmaCore.Types.PassiveStatus) ? hiddenLayout : visibleLayout, {"x": x, "y": y}); + var plasmoidContainer = component.createObject(invisibleEntriesContainer, {"x": x, "y": y, "applet": applet}); - plasmoidContainer.applet = applet applet.parent = plasmoidContainer applet.anchors.left = plasmoidContainer.left applet.anchors.top = plasmoidContainer.top applet.anchors.bottom = plasmoidContainer.bottom applet.width = plasmoidContainer.height applet.visible = true plasmoidContainer.visible = true } - Containment.onAppletAdded: { - addApplet(applet, x, y); - } - Containment.onAppletRemoved: { } Connections { target: plasmoid.configuration onExtraItemsChanged: plasmoid.nativeInterface.allowedPlasmoids = plasmoid.configuration.extraItems } Component.onCompleted: { //script, don't bind plasmoid.nativeInterface.allowedPlasmoids = initializePlasmoidList(); } function initializePlasmoidList() { var newKnownItems = []; var newExtraItems = []; //NOTE:why this? otherwise the interpreter will execute plasmoid.nativeInterface.defaultPlasmoids() on //every access of defaults[], resulting in a very slow iteration var defaults = []; //defaults = defaults.concat(plasmoid.nativeInterface.defaultPlasmoids); defaults = plasmoid.nativeInterface.defaultPlasmoids.slice() var candidate; //Add every plasmoid that is both not enabled explicitly and not already known for (var i = 0; i < defaults.length; ++i) { candidate = defaults[i]; if (plasmoid.configuration.knownItems.indexOf(candidate) === -1) { newKnownItems.push(candidate); if (plasmoid.configuration.extraItems.indexOf(candidate) === -1) { newExtraItems.push(candidate); } } } if (newExtraItems.length > 0) { plasmoid.configuration.extraItems = plasmoid.configuration.extraItems.slice().concat(newExtraItems); } if (newKnownItems.length > 0) { plasmoid.configuration.knownItems = plasmoid.configuration.knownItems.slice().concat(newKnownItems); } return plasmoid.configuration.extraItems; } PlasmaCore.DataSource { id: statusNotifierSource engine: "statusnotifieritem" interval: 0 onSourceAdded: { connectSource(source) } Component.onCompleted: { connectedSources = sources } } //due to the magic of property bindings this function will be //re-executed all the times a setting changes property var shownCategories: { var array = []; if (plasmoid.configuration.applicationStatusShown) { array.push("ApplicationStatus"); } if (plasmoid.configuration.communicationsShown) { array.push("Communications"); } if (plasmoid.configuration.systemServicesShown) { array.push("SystemServices"); } if (plasmoid.configuration.hardwareControlShown) { array.push("Hardware"); } if (plasmoid.configuration.miscellaneousShown) { array.push("UnknownCategory"); } //nothing? make a regexp that matches nothing if (array.length == 0) { array.push("$^") } return array; } - + PlasmaCore.SortFilterModel { id: statusNotifierModel - filterRole: "Category" - filterRegExp: "("+shownCategories.join("|")+")" sourceModel: PlasmaCore.DataModel { dataSource: statusNotifierSource } } //This is a dump for items we don't want to be seen or as an incubation, when they are //created as a nursery before going in their final place Item { id: invisibleEntriesContainer visible: false Repeater { id: tasksRepeater model: statusNotifierModel delegate: StatusNotifierItem {} } //NOTE: this exists mostly for not causing reference errors property QtObject marginHints: QtObject { property int left: 0 property int top: 0 property int right: 0 property int bottom: 0 } } CurrentItemHighLight { target: root.activeApplet && root.activeApplet.parent.parent == tasksRow ? root.activeApplet.parent : root location: plasmoid.location } //Main Layout Flow { id: tasksRow spacing: 0 height: parent.height - (vertical ? expander.height : 0) width: parent.width - (vertical ? 0 : expander.width) property string skipItems flow: vertical ? Flow.LeftToRight : Flow.TopToBottom //To make it look centered y: Math.round(height/2 - childrenRect.height/2) x: Math.round(width/2 - childrenRect.width/2) - //make last icon appeared nearer to the tasskbar - //TODO: Qt 5.6 will have functions for it - function addItem(item) { - if (item.parent == tasksRow || item.repositioningInProgress == true || item == marginHints) { - return; - } - var items = []; - for (var i = 0; i < tasksRow.children.length; ++i) { - items.push(tasksRow.children[i]); - } - for (var i = 0; i < items.length; ++i) { - items[i].repositioningInProgress = true; - items[i].parent = invisibleEntriesContainer; - } - - item.parent = tasksRow; - - for (var i = 0; i < items.length; ++i) { - items[i].parent = tasksRow; - items[i].repositioningInProgress = false; - } - } - - //NOTE: this exists mostly for not causing reference errors + //Do spacing with margins, to correctly compute the number of lines property QtObject marginHints: QtObject { property int left: Math.round(units.smallSpacing / 2) property int top: Math.round(units.smallSpacing / 2) property int right: Math.round(units.smallSpacing / 2) property int bottom: Math.round(units.smallSpacing / 2) } - /*FIXME: need Qt 5.6 for transitions to look good - add: Transition { + //add doesn't seem to work used in conjunction with stackBefore/stackAfter + /*add: Transition { NumberAnimation { property: "scale" from: 0 to: 1 easing.type: Easing.InQuad duration: units.longDuration } } move: Transition { NumberAnimation { properties: "x,y" easing.type: Easing.InQuad duration: units.longDuration } }*/ } ExpanderArrow { id: expander anchors { fill: parent leftMargin: vertical ? 0 : parent.width - implicitWidth topMargin: vertical ? parent.height - implicitHeight : 0 } } //Main popup PlasmaCore.Dialog { id: dialog visualParent: root flags: Qt.WindowStaysOnTopHint location: plasmoid.location hideOnWindowDeactivate: expandedRepresentation.hideOnWindowDeactivate onVisibleChanged: { if (!visible && root.activeApplet) { root.activeApplet.expanded = false; } } mainItem: ExpandedRepresentation { id: expandedRepresentation activeApplet: root.activeApplet } } } diff --git a/applets/systemtray/systemtray.cpp b/applets/systemtray/systemtray.cpp index 19db2e80f..57602b41e 100644 --- a/applets/systemtray/systemtray.cpp +++ b/applets/systemtray/systemtray.cpp @@ -1,550 +1,574 @@ /*************************************************************************** * Copyright (C) 2015 Marco Martin * * * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Q_LOGGING_CATEGORY(SYSTEMTRAY, "systemtray") class PlasmoidModel: public QStandardItemModel { public: PlasmoidModel(QObject *parent = 0) : QStandardItemModel(parent) { } QHash roleNames() const override { QHash 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::CustomPanelContainment); } SystemTray::~SystemTray() { } void SystemTray::init() { config().writeEntry("lastScreen", -1); Containment::init(); //actions()->removeAction(actions()->action("add widgets")); //actions()->removeAction(actions()->action("add panel")); //actions()->removeAction(actions()->action("lock widgets")); } void SystemTray::newTask(const QString &task) { foreach (Plasma::Applet *applet, applets()) { if (!applet->pluginInfo().isValid()) { continue; } //only allow one instance per applet if (task == applet->pluginInfo().pluginName()) { return; } } createApplet(task, QVariantList() << "org.kde.plasma:force-create"); } void SystemTray::cleanupTask(const QString &task) { foreach (Plasma::Applet *applet, applets()) { if (!applet->pluginInfo().isValid() || task == applet->pluginInfo().pluginName()) { applet->destroy(); } } } QVariant SystemTray::resolveIcon(const QVariant &variant, const QString &iconThemePath) { if (variant.canConvert()) { if (!iconThemePath.isEmpty()) { const QString path = iconThemePath; if (!path.isEmpty()) { // FIXME: If last part of path is not "icons", this won't work! QStringList tokens = path.split('/', QString::SkipEmptyParts); if (tokens.length() >= 3 && tokens.takeLast() == QLatin1String("icons")) { QString appName = tokens.takeLast(); // We use a separate instance of KIconLoader to avoid // adding all application dirs to KIconLoader::global(), to // avoid potential icon name clashes between application // icons //FIXME: leak KIconLoader *customIconLoader = new KIconLoader(appName, QStringList(), this); customIconLoader->addAppDir(appName, path); return QVariant(QIcon(new KIconEngine(variant.toString(), customIconLoader))); } else { qWarning() << "Wrong IconThemePath" << path << ": too short or does not end with 'icons'"; } } //return just the string hoping that IconItem will know how to interpret it anyways as either a normal icon or a SVG from the theme return variant; } } // Most importantly QIcons. Nothing to do for those. return variant; } void SystemTray::showPlasmoidMenu(QQuickItem *appletInterface) { if (!appletInterface) { return; } Plasma::Applet *applet = appletInterface->property("_plasma_applet").value(); QPointF pos = appletInterface->mapToScene(QPointF(15,15)); 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); 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"))); } //FIXME: systraycontainer? Plasma::Applet *systrayApplet = this; 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()); } Q_INVOKABLE QString SystemTray::plasmoidCategory(QQuickItem *appletInterface) const { if (!appletInterface) { return "UnknownCategory"; } Plasma::Applet *applet = appletInterface->property("_plasma_applet").value(); if (!applet || !applet->pluginInfo().isValid()) { return "UnknownCategory"; } const QString cat = applet->pluginInfo().property(QStringLiteral("X-Plasma-NotificationAreaCategory")).toString(); 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(job); if (!sjob) { return; } QMenu *menu = qobject_cast(sjob->result().value()); if (menu) { menu->adjustSize(); int x = sjob->parameters()[QStringLiteral("x")].toInt(); int y = sjob->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); +} + void SystemTray::restoreContents(KConfigGroup &group) { //Don't do anything here, it's too soon qWarning()<<"RestoreContents doesn't do anything here"; } void SystemTray::restorePlasmoids() { if (!isContainment()) { qWarning() << "Loaded as an applet, this shouldn't have happened"; return; } //First: remove all that are not allowed anymore QStringList tasksToDelete; foreach (Plasma::Applet *applet, applets()) { //Here it should always be valid. //for some reason it not always is. if (!applet->pluginInfo().isValid()) { applet->destroy(); } else { const QString task = applet->pluginInfo().pluginName(); if (!m_allowedPlasmoids.contains(task)) { applet->destroy(); } } } 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(); } } qWarning() << "Known plasmoid ids:"<< m_knownPlugins; //X-Plasma-NotificationArea const QString constraint = QStringLiteral("[X-Plasma-NotificationArea] == true"); KPluginInfo::List applets; for (auto info : Plasma::PluginLoader::self()->listAppletInfo(QString())) { if (info.isValid() && info.property(QStringLiteral("X-Plasma-NotificationArea")).toBool() == true) { applets << info; } } QStringList ownApplets; QMap sortedApplets; foreach (const KPluginInfo &info, applets) { const QString dbusactivation = info.property(QStringLiteral("X-Plasma-DBusActivationService")).toString(); if (!dbusactivation.isEmpty()) { qCDebug(SYSTEMTRAY) << "ST Found DBus-able Applet: " << info.pluginName() << dbusactivation; m_dbusActivatableTasks[info.pluginName()] = dbusactivation; continue; } if (m_allowedPlasmoids.contains(info.pluginName()) && //FIXME //!m_tasks.contains(info.pluginName()) && dbusactivation.isEmpty()) { // 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(SYSTEMTRAY) << " Adding applet: " << info.name(); qCDebug(SYSTEMTRAY) << "\n\n =========================================================================================="; if (m_allowedPlasmoids.contains(info.pluginName())) { newTask(info.pluginName()); } } initDBusActivatables(); } QStringList SystemTray::defaultPlasmoids() const { QStringList ret; for (auto info : Plasma::PluginLoader::self()->listAppletInfo(QString())) { if (info.isValid() && info.property(QStringLiteral("X-Plasma-NotificationArea")) == "true" && info.isPluginEnabledByDefault()) { ret += info.pluginName(); } } return ret; } QAbstractItemModel* SystemTray::availablePlasmoids() { if (!m_availablePlasmoidsModel) { m_availablePlasmoidsModel = new PlasmoidModel(this); //Filter X-Plasma-NotificationArea KPluginInfo::List applets; for (auto info : Plasma::PluginLoader::self()->listAppletInfo(QString())) { if (info.property(QStringLiteral("X-Plasma-NotificationArea")) == "true") { applets << info; } } foreach (const KPluginInfo &info, applets) { QString name = info.name(); KService::Ptr service = info.service(); 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 propsReply = *watcher; watcher->deleteLater(); if (propsReply.isError()) { qCWarning(SYSTEMTRAY) << "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) { qDebug() << "DBus service appeared:" << service; foreach (const QString &plugin, m_dbusActivatableTasks.keys()) { if (!m_allowedPlasmoids.contains(plugin)) { continue; } const QString& pattern = m_dbusActivatableTasks.value(plugin); QRegExp rx(pattern); rx.setPatternSyntax(QRegExp::Wildcard); if (rx.exactMatch(service)) { qDebug() << "ST : DBus service " << m_dbusActivatableTasks[plugin] << "appeared. Loading " << plugin; newTask(plugin); m_dbusServiceCounts[plugin]++; } } } void SystemTray::serviceUnregistered(const QString &service) { qDebug() << "DBus service disappeared:" << service; foreach (const QString &plugin, m_dbusActivatableTasks.keys()) { if (!m_allowedPlasmoids.contains(plugin)) { continue; } const QString& pattern = m_dbusActivatableTasks.value(plugin); 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) { qDebug() << "ST : DBus service " << m_dbusActivatableTasks[plugin] << " disappeared. Unloading " << plugin; cleanupTask(plugin); } } } } K_EXPORT_PLASMA_APPLET_WITH_JSON(systemtray, SystemTray, "metadata.json") #include "systemtray.moc" diff --git a/applets/systemtray/systemtray.h b/applets/systemtray/systemtray.h index ebcee7cdd..91dbee28f 100644 --- a/applets/systemtray/systemtray.h +++ b/applets/systemtray/systemtray.h @@ -1,110 +1,126 @@ /*************************************************************************** * Copyright (C) 2015 Marco Martin * * * * * 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 . * ***************************************************************************/ #ifndef SYSTEMTRAY_H #define SYSTEMTRAY_H #include #include class QDBusPendingCallWatcher; class QDBusConnection; class QQuickItem; class PlasmoidModel; class SystemTray : public Plasma::Containment { Q_OBJECT Q_PROPERTY(QAbstractItemModel* availablePlasmoids READ availablePlasmoids CONSTANT) Q_PROPERTY(QStringList allowedPlasmoids READ allowedPlasmoids WRITE setAllowedPlasmoids NOTIFY allowedPlasmoidsChanged) Q_PROPERTY(QStringList defaultPlasmoids READ defaultPlasmoids CONSTANT) public: SystemTray( QObject *parent, const QVariantList &args ); ~SystemTray() override; void init() override; void restoreContents(KConfigGroup &group) Q_DECL_OVERRIDE; void restorePlasmoids(); QStringList defaultPlasmoids() const; QAbstractItemModel* availablePlasmoids(); QStringList allowedPlasmoids() const; void setAllowedPlasmoids(const QStringList &allowed); //Creates an applet *if not already existing* void newTask(const QString &task); //cleans all instances of a given applet void cleanupTask(const QString &task); //Invokable utilities /** * returns either a simple icon name or a custom path if the app is * using a custom theme */ Q_INVOKABLE QVariant resolveIcon(const QVariant &variant, const QString &iconThemePath); /** * Given an AppletInterface pointer, shows a proper context menu fot it */ Q_INVOKABLE void showPlasmoidMenu(QQuickItem *appletInterface); /** * Returns the "X-Plasma-NotificationAreaCategory" * of the plasmoid metadata */ Q_INVOKABLE QString plasmoidCategory(QQuickItem *appletInterface) const; /** * Shows the context menu for a statusnotifieritem */ Q_INVOKABLE void showStatusNotifierContextMenu(KJob *job, QQuickItem *statusNotifierIcon); /** * Find out global coordinates for a popup given local MouseArea * coordinates */ Q_INVOKABLE QPointF popupPosition(QQuickItem* visualParent, int x, int y); + /** + * Reparent the item "before" with the same parent as the item "after", + * then restack it before it, using QQuickITem::stackBefore. + * used to quickly reorder icons in the systray (or hidden popup) + * @see QQuickITem::stackBefore + */ + Q_INVOKABLE void reorderItemBefore(QQuickItem* before, QQuickItem* after); + + /** + * Reparent the item "after" with the same parent as the item "before", + * then restack it after it, using QQuickITem::stackAfter. + * used to quickly reorder icons in the systray (or hidden popup) + * @see QQuickITem::stackAfter + */ + Q_INVOKABLE void reorderItemAfter(QQuickItem* after, QQuickItem* before); + private Q_SLOTS: void serviceNameFetchFinished(QDBusPendingCallWatcher* watcher, const QDBusConnection &connection); void serviceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner); private: void serviceRegistered(const QString &service); void serviceUnregistered(const QString &service); Q_SIGNALS: void allowedPlasmoidsChanged(); private: void initDBusActivatables(); QStringList m_allowedPlasmoids; PlasmoidModel *m_availablePlasmoidsModel; QHash m_knownPlugins; QHash m_dbusActivatableTasks; QHash m_dbusServiceCounts; }; #endif