diff --git a/components/containmentlayoutmanager/appletslayout.cpp b/components/containmentlayoutmanager/appletslayout.cpp index 3e616dd71..a2f8ad765 100644 --- a/components/containmentlayoutmanager/appletslayout.cpp +++ b/components/containmentlayoutmanager/appletslayout.cpp @@ -1,703 +1,742 @@ /* * Copyright 2019 by 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. * */ #include "appletslayout.h" #include "appletcontainer.h" #include "gridlayoutmanager.h" #include #include #include #include // Plasma #include #include #include AppletsLayout::AppletsLayout(QQuickItem *parent) : QQuickItem(parent) { m_layoutManager = new GridLayoutManager(this); setFlags(QQuickItem::ItemIsFocusScope); setAcceptedMouseButtons(Qt::LeftButton); m_saveLayoutTimer = new QTimer(this); m_saveLayoutTimer->setSingleShot(true); m_saveLayoutTimer->setInterval(100); connect(m_layoutManager, &AbstractLayoutManager::layoutNeedsSaving, m_saveLayoutTimer, QOverload<>::of(&QTimer::start)); connect(m_saveLayoutTimer, &QTimer::timeout, this, [this] () { if (!m_configKey.isEmpty() && m_containment && m_containment->corona()->isStartupCompleted()) { const QString serializedConfig = m_layoutManager->serializeLayout(); m_containment->config().writeEntry(m_configKey, serializedConfig); //FIXME: something more efficient m_layoutManager->parseLayout(serializedConfig); m_savedSize = size(); m_containment->corona()->requireConfigSync(); } }); m_configKeyChangeTimer = new QTimer(this); m_configKeyChangeTimer->setSingleShot(true); m_configKeyChangeTimer->setInterval(100); connect(m_configKeyChangeTimer, &QTimer::timeout, this, [this] () { if (!m_configKey.isEmpty() && m_containment) { m_layoutManager->parseLayout(m_containment->config().readEntry(m_configKey, "")); if (width() > 0 && height() > 0) { m_layoutManager->resetLayoutFromConfig(); m_savedSize = size(); } } }); m_pressAndHoldTimer = new QTimer(this); m_pressAndHoldTimer->setSingleShot(true); connect(m_pressAndHoldTimer, &QTimer::timeout, this, [this]() { setEditMode(true); }); m_sizeSyncTimer = new QTimer(this); m_sizeSyncTimer->setSingleShot(true); m_sizeSyncTimer->setInterval(150); connect(m_sizeSyncTimer, &QTimer::timeout, this, [this]() { const QRect newGeom(x(), y(), width(), height()); // The size has been restored from the last one it has been saved: restore that exact same layout if (newGeom.size() == m_savedSize) { m_layoutManager->resetLayoutFromConfig(); // If the resize is consequence of a screen resolution change, queue a relayout maintaining the distance between screen edges } else if (!m_geometryBeforeResolutionChange.isEmpty()) { m_layoutManager->layoutGeometryChanged(newGeom, m_geometryBeforeResolutionChange); m_geometryBeforeResolutionChange = QRectF(); // Heuristically relayout items only when the plasma startup is fully completed } else { polish(); } }); - - m_window = window(); - if (m_window) { - connect(m_window, &QWindow::activeChanged, this, [this]() { - if (!m_window->isActive()) { - setEditMode(false); - } - }); - } - connect(this, &QQuickItem::windowChanged, this, [this]() { - if (m_window) { - disconnect(m_window, &QWindow::activeChanged, this, nullptr); - } - m_window = window(); - if (m_window) { - connect(m_window, &QWindow::activeChanged, this, [this]() { - if (!m_window->isActive()) { - setEditMode(false); - } - }); - } - }); } AppletsLayout::~AppletsLayout() { } PlasmaQuick::AppletQuickItem *AppletsLayout::containment() const { return m_containmentItem; } void AppletsLayout::setContainment(PlasmaQuick::AppletQuickItem *containmentItem) { // Forbid changing containmentItem at runtime if (m_containmentItem || containmentItem == m_containmentItem || !containmentItem->applet() || !containmentItem->applet()->isContainment()) { qWarning() << "Error: cannot change the containment to AppletsLayout"; return; } // Can't assign containments that aren't parents QQuickItem *candidate = parentItem(); while (candidate) { if (candidate == m_containmentItem) { break; } candidate = candidate->parentItem(); } if (candidate != m_containmentItem) { return; } m_containmentItem = containmentItem; m_containment = static_cast(m_containmentItem->applet()); connect(m_containmentItem, SIGNAL(appletAdded(QObject *, int, int)), this, SLOT(appletAdded(QObject *, int, int))); connect(m_containmentItem, SIGNAL(appletRemoved(QObject *)), this, SLOT(appletRemoved(QObject *))); emit containmentChanged(); } QString AppletsLayout::configKey() const { return m_configKey; } void AppletsLayout::setConfigKey(const QString &key) { if (m_configKey == key) { return; } m_configKey = key; // Reloading everything from the new config is expansive, event compress it m_configKeyChangeTimer->start(); emit configKeyChanged(); } QJSValue AppletsLayout::acceptsAppletCallback() const { return m_acceptsAppletCallback; } qreal AppletsLayout::minimumItemWidth() const { return m_minimumItemSize.width(); } void AppletsLayout::setMinimumItemWidth(qreal width) { if (qFuzzyCompare(width, m_minimumItemSize.width())) { return; } m_minimumItemSize.setWidth(width); emit minimumItemWidthChanged(); } qreal AppletsLayout::minimumItemHeight() const { return m_minimumItemSize.height(); } void AppletsLayout::setMinimumItemHeight(qreal height) { if (qFuzzyCompare(height, m_minimumItemSize.height())) { return; } m_minimumItemSize.setHeight(height); emit minimumItemHeightChanged(); } qreal AppletsLayout::defaultItemWidth() const { return m_defaultItemSize.width(); } void AppletsLayout::setDefaultItemWidth(qreal width) { if (qFuzzyCompare(width, m_defaultItemSize.width())) { return; } m_defaultItemSize.setWidth(width); emit defaultItemWidthChanged(); } qreal AppletsLayout::defaultItemHeight() const { return m_defaultItemSize.height(); } void AppletsLayout::setDefaultItemHeight(qreal height) { if (qFuzzyCompare(height, m_defaultItemSize.height())) { return; } m_defaultItemSize.setHeight(height); emit defaultItemHeightChanged(); } qreal AppletsLayout::cellWidth() const { return m_layoutManager->cellSize().width(); } void AppletsLayout::setCellWidth(qreal width) { if (qFuzzyCompare(width, m_layoutManager->cellSize().width())) { return; } m_layoutManager->setCellSize(QSizeF(width, m_layoutManager->cellSize().height())); emit cellWidthChanged(); } qreal AppletsLayout::cellHeight() const { return m_layoutManager->cellSize().height(); } void AppletsLayout::setCellHeight(qreal height) { if (qFuzzyCompare(height, m_layoutManager->cellSize().height())) { return; } m_layoutManager->setCellSize(QSizeF(m_layoutManager->cellSize().width(), height)); emit cellHeightChanged(); } void AppletsLayout::setAcceptsAppletCallback(const QJSValue& callback) { if (m_acceptsAppletCallback.strictlyEquals(callback)) { return; } if (!callback.isNull() && !callback.isCallable()) { return; } m_acceptsAppletCallback = callback; Q_EMIT acceptsAppletCallbackChanged(); } QQmlComponent *AppletsLayout::appletContainerComponent() const { return m_appletContainerComponent; } void AppletsLayout::setAppletContainerComponent(QQmlComponent *component) { if (m_appletContainerComponent == component) { return; } m_appletContainerComponent = component; emit appletContainerComponentChanged(); } AppletsLayout::EditModeCondition AppletsLayout::editModeCondition() const { return m_editModeCondition; } void AppletsLayout::setEditModeCondition(AppletsLayout::EditModeCondition condition) { if (m_editModeCondition == condition) { return; } if (m_editModeCondition == Locked) { setEditMode(false); } m_editModeCondition = condition; emit editModeConditionChanged(); } bool AppletsLayout::editMode() const { return m_editMode; } void AppletsLayout::setEditMode(bool editMode) { if (m_editMode == editMode) { return; } m_editMode = editMode; emit editModeChanged(); } ItemContainer *AppletsLayout::placeHolder() const { return m_placeHolder; } void AppletsLayout::setPlaceHolder(ItemContainer *placeHolder) { if (m_placeHolder == placeHolder) { return; } m_placeHolder = placeHolder; m_placeHolder->setParentItem(this); m_placeHolder->setZ(9999); m_placeHolder->setOpacity(false); emit placeHolderChanged(); } +QQuickItem *AppletsLayout::eventManagerToFilter() const +{ + return m_eventManagerToFilter; +} + +void AppletsLayout::setEventManagerToFilter(QQuickItem *item) +{ + if (m_eventManagerToFilter == item) { + return; + } + + m_eventManagerToFilter = item; + setFiltersChildMouseEvents(m_eventManagerToFilter); + emit eventManagerToFilterChanged(); +} + + void AppletsLayout::save() { m_saveLayoutTimer->start(); } void AppletsLayout::showPlaceHolderAt(const QRectF &geom) { if (!m_placeHolder) { return; } m_placeHolder->setPosition(geom.topLeft()); m_placeHolder->setSize(geom.size()); m_layoutManager->positionItem(m_placeHolder); m_placeHolder->setProperty("opacity", 1); } void AppletsLayout::showPlaceHolderForItem(ItemContainer *item) { if (!m_placeHolder) { return; } m_placeHolder->setPreferredLayoutDirection(item->preferredLayoutDirection()); m_placeHolder->setPosition(item->position()); m_placeHolder->setSize(item->size()); m_layoutManager->positionItem(m_placeHolder); m_placeHolder->setProperty("opacity", 1); } void AppletsLayout::hidePlaceHolder() { if (!m_placeHolder) { return; } m_placeHolder->setProperty("opacity", 0); } bool AppletsLayout::isRectAvailable(qreal x, qreal y, qreal width, qreal height) { return m_layoutManager->isRectAvailable(QRectF(x, y, width, height)); } bool AppletsLayout::itemIsManaged(ItemContainer *item) { if (!item) { return false; } return m_layoutManager->itemIsManaged(item); } void AppletsLayout::positionItem(ItemContainer *item) { if (!item) { return; } item->setParent(this); m_layoutManager->positionItemAndAssign(item); } void AppletsLayout::restoreItem(ItemContainer *item) { m_layoutManager->restoreItem(item); } void AppletsLayout::releaseSpace(ItemContainer *item) { if (!item) { return; } m_layoutManager->releaseSpace(item); } void AppletsLayout::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) { // Ignore completely moves without resize if (newGeometry.size() == oldGeometry.size()) { QQuickItem::geometryChanged(newGeometry, oldGeometry); return; } // Don't care for anything happening before startup completion if (!m_containment || !m_containment->corona() || !m_containment->corona()->isStartupCompleted()) { QQuickItem::geometryChanged(newGeometry, oldGeometry); return; } // Only do a layouting procedure if we received a valid size if (!newGeometry.isEmpty()) { m_sizeSyncTimer->start(); } QQuickItem::geometryChanged(newGeometry, oldGeometry); } void AppletsLayout::updatePolish() { m_layoutManager->resetLayout(); m_savedSize = size(); } void AppletsLayout::componentComplete() { if (!m_containment || !m_containmentItem) { QQuickItem::componentComplete(); return; } if (!m_configKey.isEmpty()) { m_layoutManager->parseLayout(m_containment->config().readEntry(m_configKey, "")); } QList appletObjects = m_containmentItem->property("applets").value >(); for (auto *obj : appletObjects) { PlasmaQuick::AppletQuickItem *appletItem = qobject_cast(obj); if (!obj) { continue; } AppletContainer *container = createContainerForApplet(appletItem); if (width() > 0 && height() > 0) { m_layoutManager->positionItemAndAssign(container); } } //layout all extra non applet items if (width() > 0 && height() > 0) { for (auto *child : childItems()) { ItemContainer *item = qobject_cast(child); if (item && item != m_placeHolder && !m_layoutManager->itemIsManaged(item)) { m_layoutManager->positionItemAndAssign(item); } } } if (m_containment && m_containment->corona()) { connect(m_containment->corona(), &Plasma::Corona::startupCompleted, this, [this](){ // m_savedSize = size(); }); // When the screen geometry changes, we need to know the geometry just before it did, so we can apply out heuristic of keeping the distance with borders constant connect(m_containment->corona(), &Plasma::Corona::screenGeometryChanged, this, [this](int id){ if (m_containment->screen() == id) { m_geometryBeforeResolutionChange = QRectF(x(), y(), width(), height()); } }); } QQuickItem::componentComplete(); } +bool AppletsLayout::childMouseEventFilter(QQuickItem *item, QEvent *event) +{ + if (item != m_eventManagerToFilter) { + return QQuickItem::childMouseEventFilter(item, event); + } + + switch (event->type()) { + case QEvent::MouseButtonPress: { + QMouseEvent *me = static_cast(event); + if (me->buttons() & Qt::LeftButton) { + mousePressEvent(me); + } + break; + } + case QEvent::MouseMove: { + QMouseEvent *me = static_cast(event); + mouseMoveEvent(me); + break; + } + case QEvent::MouseButtonRelease: { + QMouseEvent *me = static_cast(event); + mouseReleaseEvent(me); + break; + } + case QEvent::UngrabMouse: + mouseUngrabEvent(); + break; + default: + break; + } + + return QQuickItem::childMouseEventFilter(item, event); +} void AppletsLayout::mousePressEvent(QMouseEvent *event) { forceActiveFocus(Qt::MouseFocusReason); if (!m_editMode && m_editModeCondition == AppletsLayout::Manual) { return; } if (!m_editMode && m_editModeCondition == AppletsLayout::AfterPressAndHold) { m_pressAndHoldTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval()); } m_mouseDownWasEditMode = m_editMode; m_mouseDownPosition = event->windowPos(); //event->setAccepted(false); } void AppletsLayout::mouseMoveEvent(QMouseEvent *event) { if (!m_editMode && m_editModeCondition == AppletsLayout::Manual) { return; } if (!m_editMode && QPointF(event->windowPos() - m_mouseDownPosition).manhattanLength() >= QGuiApplication::styleHints()->startDragDistance()) { m_pressAndHoldTimer->stop(); } } void AppletsLayout::mouseReleaseEvent(QMouseEvent *event) { if (m_editMode && m_mouseDownWasEditMode + // By only accepting synthetyzed events, this makes the + // close by tapping in any empty area only work with real + // touch events, as we want a different behavior between desktop + // and tablet mode + && (event->source() == Qt::MouseEventSynthesizedBySystem + || event->source() == Qt::MouseEventSynthesizedByQt) && QPointF(event->windowPos() - m_mouseDownPosition).manhattanLength() < QGuiApplication::styleHints()->startDragDistance()) { setEditMode(false); } m_pressAndHoldTimer->stop(); if (!m_editMode) { for (auto *child : childItems()) { ItemContainer *item = qobject_cast(child); if (item && item != m_placeHolder) { item->setEditMode(false); } } } } +void AppletsLayout::mouseUngrabEvent() +{ + m_pressAndHoldTimer->stop(); +} + void AppletsLayout::appletAdded(QObject *applet, int x, int y) { PlasmaQuick::AppletQuickItem *appletItem = qobject_cast(applet); //maybe even an assert? if (!appletItem) { return; } if (m_acceptsAppletCallback.isCallable()) { QQmlEngine *engine = QQmlEngine::contextForObject(this)->engine(); Q_ASSERT(engine); QJSValueList args; args << engine->newQObject(applet) << QJSValue(x) << QJSValue(y); if (!m_acceptsAppletCallback.call(args).toBool()) { emit appletRefused(applet, x, y); return; } } AppletContainer *container = createContainerForApplet(appletItem); container->setPosition(QPointF(x, y)); container->setVisible(true); m_layoutManager->positionItemAndAssign(container); } void AppletsLayout::appletRemoved(QObject *applet) { PlasmaQuick::AppletQuickItem *appletItem = qobject_cast(applet); //maybe even an assert? if (!appletItem) { return; } AppletContainer *container = m_containerForApplet.value(appletItem); if (!container) { return; } m_layoutManager->releaseSpace(container); m_containerForApplet.remove(appletItem); appletItem->setParentItem(this); container->deleteLater(); } AppletContainer *AppletsLayout::createContainerForApplet(PlasmaQuick::AppletQuickItem *appletItem) { AppletContainer *container = m_containerForApplet.value(appletItem); if (container) { return container; } bool createdFromQml = true; if (m_appletContainerComponent) { QQmlContext *context = QQmlEngine::contextForObject(this); Q_ASSERT(context); QObject *instance = m_appletContainerComponent->beginCreate(context); container = qobject_cast(instance); if (container) { container->setParentItem(this); } else { qWarning() << "Error: provided component not an AppletContainer instance"; if (instance) { instance->deleteLater(); } createdFromQml = false; } } if (!container) { container = new AppletContainer(this); } container->setVisible(false); const QSizeF appletSize = appletItem->size(); container->setContentItem(appletItem); m_containerForApplet[appletItem] = container; container->setLayout(this); container->setKey(QLatin1String("Applet-") + QString::number(appletItem->applet()->id())); const bool geometryWasSaved = m_layoutManager->restoreItem(container); if (!geometryWasSaved) { container->setPosition(QPointF(appletItem->x() - container->leftPadding(), appletItem->y() - container->topPadding())); if (!appletSize.isEmpty()) { container->setSize(QSizeF(qMax(m_minimumItemSize.width(), appletSize.width() + container->leftPadding() + container->rightPadding()), qMax(m_minimumItemSize.height(), appletSize.height() + container->topPadding() + container->bottomPadding()))); } } if (m_appletContainerComponent && createdFromQml) { m_appletContainerComponent->completeCreate(); } //NOTE: This has to be done here as we need the component completed to have all the bindings evaluated if (!geometryWasSaved && appletSize.isEmpty()) { if (container->initialSize().width() > m_minimumItemSize.width() && container->initialSize().height() > m_minimumItemSize.height()) { const QSizeF size = m_layoutManager->cellAlignedContainingSize( container->initialSize()); container->setSize(size); } else { container->setSize(QSizeF(qMax(m_minimumItemSize.width(), m_defaultItemSize.width()), qMax(m_minimumItemSize.height(), m_defaultItemSize.height()))); } } container->setVisible(true); appletItem->setVisible(true); return container; } #include "moc_appletslayout.cpp" diff --git a/components/containmentlayoutmanager/appletslayout.h b/components/containmentlayoutmanager/appletslayout.h index 55c37612c..fe65dcc97 100644 --- a/components/containmentlayoutmanager/appletslayout.h +++ b/components/containmentlayoutmanager/appletslayout.h @@ -1,213 +1,226 @@ /* * Copyright 2019 by 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. * */ #pragma once #include #include #include #include class QTimer; namespace Plasma { class Containment; } namespace PlasmaQuick { class AppletQuickItem; } class AbstractLayoutManager; class AppletContainer; class ItemContainer; class AppletsLayout: public QQuickItem { Q_OBJECT Q_INTERFACES(QQmlParserStatus) Q_PROPERTY(QString configKey READ configKey WRITE setConfigKey NOTIFY configKeyChanged) Q_PROPERTY(PlasmaQuick::AppletQuickItem *containment READ containment WRITE setContainment NOTIFY containmentChanged) Q_PROPERTY(QJSValue acceptsAppletCallback READ acceptsAppletCallback WRITE setAcceptsAppletCallback NOTIFY acceptsAppletCallbackChanged) Q_PROPERTY(qreal minimumItemWidth READ minimumItemWidth WRITE setMinimumItemWidth NOTIFY minimumItemWidthChanged) Q_PROPERTY(qreal minimumItemHeight READ minimumItemHeight WRITE setMinimumItemHeight NOTIFY minimumItemHeightChanged) Q_PROPERTY(qreal defaultItemWidth READ defaultItemWidth WRITE setDefaultItemWidth NOTIFY defaultItemWidthChanged) Q_PROPERTY(qreal defaultItemHeight READ defaultItemHeight WRITE setDefaultItemHeight NOTIFY defaultItemHeightChanged) Q_PROPERTY(qreal cellWidth READ cellWidth WRITE setCellWidth NOTIFY cellWidthChanged) Q_PROPERTY(qreal cellHeight READ cellHeight WRITE setCellHeight NOTIFY cellHeightChanged) Q_PROPERTY(QQmlComponent *appletContainerComponent READ appletContainerComponent WRITE setAppletContainerComponent NOTIFY appletContainerComponentChanged) Q_PROPERTY(ItemContainer *placeHolder READ placeHolder WRITE setPlaceHolder NOTIFY placeHolderChanged); + /** + * if the applets layout contains some kind of main MouseArea, + * MouseEventListener or Flickable, we want to filter its events to make the + * long mouse press work + */ + Q_PROPERTY(QQuickItem *eventManagerToFilter READ eventManagerToFilter WRITE setEventManagerToFilter NOTIFY eventManagerToFilterChanged); + Q_PROPERTY(AppletsLayout::EditModeCondition editModeCondition READ editModeCondition WRITE setEditModeCondition NOTIFY editModeConditionChanged) Q_PROPERTY(bool editMode READ editMode WRITE setEditMode NOTIFY editModeChanged) public: enum PreferredLayoutDirection { Closest = 0, LeftToRight, RightToLeft, TopToBottom, BottomToTop }; Q_ENUM(PreferredLayoutDirection) enum EditModeCondition { Locked = 0, Manual, AfterPressAndHold, }; Q_ENUM(EditModeCondition) AppletsLayout(QQuickItem *parent = nullptr); ~AppletsLayout(); // QML setters and getters QString configKey() const; void setConfigKey(const QString &key); PlasmaQuick::AppletQuickItem *containment() const; void setContainment(PlasmaQuick::AppletQuickItem *containment); QJSValue acceptsAppletCallback() const; void setAcceptsAppletCallback(const QJSValue& callback); qreal minimumItemWidth() const; void setMinimumItemWidth(qreal width); qreal minimumItemHeight() const; void setMinimumItemHeight(qreal height); qreal defaultItemWidth() const; void setDefaultItemWidth(qreal width); qreal defaultItemHeight() const; void setDefaultItemHeight(qreal height); qreal cellWidth() const; void setCellWidth(qreal width); qreal cellHeight() const; void setCellHeight(qreal height); QQmlComponent *appletContainerComponent() const; void setAppletContainerComponent(QQmlComponent *component); ItemContainer *placeHolder() const; void setPlaceHolder(ItemContainer *placeHolder); + QQuickItem *eventManagerToFilter() const; + void setEventManagerToFilter(QQuickItem *item); + EditModeCondition editModeCondition() const; void setEditModeCondition(EditModeCondition condition); bool editMode() const; void setEditMode(bool edit); Q_INVOKABLE void save(); Q_INVOKABLE void showPlaceHolderAt(const QRectF &geom); Q_INVOKABLE void showPlaceHolderForItem(ItemContainer *item); Q_INVOKABLE void hidePlaceHolder(); Q_INVOKABLE bool isRectAvailable(qreal x, qreal y, qreal width, qreal height); Q_INVOKABLE bool itemIsManaged(ItemContainer *item); Q_INVOKABLE void positionItem(ItemContainer *item); Q_INVOKABLE void restoreItem(ItemContainer *item); Q_INVOKABLE void releaseSpace(ItemContainer *item); Q_SIGNALS: /** * An applet has been refused by the layout: acceptsAppletCallback * returned false and will need to be managed in a different way */ void appletRefused(QObject *applet, int x, int y); void configKeyChanged(); void containmentChanged(); void minimumItemWidthChanged(); void minimumItemHeightChanged(); void defaultItemWidthChanged(); void defaultItemHeightChanged(); void cellWidthChanged(); void cellHeightChanged(); void acceptsAppletCallbackChanged(); void appletContainerComponentChanged(); void placeHolderChanged(); + void eventManagerToFilterChanged(); void editModeConditionChanged(); void editModeChanged(); protected: + bool childMouseEventFilter(QQuickItem *item, QEvent *event) override; void updatePolish() override; void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; //void classBegin() override; void componentComplete() override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; + void mouseUngrabEvent() override; private Q_SLOTS: void appletAdded(QObject *applet, int x, int y); void appletRemoved(QObject *applet); private: AppletContainer *createContainerForApplet(PlasmaQuick::AppletQuickItem *appletItem); QString m_configKey; QTimer *m_saveLayoutTimer; QTimer *m_configKeyChangeTimer; PlasmaQuick::AppletQuickItem *m_containmentItem = nullptr; Plasma::Containment *m_containment = nullptr; QQmlComponent *m_appletContainerComponent = nullptr; AbstractLayoutManager *m_layoutManager = nullptr; QPointer m_placeHolder; + QPointer m_eventManagerToFilter; QTimer *m_pressAndHoldTimer; QTimer *m_sizeSyncTimer; QJSValue m_acceptsAppletCallback; AppletsLayout::EditModeCondition m_editModeCondition = AppletsLayout::Manual; QHash m_containerForApplet; - QPointer m_window; QSizeF m_minimumItemSize; QSizeF m_defaultItemSize; QSizeF m_savedSize; QRectF m_geometryBeforeResolutionChange; QPointF m_mouseDownPosition = QPoint(-1, -1); bool m_mouseDownWasEditMode = false; bool m_editMode = false; }; diff --git a/components/containmentlayoutmanager/itemcontainer.cpp b/components/containmentlayoutmanager/itemcontainer.cpp index 6cb7dd18a..2cf0e6b21 100644 --- a/components/containmentlayoutmanager/itemcontainer.cpp +++ b/components/containmentlayoutmanager/itemcontainer.cpp @@ -1,780 +1,787 @@ /* * Copyright 2019 by 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. * */ #include "itemcontainer.h" #include "configoverlay.h" #include #include #include #include #include #include #include #include ItemContainer::ItemContainer(QQuickItem *parent) : QQuickItem(parent) { setFiltersChildMouseEvents(true); setFlags(QQuickItem::ItemIsFocusScope); setActiveFocusOnTab(true); setAcceptedMouseButtons(Qt::LeftButton); setLayout(qobject_cast(parent)); m_editModeTimer = new QTimer(this); m_editModeTimer->setSingleShot(true); connect(this, &QQuickItem::parentChanged, this, [this]() { setLayout(qobject_cast(parentItem())); }); connect(m_editModeTimer, &QTimer::timeout, this, [this]() { setEditMode(true); }); setKeepMouseGrab(true); m_sizeHintAdjustTimer = new QTimer(this); m_sizeHintAdjustTimer->setSingleShot(true); m_sizeHintAdjustTimer->setInterval(0); connect(m_sizeHintAdjustTimer, &QTimer::timeout, this, &ItemContainer::sizeHintsChanged); } ItemContainer::~ItemContainer() { if (m_contentItem) { m_contentItem->setEnabled(true); } } QString ItemContainer::key() const { return m_key; } void ItemContainer::setKey(const QString &key) { if (m_key == key) { return; } m_key = key; emit keyChanged(); } bool ItemContainer::editMode() const { return m_editMode; } bool ItemContainer::dragActive() const { return m_dragActive; } void ItemContainer::setEditMode(bool editMode) { if (m_editMode == editMode) { return; } if (editMode && editModeCondition() == Locked) { return; } m_editMode = editMode; - // Leave this decision to QML? - if (m_editModeCondition != AfterMouseOver || m_layout->editMode()) { + if (m_editModeCondition != AfterMouseOver || (m_layout && m_layout->editMode())) { m_contentItem->setEnabled(!editMode); } if (editMode) { setZ(1); } else { setZ(0); } if (m_mouseDown) { sendUngrabRecursive(m_contentItem); grabMouse(); } if (m_dragActive != editMode && m_mouseDown) { m_dragActive = editMode && m_mouseDown; emit dragActiveChanged(); } setConfigOverlayVisible(editMode); emit editModeChanged(editMode); } ItemContainer::EditModeCondition ItemContainer::editModeCondition() const { if (m_layout && m_layout->editModeCondition() == AppletsLayout::Locked) { return Locked; } return m_editModeCondition; } void ItemContainer::setEditModeCondition(EditModeCondition condition) { if (condition == m_editModeCondition) { return; } if (condition == Locked) { setEditMode(false); } m_editModeCondition = condition; - setAcceptHoverEvents(condition == AfterMouseOver); + setAcceptHoverEvents(condition == AfterMouseOver || (m_layout && m_layout->editMode())); emit editModeConditionChanged(); } AppletsLayout::PreferredLayoutDirection ItemContainer::preferredLayoutDirection() const { return m_preferredLayoutDirection; } void ItemContainer::setPreferredLayoutDirection(AppletsLayout::PreferredLayoutDirection direction) { if (direction == m_preferredLayoutDirection) { return; } m_preferredLayoutDirection = direction; emit preferredLayoutDirectionChanged(); } void ItemContainer::setLayout(AppletsLayout *layout) { if (m_layout == layout) { return; } if (m_layout) { disconnect(m_layout, &AppletsLayout::editModeConditionChanged, this, nullptr); + disconnect(m_layout, &AppletsLayout::editModeChanged, this, nullptr); if (m_editMode) { m_layout->hidePlaceHolder(); } } m_layout = layout; if (!layout) { emit layoutChanged(); return; } if (parentItem() != layout) { setParentItem(layout); } connect(m_layout, &AppletsLayout::editModeConditionChanged, this, [this]() { if (m_layout->editModeCondition() == AppletsLayout::Locked) { setEditMode(false); } if ((m_layout->editModeCondition() == AppletsLayout::Locked) != (m_editModeCondition == ItemContainer::Locked)) { emit editModeConditionChanged(); } }); + connect(m_layout, &AppletsLayout::editModeChanged, this, [this]() { + setAcceptHoverEvents(m_editModeCondition == AfterMouseOver || m_layout->editMode()); + }); emit layoutChanged(); } AppletsLayout *ItemContainer::layout() const { return m_layout; } void ItemContainer::syncChildItemsGeometry(const QSizeF &size) { if (m_contentItem) { m_contentItem->setPosition(QPointF(m_leftPadding, m_topPadding)); m_contentItem->setSize(QSizeF(size.width() - m_leftPadding - m_rightPadding, size.height() - m_topPadding - m_bottomPadding)); } if (m_backgroundItem) { m_backgroundItem->setPosition(QPointF(0, 0)); m_backgroundItem->setSize(size); } if (m_configOverlay) { m_configOverlay->setPosition(QPointF(0, 0)); m_configOverlay->setSize(size); } } QQmlComponent *ItemContainer::configOverlayComponent() const { return m_configOverlayComponent; } void ItemContainer::setConfigOverlayComponent(QQmlComponent *component) { if (component == m_configOverlayComponent) { return; } m_configOverlayComponent = component; if (m_configOverlay) { m_configOverlay->deleteLater(); m_configOverlay = nullptr; } emit configOverlayComponentChanged(); } ConfigOverlay *ItemContainer::configOverlayItem() const { return m_configOverlay; } QSizeF ItemContainer::initialSize() const { return m_initialSize; } void ItemContainer::setInitialSize(const QSizeF &size) { if (m_initialSize == size) { return; } m_initialSize = size; emit initialSizeChanged(); } bool ItemContainer::configOverlayVisible() const { return m_configOverlay && m_configOverlay->open(); } void ItemContainer::setConfigOverlayVisible(bool visible) { if (!m_configOverlayComponent) { return; } if (visible == configOverlayVisible()) { return; } if (visible && !m_configOverlay) { QQmlContext *context = QQmlEngine::contextForObject(this); Q_ASSERT(context); QObject *instance = m_configOverlayComponent->beginCreate(context); m_configOverlay = qobject_cast(instance); if (!m_configOverlay) { qWarning() << "Error: Applet configOverlay not of ConfigOverlay type"; if (instance) { instance->deleteLater(); } return; } m_configOverlay->setVisible(false); m_configOverlay->setItemContainer(this); m_configOverlay->setParentItem(this); m_configOverlay->setTouchInteraction(m_mouseSynthetizedFromTouch); m_configOverlay->setZ(999); m_configOverlay->setPosition(QPointF(0, 0)); m_configOverlay->setSize(size()); m_configOverlayComponent->completeCreate(); connect(m_configOverlay, &ConfigOverlay::openChanged, this, [this]() { emit configOverlayVisibleChanged(m_configOverlay->open()); }); emit configOverlayItemChanged(); } if (m_configOverlay) { m_configOverlay->setOpen(visible); } } void ItemContainer::contentData_append(QQmlListProperty *prop, QObject *object) { ItemContainer *container = static_cast(prop->object); if (!container) { return; } // QQuickItem *item = qobject_cast(object); container->m_contentData.append(object); } int ItemContainer::contentData_count(QQmlListProperty *prop) { ItemContainer *container = static_cast(prop->object); if (!container) { return 0; } return container->m_contentData.count(); } QObject *ItemContainer::contentData_at(QQmlListProperty *prop, int index) { ItemContainer *container = static_cast(prop->object); if (!container) { return nullptr; } if (index < 0 || index >= container->m_contentData.count()) { return nullptr; } return container->m_contentData.value(index); } void ItemContainer::contentData_clear(QQmlListProperty *prop) { ItemContainer *container = static_cast(prop->object); if (!container) { return; } return container->m_contentData.clear(); } QQmlListProperty ItemContainer::contentData() { return QQmlListProperty(this, nullptr, contentData_append, contentData_count, contentData_at, contentData_clear); } void ItemContainer::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) { syncChildItemsGeometry(newGeometry.size()); QQuickItem::geometryChanged(newGeometry, oldGeometry); emit contentWidthChanged(); emit contentHeightChanged(); } void ItemContainer::componentComplete() { if (!m_contentItem) { //qWarning()<<"Creating default contentItem"; m_contentItem = new QQuickItem(this); syncChildItemsGeometry(size()); } for (auto *o : m_contentData) { QQuickItem *item = qobject_cast(o); if (item) { item->setParentItem(m_contentItem); } } // Search for the Layout attached property // Qt6: this should become public api // https://bugreports.qt.io/browse/QTBUG-77103 for (auto *o : children()) { if (o->inherits("QQuickLayoutAttached")) { m_layoutAttached = o; } } if (m_layoutAttached) { //NOTE: new syntax cannot be used because we don't have access to the QQuickLayoutAttached class connect(m_layoutAttached, SIGNAL(minimumHeightChanged()), m_sizeHintAdjustTimer, SLOT(start())); connect(m_layoutAttached, SIGNAL(minimumWidthChanged()), m_sizeHintAdjustTimer, SLOT(start())); connect(m_layoutAttached, SIGNAL(preferredHeightChanged()), m_sizeHintAdjustTimer, SLOT(start())); connect(m_layoutAttached, SIGNAL(preferredWidthChanged()), m_sizeHintAdjustTimer, SLOT(start())); connect(m_layoutAttached, SIGNAL(maximumHeightChanged()), m_sizeHintAdjustTimer, SLOT(start())); connect(m_layoutAttached, SIGNAL(maximumWidthChanged()), m_sizeHintAdjustTimer, SLOT(start())); } QQuickItem::componentComplete(); } void ItemContainer::sendUngrabRecursive(QQuickItem *item) { if (!item || !item->window()) { return; } for (auto *child : item->childItems()) { sendUngrabRecursive(child); } QEvent ev(QEvent::UngrabMouse); item->window()->sendEvent(item, &ev); } bool ItemContainer::childMouseEventFilter(QQuickItem *item, QEvent *event) { // Don't filter the configoverlay if (item == m_configOverlay || (m_configOverlay && m_configOverlay->isAncestorOf(item)) || (!m_editMode && m_editModeCondition == Manual)) { return QQuickItem::childMouseEventFilter(item, event); } //give more time before closing if (m_closeEditModeTimer && m_closeEditModeTimer->isActive()) { m_closeEditModeTimer->start(); } if (event->type() == QEvent::MouseButtonPress) { QMouseEvent *me = static_cast(event); if (me->button() != Qt::LeftButton && !(me->buttons() & Qt::LeftButton)) { return QQuickItem::childMouseEventFilter(item, event); } forceActiveFocus(Qt::MouseFocusReason); m_mouseDown = true; m_mouseSynthetizedFromTouch = me->source() == Qt::MouseEventSynthesizedBySystem || me->source() == Qt::MouseEventSynthesizedByQt; if (m_configOverlay) { m_configOverlay->setTouchInteraction(m_mouseSynthetizedFromTouch); } const bool wasEditMode = m_editMode; if (m_layout && m_layout->editMode()) { setEditMode(true); } else if (m_editModeCondition == AfterPressAndHold) { m_editModeTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval()); } m_lastMousePosition = me->windowPos(); m_mouseDownPosition = me->windowPos(); if (m_editMode && !wasEditMode) { event->accept(); return true; } } else if (event->type() == QEvent::MouseMove) { QMouseEvent *me = static_cast(event); if (!m_editMode && QPointF(me->windowPos() - m_mouseDownPosition).manhattanLength() >= QGuiApplication::styleHints()->startDragDistance()) { m_editModeTimer->stop(); } else if (m_editMode) { event->accept(); } } else if (event->type() == QEvent::MouseButtonRelease) { m_editModeTimer->stop(); m_mouseDown = false; m_mouseSynthetizedFromTouch = false; ungrabMouse(); event->accept(); m_dragActive = false; if (m_editMode) { emit dragActiveChanged(); } } return QQuickItem::childMouseEventFilter(item, event); } void ItemContainer::mousePressEvent(QMouseEvent *event) { forceActiveFocus(Qt::MouseFocusReason); if (!m_editMode && m_editModeCondition == Manual) { return; } m_mouseDown = true; m_mouseSynthetizedFromTouch = event->source() == Qt::MouseEventSynthesizedBySystem || event->source() == Qt::MouseEventSynthesizedByQt; if (m_configOverlay) { m_configOverlay->setTouchInteraction(m_mouseSynthetizedFromTouch); } if (m_layout && m_layout->editMode()) { setEditMode(true); } if (m_editMode) { grabMouse(); m_dragActive = true; emit dragActiveChanged(); } else if (m_editModeCondition == AfterPressAndHold) { m_editModeTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval()); } m_lastMousePosition = event->windowPos(); m_mouseDownPosition = event->windowPos(); event->accept(); } void ItemContainer::mouseReleaseEvent(QMouseEvent *event) { Q_UNUSED(event); if (!m_layout || (!m_editMode && m_editModeCondition == Manual)) { return; } m_mouseDown = false; m_mouseSynthetizedFromTouch = false; m_editModeTimer->stop(); ungrabMouse(); if (m_editMode && !m_layout->itemIsManaged(this)) { m_layout->hidePlaceHolder(); m_layout->positionItem(this); } m_dragActive = false; if (m_editMode) { emit dragActiveChanged(); } event->accept(); } void ItemContainer::mouseMoveEvent(QMouseEvent *event) { if ((event->button() == Qt::NoButton && event->buttons() == Qt::NoButton) || (!m_editMode && m_editModeCondition == Manual)) { return; } if (!m_editMode && QPointF(event->windowPos() - m_mouseDownPosition).manhattanLength() >= QGuiApplication::styleHints()->startDragDistance()) { if (m_editModeCondition == AfterPress) { setEditMode(true); } else { m_editModeTimer->stop(); } } if (!m_editMode) { return; } if (m_layout && m_layout->itemIsManaged(this)) { m_layout->releaseSpace(this); grabMouse(); m_dragActive = true; emit dragActiveChanged(); } else { setPosition(QPointF(x() + event->windowPos().x() - m_lastMousePosition.x(), y() + event->windowPos().y() - m_lastMousePosition.y())); if (m_layout) { m_layout->showPlaceHolderForItem(this); } emit userDrag(QPointF(x(), y()), event->pos()); } m_lastMousePosition = event->windowPos(); event->accept(); } void ItemContainer::mouseUngrabEvent() { m_mouseDown = false; m_mouseSynthetizedFromTouch = false; m_editModeTimer->stop(); ungrabMouse(); if (m_layout && m_editMode && !m_layout->itemIsManaged(this)) { m_layout->hidePlaceHolder(); m_layout->positionItem(this); } m_dragActive = false; if (m_editMode) { emit dragActiveChanged(); } } void ItemContainer::hoverEnterEvent(QHoverEvent *event) { Q_UNUSED(event); - if (m_editModeCondition != AfterMouseOver) { + if (m_editModeCondition != AfterMouseOver && !m_layout->editMode()) { return; } if (m_closeEditModeTimer) { m_closeEditModeTimer->stop(); } - m_editModeTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval()); + if (m_layout->editMode()) { + setEditMode(true); + } else { + m_editModeTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval()); + } } void ItemContainer::hoverLeaveEvent(QHoverEvent *event) { Q_UNUSED(event); - if (m_editModeCondition != AfterMouseOver) { + if (m_editModeCondition != AfterMouseOver && !m_layout->editMode()) { return; } m_editModeTimer->stop(); if (!m_closeEditModeTimer) { m_closeEditModeTimer = new QTimer(this); m_closeEditModeTimer->setSingleShot(true); m_closeEditModeTimer->setInterval(500); connect(m_closeEditModeTimer, &QTimer::timeout, this, [this] () { setEditMode(false); }); } m_closeEditModeTimer->start(); } QQuickItem *ItemContainer::contentItem() const { return m_contentItem; } void ItemContainer::setContentItem(QQuickItem *item) { if (m_contentItem == item) { return; } m_contentItem = item; item->setParentItem(this); m_contentItem->setPosition(QPointF(m_leftPadding, m_topPadding)); m_contentItem->setSize(QSizeF(width() - m_leftPadding - m_rightPadding, height() - m_topPadding - m_bottomPadding)); emit contentItemChanged(); } QQuickItem *ItemContainer::background() const { return m_backgroundItem; } void ItemContainer::setBackground(QQuickItem *item) { if (m_backgroundItem == item) { return; } m_backgroundItem = item; m_backgroundItem->setParentItem(this); m_backgroundItem->setPosition(QPointF(0, 0)); m_backgroundItem->setSize(size()); emit backgroundChanged(); } int ItemContainer::leftPadding() const { return m_leftPadding; } void ItemContainer::setLeftPadding(int padding) { if (m_leftPadding == padding) { return; } m_leftPadding = padding; syncChildItemsGeometry(size()); emit leftPaddingChanged(); emit contentWidthChanged(); } int ItemContainer::topPadding() const { return m_topPadding; } void ItemContainer::setTopPadding(int padding) { if (m_topPadding == padding) { return; } m_topPadding = padding; syncChildItemsGeometry(size()); emit topPaddingChanged(); emit contentHeightChanged(); } int ItemContainer::rightPadding() const { return m_rightPadding; } void ItemContainer::setRightPadding(int padding) { if (m_rightPadding == padding) { return; } m_rightPadding = padding; syncChildItemsGeometry(size()); emit rightPaddingChanged(); emit contentWidthChanged(); } int ItemContainer::bottomPadding() const { return m_bottomPadding; } void ItemContainer::setBottomPadding(int padding) { if (m_bottomPadding == padding) { return; } m_bottomPadding = padding; syncChildItemsGeometry(size()); emit bottomPaddingChanged(); emit contentHeightChanged(); } int ItemContainer::contentWidth() const { return width() - m_leftPadding - m_rightPadding; } int ItemContainer::contentHeight() const { return height() - m_topPadding - m_bottomPadding; } #include "moc_itemcontainer.cpp" diff --git a/containmentactions/contextmenu/menu.cpp b/containmentactions/contextmenu/menu.cpp index cfaf83453..243428790 100644 --- a/containmentactions/contextmenu/menu.cpp +++ b/containmentactions/contextmenu/menu.cpp @@ -1,314 +1,318 @@ /* * Copyright 2009 by Chani Armitage * * 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 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. */ #include "menu.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "kworkspace.h" #include "krunner_interface.h" #include "screensaver_interface.h" #ifdef Q_OS_WIN #define _WIN32_WINNT 0x0500 // require NT 5.0 (win 2k pro) #include #endif // Q_OS_WIN ContextMenu::ContextMenu(QObject *parent, const QVariantList &args) : Plasma::ContainmentActions(parent, args), m_runCommandAction(nullptr), m_lockScreenAction(nullptr), m_logoutAction(nullptr), m_separator1(nullptr), m_separator2(nullptr), m_separator3(nullptr), m_buttons(nullptr) { } ContextMenu::~ContextMenu() { } void ContextMenu::restore(const KConfigGroup &config) { Plasma::Containment *c = containment(); Q_ASSERT(c); m_actions.clear(); m_actionOrder.clear(); QHash actions; QSet disabled; if (c->containmentType() == Plasma::Types::PanelContainment || c->containmentType() == Plasma::Types::CustomPanelContainment) { - m_actionOrder << QStringLiteral("add widgets") << QStringLiteral("_add panel") << QStringLiteral("lock widgets") << QStringLiteral("_context") << QStringLiteral("configure") << QStringLiteral("remove"); + m_actionOrder << QStringLiteral("add widgets") << QStringLiteral("_add panel") << QStringLiteral("lock widgets") << QStringLiteral("edit mode") << QStringLiteral("_context") << QStringLiteral("configure") << QStringLiteral("remove"); } else { actions.insert(QStringLiteral("configure shortcuts"), false); m_actionOrder << QStringLiteral("_context") << QStringLiteral("_run_command") << QStringLiteral("add widgets") << QStringLiteral("_add panel") - << QStringLiteral("manage activities") << QStringLiteral("remove") << QStringLiteral("lock widgets") << QStringLiteral("_sep1") + << QStringLiteral("manage activities") << QStringLiteral("remove") << QStringLiteral("lock widgets") << QStringLiteral("edit mode") << QStringLiteral("_sep1") < it(actions); while (it.hasNext()) { it.next(); m_actions.insert(it.key(), config.readEntry(it.key(), it.value())); } // everything below should only happen once, so check for it if (!m_runCommandAction) { m_runCommandAction = new QAction(i18nc("plasma_containmentactions_contextmenu", "Show KRunner"), this); m_runCommandAction->setIcon(QIcon::fromTheme(QStringLiteral("plasma-search"))); m_runCommandAction->setShortcut(KGlobalAccel::self()->globalShortcut(QStringLiteral("krunner.desktop"), QStringLiteral("_launch")).value(0)); connect(m_runCommandAction, &QAction::triggered, this, &ContextMenu::runCommand); m_lockScreenAction = new QAction(i18nc("plasma_containmentactions_contextmenu", "Lock Screen"), this); m_lockScreenAction->setIcon(QIcon::fromTheme(QStringLiteral("system-lock-screen"))); m_lockScreenAction->setShortcut(KGlobalAccel::self()->globalShortcut(QStringLiteral("ksmserver"), QStringLiteral("Lock Session")).value(0)); connect(m_lockScreenAction, &QAction::triggered, this, &ContextMenu::lockScreen); m_logoutAction = new QAction(i18nc("plasma_containmentactions_contextmenu", "Leave..."), this); m_logoutAction->setIcon(QIcon::fromTheme(QStringLiteral("system-log-out"))); m_logoutAction->setShortcut(KGlobalAccel::self()->globalShortcut(QStringLiteral("ksmserver"), QStringLiteral("Log Out")).value(0)); connect(m_logoutAction, &QAction::triggered, this, &ContextMenu::startLogout); m_separator1 = new QAction(this); m_separator1->setSeparator(true); m_separator2 = new QAction(this); m_separator2->setSeparator(true); m_separator3 = new QAction(this); m_separator3->setSeparator(true); } } QList ContextMenu::contextualActions() { Plasma::Containment *c = containment(); Q_ASSERT(c); QList actions; foreach (const QString &name, m_actionOrder) { if (!m_actions.value(name)) { continue; } if (name == QLatin1String("_context")) { actions << c->contextualActions(); } if (name == QLatin1String("_wallpaper")) { if (!c->wallpaper().isEmpty()) { QObject *wallpaperGraphicsObject = c->property("wallpaperGraphicsObject").value(); if (wallpaperGraphicsObject) { actions << wallpaperGraphicsObject->property("contextualActions").value >(); } } } else if (QAction *a = action(name)) { // Bug 364292: show "Remove this Panel" option only when panelcontroller is opened if (name != QLatin1String("remove") || c->isUserConfiguring() || (c->containmentType() != Plasma::Types::PanelContainment && c->containmentType() != Plasma::Types::CustomPanelContainment)) { actions << a; } } } return actions; } QAction *ContextMenu::action(const QString &name) { Plasma::Containment *c = containment(); Q_ASSERT(c); if (name == QLatin1String("_sep1")) { return m_separator1; } else if (name == QLatin1String("_sep2")) { return m_separator2; } else if (name == QLatin1String("_sep3")) { return m_separator3; } else if (name == QLatin1String("_add panel")) { if (c->corona() && c->corona()->immutability() == Plasma::Types::Mutable) { return c->corona()->actions()->action(QStringLiteral("add panel")); } } else if (name == QLatin1String("_run_command")) { if (KAuthorized::authorizeAction(QStringLiteral("run_command")) && KAuthorized::authorize(QStringLiteral("run_command"))) { return m_runCommandAction; } } else if (name == QLatin1String("_lock_screen")) { if (KAuthorized::authorizeAction(QStringLiteral("lock_screen"))) { return m_lockScreenAction; } } else if (name == QLatin1String("_logout")) { if (KAuthorized::authorize(QStringLiteral("logout"))) { return m_logoutAction; } } else if (name == QLatin1String("lock widgets")) { if (c->corona()) { return c->corona()->actions()->action(QStringLiteral("lock widgets")); } + } else if (name == QLatin1String("edit mode")) { + if (c->corona()) { + return c->corona()->actions()->action(QStringLiteral("edit mode")); + } } else if (name == QLatin1String("manage activities")) { if (c->corona()) { return c->corona()->actions()->action(QStringLiteral("manage activities")); } } else { //FIXME: remove action: make removal of current activity possible return c->actions()->action(name); } return nullptr; } void ContextMenu::runCommand() { if (!KAuthorized::authorizeAction(QStringLiteral("run_command"))) { return; } QString interface(QStringLiteral("org.kde.krunner")); org::kde::krunner::App krunner(interface, QStringLiteral("/App"), QDBusConnection::sessionBus()); krunner.display(); } void ContextMenu::lockScreen() { if (!KAuthorized::authorizeAction(QStringLiteral("lock_screen"))) { return; } #ifndef Q_OS_WIN QString interface(QStringLiteral("org.freedesktop.ScreenSaver")); org::freedesktop::ScreenSaver screensaver(interface, QStringLiteral("/ScreenSaver"), QDBusConnection::sessionBus()); if (screensaver.isValid()) { screensaver.Lock(); } #else LockWorkStation(); #endif // !Q_OS_WIN } void ContextMenu::startLogout() { // this short delay is due to two issues: // a) KWorkSpace's DBus alls are all synchronous // b) the destruction of the menu that this action is in is delayed // // (a) leads to the menu hanging out where everyone can see it because // the even loop doesn't get returned to allowing it to close. // // (b) leads to a 0ms timer not working since a 0ms timer just appends to // the event queue, and then the menu closing event gets appended to that. // // ergo a timer with small timeout QTimer::singleShot(10, this, &ContextMenu::logout); } void ContextMenu::logout() { if (!KAuthorized::authorizeAction(QStringLiteral("logout"))) { return; } KWorkSpace::requestShutDown(); } QWidget* ContextMenu::createConfigurationInterface(QWidget* parent) { QWidget *widget = new QWidget(parent); QVBoxLayout *lay = new QVBoxLayout(); widget->setLayout(lay); widget->setWindowTitle(i18nc("plasma_containmentactions_contextmenu", "Configure Contextual Menu Plugin")); m_buttons = new QButtonGroup(widget); m_buttons->setExclusive(false); foreach (const QString &name, m_actionOrder) { QCheckBox *item = nullptr; if (name == QLatin1String("_context")) { item = new QCheckBox(widget); //FIXME better text item->setText(i18nc("plasma_containmentactions_contextmenu", "[Other Actions]")); } else if (name == QLatin1String("_wallpaper")) { item = new QCheckBox(widget); item->setText(i18nc("plasma_containmentactions_contextmenu", "Wallpaper Actions")); item->setIcon(QIcon::fromTheme(QStringLiteral("user-desktop"))); } else if (name == QLatin1String("_sep1") || name ==QLatin1String("_sep2") || name == QLatin1String("_sep3")) { item = new QCheckBox(widget); item->setText(i18nc("plasma_containmentactions_contextmenu", "[Separator]")); } else { QAction *a = action(name); if (a) { item = new QCheckBox(widget); item->setText(a->text()); item->setIcon(a->icon()); } } if (item) { item->setChecked(m_actions.value(name)); item->setProperty("actionName", name); lay->addWidget(item); m_buttons->addButton(item); } } return widget; } void ContextMenu::configurationAccepted() { QList buttons = m_buttons->buttons(); QListIterator it(buttons); while (it.hasNext()) { QAbstractButton *b = it.next(); if (b) { m_actions.insert(b->property("actionName").toString(), b->isChecked()); } } } void ContextMenu::save(KConfigGroup &config) { QHashIterator it(m_actions); while (it.hasNext()) { it.next(); config.writeEntry(it.key(), it.value()); } } K_EXPORT_PLASMA_CONTAINMENTACTIONS_WITH_JSON(contextmenu, ContextMenu, "plasma-containmentactions-contextmenu.json") #include "menu.moc" diff --git a/shell/dbus/org.kde.PlasmaShell.xml b/shell/dbus/org.kde.PlasmaShell.xml index f074e9b49..72e8ca5ef 100644 --- a/shell/dbus/org.kde.PlasmaShell.xml +++ b/shell/dbus/org.kde.PlasmaShell.xml @@ -1,35 +1,36 @@ + diff --git a/shell/shellcorona.cpp b/shell/shellcorona.cpp index 6c95ecf31..a8e37417c 100644 --- a/shell/shellcorona.cpp +++ b/shell/shellcorona.cpp @@ -1,2180 +1,2193 @@ /* * Copyright 2008 Aaron Seigo * Copyright 2013 Sebastian Kügler * Copyright 2013 Ivan Cukic * Copyright 2013 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 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. */ #include "shellcorona.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef WITH_KUSERFEEDBACKCORE #include #include #include #include #include #include #include #include #include "panelcountsource.h" #endif #include #include #include #include #include "config-ktexteditor.h" // HAVE_KTEXTEDITOR #include "alternativeshelper.h" #include "desktopview.h" #include "panelview.h" #include "scripting/scriptengine.h" #include "osd.h" #include "screenpool.h" #include "plasmashelladaptor.h" #include "debug.h" #include "futureutil.h" #ifndef NDEBUG #define CHECK_SCREEN_INVARIANTS screenInvariants(); #else #define CHECK_SCREEN_INVARIANTS #endif #if HAVE_X11 #include #include #include #endif static const int s_configSyncDelay = 10000; // 10 seconds ShellCorona::ShellCorona(QObject *parent) : Plasma::Corona(parent), m_config(KSharedConfig::openConfig(QStringLiteral("plasmarc"))), m_screenPool(new ScreenPool(KSharedConfig::openConfig(), this)), m_activityController(new KActivities::Controller(this)), m_addPanelAction(nullptr), m_addPanelsMenu(nullptr), m_interactiveConsole(nullptr), m_waylandPlasmaShell(nullptr), m_closingDown(false) { setupWaylandIntegration(); qmlRegisterUncreatableType("org.kde.plasma.shell", 2, 0, "Desktop", QStringLiteral("It is not possible to create objects of type Desktop")); qmlRegisterUncreatableType("org.kde.plasma.shell", 2, 0, "Panel", QStringLiteral("It is not possible to create objects of type Panel")); m_lookAndFeelPackage = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel")); KConfigGroup cg(KSharedConfig::openConfig(QStringLiteral("kdeglobals")), "KDE"); const QString packageName = cg.readEntry("LookAndFeelPackage", QString()); if (!packageName.isEmpty()) { m_lookAndFeelPackage.setPath(packageName); } connect(this, &Plasma::Corona::containmentCreated, this, [this] (Plasma::Containment *c) { executeSetupPlasmoidScript(c, c); }); connect(this, &Plasma::Corona::availableScreenRectChanged, this, &Plasma::Corona::availableScreenRegionChanged); m_appConfigSyncTimer.setSingleShot(true); m_appConfigSyncTimer.setInterval(s_configSyncDelay); connect(&m_appConfigSyncTimer, &QTimer::timeout, this, &ShellCorona::syncAppConfig); //we want our application config with screen mapping to always be in sync with the applets one, so a crash at any time will still //leave containments pointing to the correct screens connect(this, &Corona::configSynced, this, &ShellCorona::syncAppConfig); m_waitingPanelsTimer.setSingleShot(true); m_waitingPanelsTimer.setInterval(250); connect(&m_waitingPanelsTimer, &QTimer::timeout, this, &ShellCorona::createWaitingPanels); m_reconsiderOutputsTimer.setSingleShot(true); m_reconsiderOutputsTimer.setInterval(1000); connect(&m_reconsiderOutputsTimer, &QTimer::timeout, this, &ShellCorona::reconsiderOutputs); m_desktopDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(package().filePath("defaults")), "Desktop"); m_lnfDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(m_lookAndFeelPackage.filePath("defaults")), "Desktop"); m_lnfDefaultsConfig = KConfigGroup(&m_lnfDefaultsConfig, QStringLiteral("org.kde.plasma.desktop")); new PlasmaShellAdaptor(this); QDBusConnection dbus = QDBusConnection::sessionBus(); dbus.registerObject(QStringLiteral("/PlasmaShell"), this); // Look for theme config in plasmarc, if it isn't configured, take the theme from the // LookAndFeel package, if either is set, change the default theme connect(qApp, &QCoreApplication::aboutToQuit, this, [this]() { //saveLayout is a slot but arguments not compatible m_closingDown = true; saveLayout(); }); connect(this, &ShellCorona::containmentAdded, this, &ShellCorona::handleContainmentAdded); QAction *dashboardAction = actions()->addAction(QStringLiteral("show dashboard")); QObject::connect(dashboardAction, &QAction::triggered, this, &ShellCorona::setDashboardShown); dashboardAction->setText(i18n("Show Desktop")); connect(KWindowSystem::self(), &KWindowSystem::showingDesktopChanged, [dashboardAction](bool showing) { dashboardAction->setText(showing ? i18n("Hide Desktop") : i18n("Show Desktop")); dashboardAction->setChecked(showing); }); dashboardAction->setAutoRepeat(true); dashboardAction->setCheckable(true); dashboardAction->setIcon(QIcon::fromTheme(QStringLiteral("dashboard-show"))); dashboardAction->setData(Plasma::Types::ControlAction); KGlobalAccel::self()->setGlobalShortcut(dashboardAction, Qt::CTRL + Qt::Key_F12); checkAddPanelAction(); connect(KSycoca::self(), SIGNAL(databaseChanged(QStringList)), this, SLOT(checkAddPanelAction(QStringList))); //Activity stuff QAction *activityAction = actions()->addAction(QStringLiteral("manage activities")); connect(activityAction, &QAction::triggered, this, &ShellCorona::toggleActivityManager); activityAction->setText(i18n("Activities...")); activityAction->setIcon(QIcon::fromTheme(QStringLiteral("activities"))); activityAction->setData(Plasma::Types::ConfigureAction); activityAction->setShortcut(QKeySequence(QStringLiteral("alt+d, alt+a"))); activityAction->setShortcutContext(Qt::ApplicationShortcut); KGlobalAccel::self()->setGlobalShortcut(activityAction, Qt::META + Qt::Key_Q); QAction *stopActivityAction = actions()->addAction(QStringLiteral("stop current activity")); QObject::connect(stopActivityAction, &QAction::triggered, this, &ShellCorona::stopCurrentActivity); stopActivityAction->setText(i18n("Stop Current Activity")); stopActivityAction->setData(Plasma::Types::ControlAction); stopActivityAction->setVisible(false); KGlobalAccel::self()->setGlobalShortcut(stopActivityAction, Qt::META + Qt::Key_S); connect(m_activityController, &KActivities::Controller::currentActivityChanged, this, &ShellCorona::currentActivityChanged); connect(m_activityController, &KActivities::Controller::activityAdded, this, &ShellCorona::activityAdded); connect(m_activityController, &KActivities::Controller::activityRemoved, this, &ShellCorona::activityRemoved); KActionCollection *taskbarActions = new KActionCollection(this); for (int i = 0; i < 10; ++i) { const int entryNumber = i + 1; const Qt::Key key = static_cast(Qt::Key_0 + (entryNumber % 10)); QAction *action = taskbarActions->addAction(QStringLiteral("activate task manager entry %1").arg(QString::number(entryNumber))); action->setText(i18n("Activate Task Manager Entry %1", entryNumber)); KGlobalAccel::setGlobalShortcut(action, QKeySequence(Qt::META + key)); connect(action, &QAction::triggered, this, [this, i] { activateTaskManagerEntry(i); }); } new Osd(m_config, this); // catch when plasmarc changes, so we e.g. enable/disable the OSd m_configPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/') + m_config->name(); KDirWatch::self()->addFile(m_configPath); connect(KDirWatch::self(), &KDirWatch::dirty, this, &ShellCorona::configurationChanged); connect(KDirWatch::self(), &KDirWatch::created, this, &ShellCorona::configurationChanged); + + connect(qApp, &QGuiApplication::focusWindowChanged, + this, [this] (QWindow *focusWindow) { + if (!focusWindow) { + setEditMode(false); + } + } + ); + connect(this, &ShellCorona::editModeChanged, + this, [this](bool edit) { + setDashboardShown(edit); + } + ); } ShellCorona::~ShellCorona() { while (!containments().isEmpty()) { //deleting a containment will remove it from the list due to QObject::destroyed connect in Corona delete containments().first(); } qDeleteAll(m_panelViews); m_panelViews.clear(); } KPackage::Package ShellCorona::lookAndFeelPackage() { return m_lookAndFeelPackage; } void ShellCorona::setShell(const QString &shell) { if (m_shell == shell) { return; } m_shell = shell; KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Shell")); package.setPath(shell); package.setAllowExternalPaths(true); setKPackage(package); m_desktopDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(package.filePath("defaults")), "Desktop"); m_lnfDefaultsConfig = KConfigGroup(KSharedConfig::openConfig(m_lookAndFeelPackage.filePath("defaults")), "Desktop"); m_lnfDefaultsConfig = KConfigGroup(&m_lnfDefaultsConfig, shell); const QString themeGroupKey = QStringLiteral("Theme"); const QString themeNameKey = QStringLiteral("name"); QString themeName; KConfigGroup plasmarc(m_config, themeGroupKey); themeName = plasmarc.readEntry(themeNameKey, themeName); if (themeName.isEmpty()) { KConfigGroup shellCfg = KConfigGroup(KSharedConfig::openConfig(package.filePath("defaults")), "Theme"); themeName = shellCfg.readEntry("name", "default"); KConfigGroup lnfCfg = KConfigGroup(KSharedConfig::openConfig( m_lookAndFeelPackage.filePath("defaults")), "plasmarc" ); lnfCfg = KConfigGroup(&lnfCfg, themeGroupKey); themeName = lnfCfg.readEntry(themeNameKey, themeName); } if (!themeName.isEmpty()) { Plasma::Theme *t = new Plasma::Theme(this); t->setThemeName(themeName); } #ifdef WITH_KUSERFEEDBACKCORE auto feedbackProvider = new KUserFeedback::Provider(this); feedbackProvider->setProductIdentifier(QStringLiteral("org.kde.plasmashell")); feedbackProvider->setFeedbackServer(QUrl(QStringLiteral("https://telemetry.kde.org/"))); feedbackProvider->setSubmissionInterval(7); feedbackProvider->setApplicationStartsUntilEncouragement(5); feedbackProvider->setEncouragementDelay(30); feedbackProvider->addDataSource(new KUserFeedback::ApplicationVersionSource); feedbackProvider->addDataSource(new KUserFeedback::CompilerInfoSource); feedbackProvider->addDataSource(new KUserFeedback::PlatformInfoSource); feedbackProvider->addDataSource(new KUserFeedback::QtVersionSource); feedbackProvider->addDataSource(new KUserFeedback::UsageTimeSource); feedbackProvider->addDataSource(new KUserFeedback::OpenGLInfoSource); feedbackProvider->addDataSource(new KUserFeedback::ScreenInfoSource); feedbackProvider->addDataSource(new PanelCountSource(this)); { auto plasmaConfig = KSharedConfig::openConfig(QStringLiteral("PlasmaUserFeedback")); feedbackProvider->setTelemetryMode(KUserFeedback::Provider::TelemetryMode(plasmaConfig->group("Global").readEntry("FeedbackLevel", int(KUserFeedback::Provider::BasicUsageStatistics)))); } #endif //FIXME: this would change the runtime platform to a fixed one if available // but a different way to load platform specific components is needed beforehand // because if we import and use two different components plugin, the second time // the import is called it will fail /* KConfigGroup cg(KSharedConfig::openConfig(package.filePath("defaults")), "General"); KDeclarative::KDeclarative::setRuntimePlatform(cg.readEntry("DefaultRuntimePlatform", QStringList()));*/ unload(); /* * we want to make an initial load once we have the initial screen config and we have loaded the activities _IF_ KAMD is running * it is valid for KAMD to not be running. * * Potentially 2 async jobs * * here we connect for status changes from KAMD, and fetch the first config from kscreen. * load() will check that we have a kscreen config, and m_activityController->serviceStatus() is not loading (i.e not unknown) * * It might seem that we only need this connection if the activityConsumer is currently in state Unknown, however * there is an issue where m_activityController will start the kactivitymanagerd, as KAMD is starting the serviceStatus will be "not running" * Whilst we are loading the kscreen config, the event loop runs and we might find KAMD has started. * m_activityController will change from "not running" to unknown, and might still be unknown when the kscreen fetching is complete. * * if that happens we want to continue monitoring for state changes, and only finally load when it is up. * * See https://bugs.kde.org/show_bug.cgi?id=342431 be careful about changing * * The unique connection makes sure we don't reload plasma if KAMD ever crashes and reloads, the signal is disconnected in the body of load */ connect(m_activityController, &KActivities::Controller::serviceStatusChanged, this, &ShellCorona::load, Qt::UniqueConnection); load(); } QJsonObject dumpconfigGroupJS(const KConfigGroup &rootGroup) { QJsonObject result; QStringList hierarchy; QStringList escapedHierarchy; QList groups{rootGroup}; QSet visitedNodes; const QSet forbiddenKeys { QStringLiteral("activityId"), QStringLiteral("ItemsGeometries"), QStringLiteral("AppletOrder"), QStringLiteral("SystrayContainmentId"), QStringLiteral("location"), QStringLiteral("plugin") }; auto groupID = [&escapedHierarchy]() { return '/' + escapedHierarchy.join('/'); }; // Perform a depth-first tree traversal for config groups while (!groups.isEmpty()) { KConfigGroup cg = groups.last(); KConfigGroup parentCg = cg; //FIXME: name is not enough hierarchy.clear(); escapedHierarchy.clear(); while (parentCg.isValid() && parentCg.name() != rootGroup.name()) { const auto name = parentCg.name(); hierarchy.prepend(name); escapedHierarchy.prepend(QString::fromUtf8(QUrl::toPercentEncoding(name.toUtf8()))); parentCg = parentCg.parent(); } visitedNodes.insert(groupID()); groups.pop_back(); QJsonObject configGroupJson; if (!cg.keyList().isEmpty()) { //TODO: this is conditional if applet or containment const auto map = cg.entryMap(); auto i = map.cbegin(); for (; i != map.cend(); ++i) { //some blacklisted keys we don't want to save if (!forbiddenKeys.contains(i.key())) { configGroupJson.insert(i.key(), i.value()); } } } const auto groupList = cg.groupList(); for (const QString &groupName : groupList) { if (groupName == QLatin1String("Applets") || visitedNodes.contains(groupID() + '/' + groupName)) { continue; } groups << KConfigGroup(&cg, groupName); } if (!configGroupJson.isEmpty()) { result.insert(groupID(), configGroupJson); } } return result; } QByteArray ShellCorona::dumpCurrentLayoutJS() const { QJsonObject root; root.insert("serializationFormatVersion", "1"); //same gridUnit calculation as ScriptEngine int gridUnit = QFontMetrics(QGuiApplication::font()).boundingRect(QStringLiteral("M")).height(); if (gridUnit % 2 != 0) { gridUnit++; } auto isPanel = [] (Plasma::Containment *cont) { return (cont->formFactor() == Plasma::Types::Horizontal || cont->formFactor() == Plasma::Types::Vertical) && (cont->location() == Plasma::Types::TopEdge || cont->location() == Plasma::Types::BottomEdge || cont->location() == Plasma::Types::LeftEdge || cont->location() == Plasma::Types::RightEdge) && cont->pluginMetaData().pluginId() != QLatin1String("org.kde.plasma.private.systemtray"); }; auto isDesktop = [] (Plasma::Containment *cont) { return !cont->activity().isEmpty(); }; const auto containments = ShellCorona::containments(); // Collecting panels QJsonArray panelsJsonArray; for (Plasma::Containment *cont : containments) { if (!isPanel(cont)) { continue; } QJsonObject panelJson; const PanelView *view = m_panelViews.value(cont); const auto location = cont->location(); panelJson.insert("location", location == Plasma::Types::TopEdge ? "top" : location == Plasma::Types::LeftEdge ? "left" : location == Plasma::Types::RightEdge ? "right" : /* Plasma::Types::BottomEdge */ "bottom"); const qreal height = // If we do not have a panel, fallback to 4 units !view ? 4 : (qreal)view->thickness() / gridUnit; panelJson.insert("height", height); if (view) { const auto alignment = view->alignment(); panelJson.insert("maximumLength", (qreal)view->maximumLength() / gridUnit); panelJson.insert("minimumLength", (qreal)view->minimumLength() / gridUnit); panelJson.insert("offset", (qreal)view->offset() / gridUnit); panelJson.insert("alignment", alignment == Qt::AlignRight ? "right" : alignment == Qt::AlignCenter ? "center" : "left"); switch (view->visibilityMode()) { case PanelView::AutoHide: panelJson.insert("hiding", "autohide"); break; case PanelView::LetWindowsCover: panelJson.insert("hiding", "windowscover"); break; case PanelView::WindowsGoBelow: panelJson.insert("hiding", "windowsbelow"); break; case PanelView::NormalPanel: default: panelJson.insert("hiding", "normal"); break; } } // Saving the config keys const KConfigGroup contConfig = cont->config(); panelJson.insert("config", dumpconfigGroupJS(contConfig)); // Generate the applets array QJsonArray appletsJsonArray; // Try to parse the encoded applets order const KConfigGroup genericConf(&contConfig, QStringLiteral("General")); const QStringList appletsOrderStrings = genericConf.readEntry(QStringLiteral("AppletOrder"), QString()) .split(QChar(';')); // Consider the applet order to be valid only if there are as many entries as applets() if (appletsOrderStrings.length() == cont->applets().length()) { for (const QString &appletId : appletsOrderStrings) { KConfigGroup appletConfig(&contConfig, QStringLiteral("Applets")); appletConfig = KConfigGroup(&appletConfig, appletId); const QString pluginName = appletConfig.readEntry(QStringLiteral("plugin"), QString()); if (pluginName.isEmpty()) { continue; } QJsonObject appletJson; appletJson.insert("plugin", pluginName); appletJson.insert("config", dumpconfigGroupJS(appletConfig)); appletsJsonArray << appletJson; } } else { const auto applets = cont->applets(); for (Plasma::Applet *applet : applets) { QJsonObject appletJson; KConfigGroup appletConfig = applet->config(); appletJson.insert("plugin", applet->pluginMetaData().pluginId()); appletJson.insert("config", dumpconfigGroupJS(appletConfig)); appletsJsonArray << appletJson; } } panelJson.insert("applets", appletsJsonArray); panelsJsonArray << panelJson; } root.insert("panels", panelsJsonArray); // Now we are collecting desktops QJsonArray desktopsJson; const auto currentActivity = m_activityController->currentActivity(); for (Plasma::Containment *cont : containments) { if (!isDesktop(cont) || cont->activity() != currentActivity) { continue; } QJsonObject desktopJson; desktopJson.insert("wallpaperPlugin", cont->wallpaper()); // Get the config for the containment KConfigGroup contConfig = cont->config(); desktopJson.insert("config", dumpconfigGroupJS(contConfig)); // Try to parse the item geometries const KConfigGroup genericConf(&contConfig, QStringLiteral("General")); const QStringList appletsGeomStrings = genericConf.readEntry(QStringLiteral("ItemsGeometries"), QString()) .split(QChar(';')); QHash appletGeometries; for (const QString &encoded : appletsGeomStrings) { const QStringList keyValue = encoded.split(QChar(':')); if (keyValue.length() != 2) { continue; } const QStringList rectPieces = keyValue.last().split(QChar(',')); if (rectPieces.length() != 5) { continue; } QRect rect(rectPieces[0].toInt(), rectPieces[1].toInt(), rectPieces[2].toInt(), rectPieces[3].toInt()); appletGeometries[keyValue.first()] = rect; } QJsonArray appletsJsonArray; const auto applets = cont->applets(); for (Plasma::Applet *applet : applets) { const QRect geometry = appletGeometries.value( QStringLiteral("Applet-") % QString::number(applet->id())); QJsonObject appletJson; appletJson.insert("title", applet->title()); appletJson.insert("plugin", applet->pluginMetaData().pluginId()); appletJson.insert("geometry.x", geometry.x() / gridUnit); appletJson.insert("geometry.y", geometry.y() / gridUnit); appletJson.insert("geometry.width", geometry.width() / gridUnit); appletJson.insert("geometry.height", geometry.height() / gridUnit); KConfigGroup appletConfig = applet->config(); appletJson.insert("config", dumpconfigGroupJS(appletConfig)); appletsJsonArray << appletJson; } desktopJson.insert("applets", appletsJsonArray); desktopsJson << desktopJson; } root.insert("desktops", desktopsJson); QJsonDocument json; json.setObject(root); return "var plasma = getApiVersion(1);\n\n" "var layout = " + json.toJson() + ";\n\n" "plasma.loadSerializedLayout(layout);\n"; } void ShellCorona::loadLookAndFeelDefaultLayout(const QString &packageName) { KPackage::Package newPack = m_lookAndFeelPackage; newPack.setPath(packageName); if (!newPack.isValid()) { return; } KSharedConfig::Ptr conf = KSharedConfig::openConfig(QLatin1String("plasma-") + m_shell + QLatin1String("-appletsrc"), KConfig::SimpleConfig); m_lookAndFeelPackage.setPath(packageName); //get rid of old config for (const QString &group : conf->groupList()) { conf->deleteGroup(group); } conf->sync(); unload(); // Put load in queue of the event loop to wait for the whole set of containments to have been deleteLater(), as some like FolderView operate on singletons which can cause inconsistent states QTimer::singleShot(0, this, &ShellCorona::load); } QString ShellCorona::shell() const { return m_shell; } void ShellCorona::load() { if (m_shell.isEmpty()) { return; } auto activityStatus = m_activityController->serviceStatus(); if (activityStatus != KActivities::Controller::Running && !qApp->property("org.kde.KActivities.core.disableAutostart").toBool()) { if (activityStatus == KActivities::Controller::NotRunning) { qWarning("Aborting shell load: The activity manager daemon (kactivitymanagerd) is not running."); qWarning("If this Plasma has been installed into a custom prefix, verify that its D-Bus services dir is known to the system for the daemon to be activatable."); } return; } disconnect(m_activityController, &KActivities::Controller::serviceStatusChanged, this, &ShellCorona::load); m_screenPool->load(); //TODO: a kconf_update script is needed QString configFileName(QStringLiteral("plasma-") + m_shell + QStringLiteral("-appletsrc")); loadLayout(configFileName); checkActivities(); if (containments().isEmpty()) { // Seems like we never really get to this point since loadLayout already // (virtually) calls loadDefaultLayout if it does not load anything // from the config file. Maybe if the config file is not empty, // but still does not have any containments loadDefaultLayout(); processUpdateScripts(); } else { processUpdateScripts(); const auto containments = this->containments(); for (Plasma::Containment *containment : containments) { if (containment->containmentType() == Plasma::Types::PanelContainment || containment->containmentType() == Plasma::Types::CustomPanelContainment) { //Don't give a view to containments that don't want one (negative lastscreen) //(this is pretty mucha special case for the systray) //also, make sure we don't have a view already. //this will be true for first startup as the view has already been created at the new Panel JS call if (!m_waitingPanels.contains(containment) && containment->lastScreen() >= 0 && !m_panelViews.contains(containment)) { m_waitingPanels << containment; } //historically CustomContainments are treated as desktops } else if (containment->containmentType() == Plasma::Types::DesktopContainment || containment->containmentType() == Plasma::Types::CustomContainment) { //FIXME ideally fix this, or at least document the crap out of it int screen = containment->lastScreen(); if (screen < 0) { screen = 0; qWarning() << "last screen is < 0 so putting containment on screen " << screen; } insertContainment(containment->activity(), screen, containment); } } } //NOTE: this is needed in case loadLayout() did *not* call loadDefaultLayout() //it needs to be after of loadLayout() as it would always create new //containments on each startup otherwise for (QScreen* screen : qGuiApp->screens()) { //the containments may have been created already by the startup script //check their existence in order to not have duplicated desktopviews if (!m_desktopViewforId.contains(m_screenPool->id(screen->name()))) { addOutput(screen); } } connect(qGuiApp, &QGuiApplication::screenAdded, this, &ShellCorona::addOutput, Qt::UniqueConnection); connect(qGuiApp, &QGuiApplication::primaryScreenChanged, this, &ShellCorona::primaryOutputChanged, Qt::UniqueConnection); connect(qGuiApp, &QGuiApplication::screenRemoved, this, &ShellCorona::handleScreenRemoved, Qt::UniqueConnection); if (!m_waitingPanels.isEmpty()) { m_waitingPanelsTimer.start(); } if (config()->isImmutable() || !KAuthorized::authorize(QStringLiteral("plasma/plasmashell/unlockedDesktop"))) { setImmutability(Plasma::Types::SystemImmutable); } else { KConfigGroup coronaConfig(config(), "General"); setImmutability((Plasma::Types::ImmutabilityType)coronaConfig.readEntry("immutability", static_cast(Plasma::Types::Mutable))); } } void ShellCorona::primaryOutputChanged() { if (!m_desktopViewforId.contains(0)) { return; } //when the appearance of a new primary screen *moves* //the position of the now secondary, the two screens will appear overlapped for an instant, and a spurious output redundant would happen here if checked immediately m_reconsiderOutputsTimer.start(); QScreen *oldPrimary = m_desktopViewforId.value(0)->screen(); QScreen *newPrimary = qGuiApp->primaryScreen(); if (!newPrimary || newPrimary == oldPrimary) { return; } qWarning()<<"Old primary output:"<id(newPrimary->name()); m_screenPool->setPrimaryConnector(newPrimary->name()); //swap order in m_desktopViewforId if (m_desktopViewforId.contains(0) && m_desktopViewforId.contains(oldIdOfPrimary)) { DesktopView *primaryDesktop = m_desktopViewforId.value(0); DesktopView *oldDesktopOfPrimary = m_desktopViewforId.value(oldIdOfPrimary); primaryDesktop->setScreenToFollow(newPrimary); oldDesktopOfPrimary->setScreenToFollow(oldPrimary); primaryDesktop->show(); oldDesktopOfPrimary->show(); //corner case: the new primary screen was added into redundant outputs when appeared, *and* !m_desktopViewforId.contains(oldIdOfPrimary) //meaning that we had only one screen, connected a new oone that //a) is now primary and //b) is at 0,0 position, moving the current screen out of the way // and this will always happen in two events } else if (m_desktopViewforId.contains(0) && m_redundantOutputs.contains(newPrimary)) { m_desktopViewforId[0]->setScreenToFollow(newPrimary); m_redundantOutputs.remove(newPrimary); m_redundantOutputs.insert(oldPrimary); } for (PanelView *panel : qAsConst(m_panelViews)) { if (panel->screen() == oldPrimary) { panel->setScreenToFollow(newPrimary); } else if (panel->screen() == newPrimary) { panel->setScreenToFollow(oldPrimary); } } //can't do the screen invariant here as reconsideroutputs wasn't executed yet //CHECK_SCREEN_INVARIANTS } #ifndef NDEBUG void ShellCorona::screenInvariants() const { Q_ASSERT(m_desktopViewforId.keys().count() <= QGuiApplication::screens().count()); QSet screens; foreach (const int id, m_desktopViewforId.keys()) { const DesktopView *view = m_desktopViewforId.value(id); QScreen *screen = view->screenToFollow(); Q_ASSERT(!screens.contains(screen)); Q_ASSERT(!m_redundantOutputs.contains(screen)); // commented out because a different part of the code-base is responsible for this // and sometimes is not yet called here. // Q_ASSERT(!view->fillScreen() || view->geometry() == screen->geometry()); Q_ASSERT(view->containment()); Q_ASSERT(view->containment()->screen() == id || view->containment()->screen() == -1); Q_ASSERT(view->containment()->lastScreen() == id || view->containment()->lastScreen() == -1); Q_ASSERT(view->isVisible()); foreach (const PanelView *panel, panelsForScreen(screen)) { Q_ASSERT(panel->containment()); Q_ASSERT(panel->containment()->screen() == id || panel->containment()->screen() == -1); //If any kscreen related activities occurred //during startup, the panel wouldn't be visible yet, and this would assert if (panel->containment()->isUiReady()) { Q_ASSERT(panel->isVisible()); } } screens.insert(screen); } foreach (QScreen* out, m_redundantOutputs) { Q_ASSERT(isOutputRedundant(out)); } if (m_desktopViewforId.isEmpty()) { qWarning() << "no screens!!"; } } #endif void ShellCorona::showAlternativesForApplet(Plasma::Applet *applet) { const QUrl alternativesQML = kPackage().fileUrl("appletalternativesui"); if (alternativesQML.isEmpty()) { return; } auto *qmlObj = new KDeclarative::QmlObjectSharedEngine(this); qmlObj->setInitializationDelayed(true); qmlObj->setSource(alternativesQML); AlternativesHelper *helper = new AlternativesHelper(applet, qmlObj); qmlObj->rootContext()->setContextProperty(QStringLiteral("alternativesHelper"), helper); qmlObj->completeInitialization(); auto dialog = qobject_cast(qmlObj->rootObject()); if (!dialog) { qWarning() << "Alternatives UI does not inherit from Dialog"; delete qmlObj; return; } connect(applet, &Plasma::Applet::destroyedChanged, qmlObj, [qmlObj] (bool destroyed) { if (!destroyed) { return; } qmlObj->deleteLater(); }); connect(dialog, &PlasmaQuick::Dialog::visibleChanged, qmlObj, [qmlObj](bool visible) { if (visible) { return; } qmlObj->deleteLater(); }); } void ShellCorona::unload() { if (m_shell.isEmpty()) { return; } qDeleteAll(m_desktopViewforId); m_desktopViewforId.clear(); qDeleteAll(m_panelViews); m_panelViews.clear(); m_waitingPanels.clear(); m_activityContainmentPlugins.clear(); while (!containments().isEmpty()) { // Some applets react to destroyedChanged rather just destroyed, // give them the possibility to react // deleting a containment will remove it from the list due to QObject::destroyed connect in Corona // this form doesn't crash, while qDeleteAll(containments()) does // And is more correct anyways to use destroy() containments().first()->destroy(); } } KSharedConfig::Ptr ShellCorona::applicationConfig() { return KSharedConfig::openConfig(); } void ShellCorona::requestApplicationConfigSync() { m_appConfigSyncTimer.start(); } void ShellCorona::loadDefaultLayout() { //pre-startup scripts QString script = m_lookAndFeelPackage.filePath("layouts", QString(shell() + "-prelayout.js").toLatin1()); if (!script.isEmpty()) { QFile file(script); if (file.open(QIODevice::ReadOnly | QIODevice::Text) ) { QString code = file.readAll(); qDebug() << "evaluating pre-startup script:" << script; WorkspaceScripting::ScriptEngine scriptEngine(this); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) { qWarning() << msg; }); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) { qDebug() << msg; }); if (!scriptEngine.evaluateScript(code, script)) { qWarning() << "failed to initialize layout properly:" << script; } } } //NOTE: Is important the containments already exist for each screen // at the moment of the script execution,the same loop in :load() // is executed too late for (QScreen* screen : qGuiApp->screens()) { addOutput(screen); } script = m_testModeLayout; if (script.isEmpty()) { script = m_lookAndFeelPackage.filePath("layouts", QString(shell() + "-layout.js").toLatin1()); } if (script.isEmpty()) { script = package().filePath("defaultlayout"); } QFile file(script); if (file.open(QIODevice::ReadOnly | QIODevice::Text) ) { QString code = file.readAll(); qDebug() << "evaluating startup script:" << script; // We need to know which activities are here in order for // the scripting engine to work. activityAdded does not mind // if we pass it the same activity multiple times const QStringList existingActivities = m_activityController->activities(); for (const QString &id : existingActivities) { activityAdded(id); } WorkspaceScripting::ScriptEngine scriptEngine(this); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) { qWarning() << msg; }); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) { qDebug() << msg; }); if (!scriptEngine.evaluateScript(code, script)) { qWarning() << "failed to initialize layout properly:" << script; } } Q_EMIT startupCompleted(); } void ShellCorona::processUpdateScripts() { const QStringList scripts = WorkspaceScripting::ScriptEngine::pendingUpdateScripts(this); if (scripts.isEmpty()) { return; } WorkspaceScripting::ScriptEngine scriptEngine(this); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) { qWarning() << msg; }); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) { qDebug() << msg; }); for (const QString &script : scripts) { QFile file(script); if (file.open(QIODevice::ReadOnly | QIODevice::Text) ) { QString code = file.readAll(); scriptEngine.evaluateScript(code); } else { qWarning() << "Unable to open the script file" << script << "for reading"; } } } int ShellCorona::numScreens() const { return qGuiApp->screens().count(); } QRect ShellCorona::screenGeometry(int id) const { DesktopView* view = m_desktopViewforId.value(id); if (!view) { qWarning() << "requesting unexisting screen" << id; QScreen *s = qGuiApp->primaryScreen(); return s ? s->geometry() : QRect(); } return view->geometry(); } QRegion ShellCorona::availableScreenRegion(int id) const { DesktopView* view = m_desktopViewforId.value(id); if (!view) { //each screen should have a view qWarning() << "requesting unexisting screen" << id; QScreen *s = qGuiApp->primaryScreen(); return s ? s->availableGeometry() : QRegion(); } QRegion r = view->geometry(); for (const PanelView *v : m_panelViews) { if (v->isVisible() && view->screen() == v->screen() && v->visibilityMode() != PanelView::AutoHide) { //if the panel is being moved around, we still want to calculate it from the edge r -= v->geometryByDistance(0); } } return r; } QRect ShellCorona::availableScreenRect(int id) const { DesktopView *view = m_desktopViewforId.value(id); if (!view) { //each screen should have a view qWarning() << "requesting unexisting screen" << id; QScreen *s = qGuiApp->primaryScreen(); return s ? s->availableGeometry() : QRect(); } QRect r = view->geometry(); for (PanelView *v : m_panelViews) { if (v->isVisible() && v->screen() == view->screen() && v->visibilityMode() != PanelView::AutoHide) { switch (v->location()) { case Plasma::Types::LeftEdge: r.setLeft(r.left() + v->thickness()); break; case Plasma::Types::RightEdge: r.setRight(r.right() - v->thickness()); break; case Plasma::Types::TopEdge: r.setTop(r.top() + v->thickness()); break; case Plasma::Types::BottomEdge: r.setBottom(r.bottom() - v->thickness()); default: break; } } } return r; } QStringList ShellCorona::availableActivities() const { return m_activityContainmentPlugins.keys(); } void ShellCorona::removeDesktop(DesktopView *desktopView) { const int idx = m_screenPool->id(desktopView->screenToFollow()->name()); auto itDesktop = m_desktopViewforId.find(idx); if (itDesktop == m_desktopViewforId.end()) { return; } Q_ASSERT(m_desktopViewforId.value(idx) == desktopView); QMutableHashIterator it(m_panelViews); while (it.hasNext()) { it.next(); PanelView *panelView = it.value(); if (panelView->containment()->screen() == idx) { m_waitingPanels << panelView->containment(); it.remove(); delete panelView; } } m_desktopViewforId.erase(itDesktop); delete desktopView; emit screenRemoved(idx); } PanelView *ShellCorona::panelView(Plasma::Containment *containment) const { return m_panelViews.value(containment); } ///// SLOTS QList ShellCorona::panelsForScreen(QScreen *screen) const { QList ret; for (PanelView *v : m_panelViews) { if (v->screenToFollow() == screen) { ret += v; } } return ret; } DesktopView* ShellCorona::desktopForScreen(QScreen* screen) const { return m_desktopViewforId.value(m_screenPool->id(screen->name())); } void ShellCorona::handleScreenRemoved(QScreen* screen) { if (DesktopView* v = desktopForScreen(screen)) { removeDesktop(v); } m_reconsiderOutputsTimer.start(); m_redundantOutputs.remove(screen); } bool ShellCorona::isOutputRedundant(QScreen* screen) const { Q_ASSERT(screen); const QRect thisGeometry = screen->geometry(); const int thisId = m_screenPool->id(screen->name()); //FIXME: QScreen doesn't have any idea of "this qscreen is clone of this other one //so this ultra inefficient heuristic has to stay until we have a slightly better api //logic is: //a screen is redundant if: //* its geometry is contained in another one //* if their resolutions are different, the "biggest" one wins //* if they have the same geometry, the one with the lowest id wins (arbitrary, but gives reproducible behavior and makes the primary screen win) const auto screens = qGuiApp->screens(); for (QScreen* s : screens) { //don't compare with itself if (screen == s) { continue; } const QRect otherGeometry = s->geometry(); const int otherId = m_screenPool->id(s->name()); if (otherGeometry.contains(thisGeometry, false) && (//since at this point contains is true, if either //measure of othergeometry is bigger, has a bigger area otherGeometry.width() > thisGeometry.width() || otherGeometry.height() > thisGeometry.height() || //ids not -1 are considered in descending order of importance //-1 means that is a screen not known yet, just arrived and //not yet in screenpool: this happens for screens that //are hotplugged and weren't known. it does NOT happen //at first startup, as screenpool populates on load with all screens connected at the moment before the rest of the shell starts up (thisId == -1 && otherId != -1) || (thisId > otherId && otherId != -1))) { return true; } } return false; } void ShellCorona::reconsiderOutputs() { const auto screens = qGuiApp->screens(); for (QScreen* screen : screens) { if (m_redundantOutputs.contains(screen)) { if (!isOutputRedundant(screen)) { //qDebug() << "not redundant anymore" << screen; addOutput(screen); } } else if (isOutputRedundant(screen)) { qDebug() << "new redundant screen" << screen << "with primary screen" << qGuiApp->primaryScreen(); if (DesktopView* v = desktopForScreen(screen)) removeDesktop(v); m_redundantOutputs.insert(screen); } // else // qDebug() << "fine screen" << out; } updateStruts(); CHECK_SCREEN_INVARIANTS } void ShellCorona::addOutput(QScreen* screen) { Q_ASSERT(screen); connect(screen, &QScreen::geometryChanged, &m_reconsiderOutputsTimer, static_cast(&QTimer::start), Qt::UniqueConnection); if (isOutputRedundant(screen)) { m_redundantOutputs.insert(screen); return; } else { m_redundantOutputs.remove(screen); } int insertPosition = m_screenPool->id(screen->name()); if (insertPosition < 0) { insertPosition = m_screenPool->firstAvailableId(); } DesktopView *view = new DesktopView(this, screen); if (view->rendererInterface()->graphicsApi() != QSGRendererInterface::Software) { connect(view, &QQuickWindow::sceneGraphError, this, &ShellCorona::glInitializationFailed); } connect(screen, &QScreen::geometryChanged, this, [=]() { const int id = m_screenPool->id(screen->name()); if (id >= 0) { emit screenGeometryChanged(id); emit availableScreenRegionChanged(); emit availableScreenRectChanged(); } }); Plasma::Containment *containment = createContainmentForActivity(m_activityController->currentActivity(), insertPosition); Q_ASSERT(containment); QAction *removeAction = containment->actions()->action(QStringLiteral("remove")); if (removeAction) { removeAction->deleteLater(); } connect(containment, &Plasma::Containment::uiReadyChanged, this, &ShellCorona::checkAllDesktopsUiReady); m_screenPool->insertScreenMapping(insertPosition, screen->name()); m_desktopViewforId[insertPosition] = view; view->setContainment(containment); view->show(); Q_ASSERT(screen == view->screen()); //need to specifically call the reactToScreenChange, since when the screen is shown it's not yet //in the list. We still don't want to have an invisible view added. containment->reactToScreenChange(); //were there any panels for this screen before it popped up? if (!m_waitingPanels.isEmpty()) { m_waitingPanelsTimer.start(); } emit availableScreenRectChanged(); emit screenAdded(m_screenPool->id(screen->name())); CHECK_SCREEN_INVARIANTS } void ShellCorona::checkAllDesktopsUiReady(bool ready) { if (!ready) return; for (auto v : qAsConst(m_desktopViewforId)) { if (!v->containment()->isUiReady()) return; qDebug() << "Plasma Shell startup completed"; QDBusMessage ksplashProgressMessage = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KSplash"), QStringLiteral("/KSplash"), QStringLiteral("org.kde.KSplash"), QStringLiteral("setStage")); ksplashProgressMessage.setArguments(QList() << QStringLiteral("desktop")); QDBusConnection::sessionBus().asyncCall(ksplashProgressMessage); } } Plasma::Containment *ShellCorona::createContainmentForActivity(const QString& activity, int screenNum) { for (Plasma::Containment *cont : containmentsForActivity(activity)) { //in the case of a corrupt config file //with multiple containments with same lastScreen //it can happen two insertContainment happen for //the same screen, leading to the old containment //to be destroyed if (!cont->destroyed() && cont->screen() == screenNum) { return cont; } } QString plugin = m_activityContainmentPlugins.value(activity); if (plugin.isEmpty()) { plugin = defaultContainmentPlugin(); } Plasma::Containment *containment = containmentForScreen(screenNum, activity, plugin, QVariantList()); Q_ASSERT(containment); if (containment) { insertContainment(activity, screenNum, containment); } return containment; } void ShellCorona::createWaitingPanels() { QList stillWaitingPanels; for (Plasma::Containment *cont : qAsConst(m_waitingPanels)) { //ignore non existing (yet?) screens int requestedScreen = cont->lastScreen(); if (requestedScreen < 0) { requestedScreen = 0; } DesktopView* desktopView = m_desktopViewforId.value(requestedScreen); if (!desktopView) { stillWaitingPanels << cont; continue; } //TODO: does a similar check make sense? //Q_ASSERT(qBound(0, requestedScreen, m_screenPool->count() - 1) == requestedScreen); QScreen *screen = desktopView->screenToFollow(); PanelView* panel = new PanelView(this, screen); if (panel->rendererInterface()->graphicsApi() != QSGRendererInterface::Software) { connect(panel, &QQuickWindow::sceneGraphError, this, &ShellCorona::glInitializationFailed); } connect(panel, &QWindow::visibleChanged, this, &Plasma::Corona::availableScreenRectChanged); connect(panel, &PanelView::locationChanged, this, &Plasma::Corona::availableScreenRectChanged); connect(panel, &PanelView::visibilityModeChanged, this, &Plasma::Corona::availableScreenRectChanged); connect(panel, &PanelView::thicknessChanged, this, &Plasma::Corona::availableScreenRectChanged); m_panelViews[cont] = panel; panel->setContainment(cont); cont->reactToScreenChange(); connect(cont, &QObject::destroyed, this, &ShellCorona::panelContainmentDestroyed); } m_waitingPanels = stillWaitingPanels; emit availableScreenRectChanged(); } void ShellCorona::panelContainmentDestroyed(QObject *cont) { auto view = m_panelViews.take(static_cast(cont)); view->deleteLater(); //don't make things relayout when the application is quitting //NOTE: qApp->closingDown() is still false here if (!m_closingDown) { emit availableScreenRectChanged(); } } void ShellCorona::handleContainmentAdded(Plasma::Containment *c) { connect(c, &Plasma::Containment::showAddWidgetsInterface, this, &ShellCorona::toggleWidgetExplorer); connect(c, &Plasma::Containment::appletAlternativesRequested, this, &ShellCorona::showAlternativesForApplet); connect(c, &Plasma::Containment::appletCreated, this, [this, c] (Plasma::Applet *applet) { executeSetupPlasmoidScript(c, applet); }); } void ShellCorona::executeSetupPlasmoidScript(Plasma::Containment *containment, Plasma::Applet *applet) { if (!applet->pluginMetaData().isValid() || !containment->pluginMetaData().isValid()) { return; } const QString scriptFile = m_lookAndFeelPackage.filePath("plasmoidsetupscripts", applet->pluginMetaData().pluginId() + ".js"); if (scriptFile.isEmpty()) { return; } WorkspaceScripting::ScriptEngine scriptEngine(this); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) { qWarning() << msg; }); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) { qDebug() << msg; }); QFile file(scriptFile); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qWarning() << i18n("Unable to load script file: %1", scriptFile); return; } QString script = file.readAll(); if (script.isEmpty()) { // qDebug() << "script is empty"; return; } scriptEngine.globalObject().setProperty(QStringLiteral("applet"), scriptEngine.wrap(applet)); scriptEngine.globalObject().setProperty(QStringLiteral("containment"), scriptEngine.wrap(containment)); scriptEngine.evaluateScript(script, scriptFile); } void ShellCorona::toggleWidgetExplorer() { const QPoint cursorPos = QCursor::pos(); for (DesktopView *view : qAsConst(m_desktopViewforId)) { if (view->screen()->geometry().contains(cursorPos)) { //The view QML has to provide something to display the widget explorer view->rootObject()->metaObject()->invokeMethod(view->rootObject(), "toggleWidgetExplorer", Q_ARG(QVariant, QVariant::fromValue(sender()))); return; } } } void ShellCorona::toggleActivityManager() { const QPoint cursorPos = QCursor::pos(); for (DesktopView *view : qAsConst(m_desktopViewforId)) { if (view->screen()->geometry().contains(cursorPos)) { //The view QML has to provide something to display the activity explorer view->rootObject()->metaObject()->invokeMethod(view->rootObject(), "toggleActivityManager", Qt::QueuedConnection); return; } } } void ShellCorona::syncAppConfig() { applicationConfig()->sync(); } void ShellCorona::setDashboardShown(bool show) { KWindowSystem::setShowingDesktop(show); } void ShellCorona::toggleDashboard() { setDashboardShown(!KWindowSystem::showingDesktop()); } void ShellCorona::loadInteractiveConsole() { if (KSharedConfig::openConfig()->isImmutable() || !KAuthorized::authorize(QStringLiteral("plasma-desktop/scripting_console"))) { delete m_interactiveConsole; m_interactiveConsole = nullptr; return; } if (!m_interactiveConsole) { const QUrl consoleQML = kPackage().fileUrl("interactiveconsole"); if (consoleQML.isEmpty()) { return; } m_interactiveConsole = new KDeclarative::QmlObjectSharedEngine(this); m_interactiveConsole->setInitializationDelayed(true); m_interactiveConsole->setSource(consoleQML); QObject *engine = new WorkspaceScripting::ScriptEngine(this, m_interactiveConsole); m_interactiveConsole->rootContext()->setContextProperty(QStringLiteral("scriptEngine"), engine); m_interactiveConsole->completeInitialization(); if (m_interactiveConsole->rootObject()) { connect(m_interactiveConsole->rootObject(), SIGNAL(visibleChanged(bool)), this, SLOT(interactiveConsoleVisibilityChanged(bool))); } } } void ShellCorona::showInteractiveConsole() { loadInteractiveConsole(); if (m_interactiveConsole && m_interactiveConsole->rootObject()) { m_interactiveConsole->rootObject()->setProperty("mode", "desktop"); m_interactiveConsole->rootObject()->setProperty("visible", true); } } void ShellCorona::loadScriptInInteractiveConsole(const QString &script) { showInteractiveConsole(); if (m_interactiveConsole) { m_interactiveConsole->rootObject()->setProperty("script", script); } } void ShellCorona::showInteractiveKWinConsole() { loadInteractiveConsole(); if (m_interactiveConsole && m_interactiveConsole->rootObject()) { m_interactiveConsole->rootObject()->setProperty("mode", "windowmanager"); m_interactiveConsole->rootObject()->setProperty("visible", true); } } void ShellCorona::loadKWinScriptInInteractiveConsole(const QString &script) { showInteractiveKWinConsole(); if (m_interactiveConsole) { m_interactiveConsole->rootObject()->setProperty("script", script); } } void ShellCorona::evaluateScript(const QString &script) { if (calledFromDBus()) { if (immutability() == Plasma::Types::SystemImmutable) { sendErrorReply(QDBusError::Failed, QStringLiteral("Widgets are locked")); return; } else if (!KAuthorized::authorize(QStringLiteral("plasma-desktop/scripting_console"))) { sendErrorReply(QDBusError::Failed, QStringLiteral("Administrative policies prevent script execution")); return; } } WorkspaceScripting::ScriptEngine scriptEngine(this); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) { qWarning() << msg; }); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) { qDebug() << msg; }); scriptEngine.evaluateScript(script); if (!scriptEngine.errorString().isEmpty() && calledFromDBus()) { sendErrorReply(QDBusError::Failed, scriptEngine.errorString()); } } void ShellCorona::interactiveConsoleVisibilityChanged(bool visible) { if (!visible) { m_interactiveConsole->deleteLater(); m_interactiveConsole = nullptr; } } void ShellCorona::checkActivities() { KActivities::Controller::ServiceStatus status = m_activityController->serviceStatus(); //qDebug() << "$%$%$#%$%$%Status:" << status; if (status != KActivities::Controller::Running) { //panic and give up - better than causing a mess qDebug() << "ShellCorona::checkActivities is called whilst activity daemon is still connecting"; return; } const QStringList existingActivities = m_activityController->activities(); for (const QString &id : existingActivities) { activityAdded(id); } // Checking whether the result we got is valid. Just in case. Q_ASSERT_X(!existingActivities.isEmpty(), "isEmpty", "There are no activities, and the service is running"); Q_ASSERT_X(existingActivities[0] != QLatin1String("00000000-0000-0000-0000-000000000000"), "null uuid", "There is a nulluuid activity present"); // Killing the unassigned containments const auto conts = containments(); for (Plasma::Containment *cont : conts) { if ((cont->containmentType() == Plasma::Types::DesktopContainment || cont->containmentType() == Plasma::Types::CustomContainment) && !existingActivities.contains(cont->activity())) { cont->destroy(); } } } void ShellCorona::currentActivityChanged(const QString &newActivity) { // qDebug() << "Activity changed:" << newActivity; for (auto it = m_desktopViewforId.constBegin(); it != m_desktopViewforId.constEnd(); ++it) { Plasma::Containment *c = createContainmentForActivity(newActivity, it.key()); QAction *removeAction = c->actions()->action(QStringLiteral("remove")); if (removeAction) { removeAction->deleteLater(); } (*it)->setContainment(c); } } void ShellCorona::activityAdded(const QString &id) { //TODO more sanity checks if (m_activityContainmentPlugins.contains(id)) { qWarning() << "Activity added twice" << id; return; } m_activityContainmentPlugins.insert(id, defaultContainmentPlugin()); } void ShellCorona::activityRemoved(const QString &id) { m_activityContainmentPlugins.remove(id); for (auto cont : containmentsForActivity(id)) { cont->destroy(); } } void ShellCorona::insertActivity(const QString &id, const QString &plugin) { activityAdded(id); const QString currentActivityReally = m_activityController->currentActivity(); // TODO: This needs to go away! // The containment creation API does not know when we have a // new activity to create a containment for, we need to pretend // that the current activity has been changed QFuture currentActivity = m_activityController->setCurrentActivity(id); awaitFuture(currentActivity); if (!currentActivity.result()) { qDebug() << "Failed to create and switch to the activity"; return; } while (m_activityController->currentActivity() != id) { QCoreApplication::processEvents(); } m_activityContainmentPlugins.insert(id, plugin); for (auto it = m_desktopViewforId.constBegin(); it != m_desktopViewforId.constEnd(); ++it) { Plasma::Containment *c = createContainmentForActivity(id, it.key()); if (c) { c->config().writeEntry("lastScreen", it.key()); } } } Plasma::Containment *ShellCorona::setContainmentTypeForScreen(int screen, const QString &plugin) { //search but not create Plasma::Containment *oldContainment = containmentForScreen(screen, m_activityController->currentActivity(), QString()); //no valid containment in given screen, giving up if (!oldContainment) { return nullptr; } if (plugin.isEmpty()) { return oldContainment; } DesktopView *view = nullptr; for (DesktopView *v : qAsConst(m_desktopViewforId)) { if (v->containment() == oldContainment) { view = v; break; } } //no view? give up if (!view) { return oldContainment; } //create a new containment Plasma::Containment *newContainment = createContainmentDelayed(plugin); //if creation failed or invalid plugin, give up if (!newContainment) { return oldContainment; } else if (!newContainment->pluginMetaData().isValid()) { newContainment->deleteLater(); return oldContainment; } newContainment->setWallpaper(oldContainment->wallpaper()); //At this point we have a valid new containment from plugin and a view //copy all configuration groups (excluded applets) KConfigGroup oldCg = oldContainment->config(); //newCg *HAS* to be from a KSharedConfig, because some KConfigSkeleton will need to be synced //this makes the configscheme work KConfigGroup newCg(KSharedConfig::openConfig(oldCg.config()->name()), "Containments"); newCg = KConfigGroup(&newCg, QString::number(newContainment->id())); //this makes containment->config() work, is a separate thing from its configscheme KConfigGroup newCg2 = newContainment->config(); const auto groups = oldCg.groupList(); for (const QString &group : groups) { if (group != QLatin1String("Applets")) { KConfigGroup subGroup(&oldCg, group); KConfigGroup newSubGroup(&newCg, group); subGroup.copyTo(&newSubGroup); KConfigGroup newSubGroup2(&newCg2, group); subGroup.copyTo(&newSubGroup2); } } newContainment->init(); newCg.writeEntry("activityId", oldContainment->activity()); newContainment->restore(newCg); newContainment->updateConstraints(Plasma::Types::StartupCompletedConstraint); newContainment->flushPendingConstraintsEvents(); emit containmentAdded(newContainment); //Move the applets const auto applets = oldContainment->applets(); for (Plasma::Applet *applet : applets) { newContainment->addApplet(applet); } //remove the "remove" action QAction *removeAction = newContainment->actions()->action(QStringLiteral("remove")); if (removeAction) { removeAction->deleteLater(); } view->setContainment(newContainment); newContainment->setActivity(oldContainment->activity()); insertContainment(oldContainment->activity(), screen, newContainment); //removing the focus from the item that is going to be destroyed //fixes a crash //delayout the destruction of the old containment fixes another crash view->rootObject()->setFocus(true, Qt::MouseFocusReason); QTimer::singleShot(2500, oldContainment, &Plasma::Applet::destroy); //Save now as we now have a screen, so lastScreen will not be -1 newContainment->save(newCg); requestConfigSync(); emit availableScreenRectChanged(); return newContainment; } void ShellCorona::checkAddPanelAction(const QStringList &sycocaChanges) { if (!sycocaChanges.isEmpty() && !sycocaChanges.contains(QLatin1String("services"))) { return; } delete m_addPanelAction; m_addPanelAction = nullptr; m_addPanelsMenu.reset(nullptr); KPluginInfo::List panelContainmentPlugins = Plasma::PluginLoader::listContainmentsOfType(QStringLiteral("Panel")); auto filter = [](const KPluginMetaData &md) -> bool { return md.value(QStringLiteral("NoDisplay")) != QLatin1String("true") && KPluginMetaData::readStringList(md.rawData(), QStringLiteral("X-Plasma-ContainmentCategories")).contains(QLatin1String("panel")); }; QList templates = KPackage::PackageLoader::self()->findPackages(QStringLiteral("Plasma/LayoutTemplate"), QString(), filter); if (panelContainmentPlugins.count() + templates.count() == 1) { m_addPanelAction = new QAction(i18n("Add Panel"), this); m_addPanelAction->setData(Plasma::Types::AddAction); connect(m_addPanelAction, SIGNAL(triggered(bool)), this, SLOT(addPanel())); } else if (!panelContainmentPlugins.isEmpty()) { m_addPanelsMenu.reset(new QMenu); m_addPanelAction = m_addPanelsMenu->menuAction(); m_addPanelAction->setText(i18n("Add Panel")); m_addPanelAction->setData(Plasma::Types::AddAction); connect(m_addPanelsMenu.data(), &QMenu::aboutToShow, this, &ShellCorona::populateAddPanelsMenu); connect(m_addPanelsMenu.data(), SIGNAL(triggered(QAction*)), this, SLOT(addPanel(QAction*))); } if (m_addPanelAction) { m_addPanelAction->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); actions()->addAction(QStringLiteral("add panel"), m_addPanelAction); } } void ShellCorona::populateAddPanelsMenu() { m_addPanelsMenu->clear(); const KPluginInfo emptyInfo; const KPluginInfo::List panelContainmentPlugins = Plasma::PluginLoader::listContainmentsOfType(QStringLiteral("Panel")); QMap > sorted; for (const KPluginInfo &plugin : panelContainmentPlugins) { if (plugin.property(QStringLiteral("NoDisplay")).toString() == QLatin1String("true")) { continue; } sorted.insert(plugin.name(), qMakePair(plugin, KPluginMetaData())); } auto filter = [](const KPluginMetaData &md) -> bool { return md.value(QStringLiteral("NoDisplay")) != QLatin1String("true") && KPluginMetaData::readStringList(md.rawData(), QStringLiteral("X-Plasma-ContainmentCategories")).contains(QLatin1String("panel")); }; const QList templates = KPackage::PackageLoader::self()->findPackages(QStringLiteral("Plasma/LayoutTemplate"), QString(), filter); for (auto tpl : templates) { sorted.insert(tpl.name(), qMakePair(emptyInfo, tpl)); } QMapIterator > it(sorted); KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LayoutTemplate")); while (it.hasNext()) { it.next(); QPair pair = it.value(); if (pair.first.isValid()) { KPluginInfo plugin = pair.first; QAction *action = m_addPanelsMenu->addAction(i18n("Empty %1", plugin.name())); if (!plugin.icon().isEmpty()) { action->setIcon(QIcon::fromTheme(plugin.icon())); } action->setData(plugin.pluginName()); } else { KPluginInfo info(pair.second); package.setPath(info.pluginName()); const QString scriptFile = package.filePath("mainscript"); if (!scriptFile.isEmpty()) { QAction *action = m_addPanelsMenu->addAction(info.name()); action->setData(QStringLiteral("plasma-desktop-template:%1").arg(info.pluginName())); } } } } void ShellCorona::addPanel() { KPluginInfo::List panelPlugins = Plasma::PluginLoader::listContainmentsOfType(QStringLiteral("Panel")); if (!panelPlugins.isEmpty()) { addPanel(panelPlugins.first().pluginName()); } } void ShellCorona::addPanel(QAction *action) { const QString plugin = action->data().toString(); if (plugin.startsWith(QLatin1String("plasma-desktop-template:"))) { WorkspaceScripting::ScriptEngine scriptEngine(this); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::printError, this, [](const QString &msg) { qWarning() << msg; }); connect(&scriptEngine, &WorkspaceScripting::ScriptEngine::print, this, [](const QString &msg) { qDebug() << msg; }); const QString templateName = plugin.right(plugin.length() - qstrlen("plasma-desktop-template:")); scriptEngine.evaluateScript(QStringLiteral("loadTemplate(\"%1\")").arg(templateName)); } else if (!plugin.isEmpty()) { addPanel(plugin); } } Plasma::Containment *ShellCorona::addPanel(const QString &plugin) { Plasma::Containment *panel = createContainment(plugin); if (!panel) { return nullptr; } QList availableLocations; availableLocations << Plasma::Types::LeftEdge << Plasma::Types::TopEdge << Plasma::Types::RightEdge << Plasma::Types::BottomEdge; for (auto it = m_panelViews.constBegin(); it != m_panelViews.constEnd(); ++it) { availableLocations.removeAll((*it)->location()); } Plasma::Types::Location loc; if (availableLocations.isEmpty()) { loc = Plasma::Types::TopEdge; } else { loc = availableLocations.first(); } panel->setLocation(loc); switch (loc) { case Plasma::Types::LeftEdge: case Plasma::Types::RightEdge: panel->setFormFactor(Plasma::Types::Vertical); break; default: panel->setFormFactor(Plasma::Types::Horizontal); break; } Q_ASSERT(panel); m_waitingPanels << panel; //not creating the panel view yet in order to have the same code path //between the first and subsequent plasma starts. we want to have the panel appearing only when its layout is completed, to not have //many visible relayouts. otherwise we had even panel resizes at startup that //made al lthe full representations be loaded. m_waitingPanelsTimer.start(); const QPoint cursorPos(QCursor::pos()); const auto screens = QGuiApplication::screens(); for (QScreen *screen : screens) { //m_panelViews.contains(panel) == false iff addPanel is executed in a startup script auto panelView = m_panelViews.value(panel); if (panelView && screen->geometry().contains(cursorPos)) { panelView->setScreenToFollow(screen); break; } } return panel; } int ShellCorona::screenForContainment(const Plasma::Containment *containment) const { //case in which this containment is child of an applet, hello systray :) if (Plasma::Applet *parentApplet = qobject_cast(containment->parent())) { if (Plasma::Containment* cont = parentApplet->containment()) { return screenForContainment(cont); } else { return -1; } } //if the desktop views already exist, base the decision upon them for (auto it = m_desktopViewforId.constBegin(), end = m_desktopViewforId.constEnd(); it != end; ++it) { if (it.value()->containment() == containment && containment->activity() == m_activityController->currentActivity()) { return it.key(); } } //if the panel views already exist, base upon them PanelView *view = m_panelViews.value(containment); if (view && view->screenToFollow()) { return m_screenPool->id(view->screenToFollow()->name()); } //Failed? fallback on lastScreen() //lastScreen() is the correct screen for panels //It is also correct for desktops *that have the correct activity()* //a containment with lastScreen() == 0 but another activity, //won't be associated to a screen // qDebug() << "ShellCorona screenForContainment: " << containment << " Last screen is " << containment->lastScreen(); for (auto screen : qGuiApp->screens()) { // containment->lastScreen() == m_screenPool->id(screen->name()) to check if the lastScreen refers to a screen that exists/it's known if (containment->lastScreen() == m_screenPool->id(screen->name()) && (containment->activity() == m_activityController->currentActivity() || containment->containmentType() == Plasma::Types::PanelContainment || containment->containmentType() == Plasma::Types::CustomPanelContainment)) { return containment->lastScreen(); } } return -1; } void ShellCorona::nextActivity() { const QStringList list = m_activityController->activities(KActivities::Info::Running); if (list.isEmpty()) { return; } const int start = list.indexOf(m_activityController->currentActivity()); const int i = (start + 1) % list.size(); m_activityController->setCurrentActivity(list.at(i)); } void ShellCorona::previousActivity() { const QStringList list = m_activityController->activities(KActivities::Info::Running); if (list.isEmpty()) { return; } const int start = list.indexOf(m_activityController->currentActivity()); int i = start - 1; if(i < 0) { i = list.size() - 1; } m_activityController->setCurrentActivity(list.at(i)); } void ShellCorona::stopCurrentActivity() { const QStringList list = m_activityController->activities(KActivities::Info::Running); if (list.isEmpty()) { return; } m_activityController->stopActivity(m_activityController->currentActivity()); } void ShellCorona::insertContainment(const QString &activity, int screenNum, Plasma::Containment *containment) { Plasma::Containment *cont = nullptr; auto candidates = containmentsForActivity(activity); for (Plasma::Containment *c : candidates) { //using lastScreen() instead of screen() catches also containments of activities that aren't the current one, so not assigned to a screen right now if (c->lastScreen() == screenNum) { cont = c; if (containment == cont) { return; } break; } } Q_ASSERT(containment != cont); //if there was a duplicate containment destroy the old one //the new one replaces it //FIXME: this whole function is probably redundant now if (cont) { cont->destroy(); } } void ShellCorona::setupWaylandIntegration() { if (!KWindowSystem::isPlatformWayland()) { return; } using namespace KWayland::Client; ConnectionThread *connection = ConnectionThread::fromApplication(this); if (!connection) { return; } Registry *registry = new Registry(this); registry->create(connection); connect(registry, &Registry::plasmaShellAnnounced, this, [this, registry] (quint32 name, quint32 version) { m_waylandPlasmaShell = registry->createPlasmaShell(name, version, this); } ); registry->setup(); connection->roundtrip(); } KWayland::Client::PlasmaShell *ShellCorona::waylandPlasmaShellInterface() const { return m_waylandPlasmaShell; } ScreenPool *ShellCorona::screenPool() const { return m_screenPool; } QList ShellCorona::screenIds() const { return m_desktopViewforId.keys(); } QString ShellCorona::defaultContainmentPlugin() const { QString plugin = m_lnfDefaultsConfig.readEntry("Containment", QString()); if (plugin.isEmpty()) { plugin = m_desktopDefaultsConfig.readEntry("Containment", "org.kde.desktopcontainment"); } return plugin; } void ShellCorona::updateStruts() { for (PanelView* view : qAsConst(m_panelViews)) { view->updateStruts(); } } void ShellCorona::configurationChanged(const QString &path) { if (path == m_configPath) { m_config->reparseConfiguration(); } } void ShellCorona::activateLauncherMenu() { for (auto it = m_panelViews.constBegin(), end = m_panelViews.constEnd(); it != end; ++it) { const auto applets = it.key()->applets(); for (auto applet : applets) { const auto provides = KPluginMetaData::readStringList(applet->pluginMetaData().rawData(), QStringLiteral("X-Plasma-Provides")); if (provides.contains(QLatin1String("org.kde.plasma.launchermenu"))) { if (!applet->globalShortcut().isEmpty()) { emit applet->activated(); return; } } } } } void ShellCorona::activateTaskManagerEntry(int index) { auto activateTaskManagerEntryOnContainment = [](const Plasma::Containment *c, int index) { const auto &applets = c->applets(); for (auto *applet : applets) { const auto &provides = KPluginMetaData::readStringList(applet->pluginMetaData().rawData(), QStringLiteral("X-Plasma-Provides")); if (provides.contains(QLatin1String("org.kde.plasma.multitasking"))) { if (QQuickItem *appletInterface = applet->property("_plasma_graphicObject").value()) { const auto &childItems = appletInterface->childItems(); if (childItems.isEmpty()) { continue; } for (QQuickItem *item : childItems) { if (auto *metaObject = item->metaObject()) { // not using QMetaObject::invokeMethod to avoid warnings when calling // this on applets that don't have it or other child items since this // is pretty much trial and error. // Also, "var" arguments are treated as QVariant in QMetaObject int methodIndex = metaObject->indexOfMethod("activateTaskAtIndex(QVariant)"); if (methodIndex == -1) { continue; } QMetaMethod method = metaObject->method(methodIndex); if (method.invoke(item, Q_ARG(QVariant, index))) { return true; } } } } } } return false; }; // To avoid overly complex configuration, we'll try to get the 90% usecase to work // which is activating a task on the task manager on a panel on the primary screen. for (auto it = m_panelViews.constBegin(), end = m_panelViews.constEnd(); it != end; ++it) { if (it.value()->screen() != qGuiApp->primaryScreen()) { continue; } if (activateTaskManagerEntryOnContainment(it.key(), index)) { return; } } // we didn't find anything on primary, try all the panels for (auto it = m_panelViews.constBegin(), end = m_panelViews.constEnd(); it != end; ++it) { if (activateTaskManagerEntryOnContainment(it.key(), index)) { return; } } } // Desktop corona handler #include "moc_shellcorona.cpp"