diff --git a/shell/panelview.cpp b/shell/panelview.cpp index ba2e35e63..ac6aaedfd 100644 --- a/shell/panelview.cpp +++ b/shell/panelview.cpp @@ -1,1248 +1,1249 @@ /* * Copyright 2013 Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include "panelview.h" #include "shellcorona.h" #include "panelshadows_p.h" #include "panelconfigview.h" #include "screenpool.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if HAVE_X11 #include #include #include #endif static const int MINSIZE = 10; PanelView::PanelView(ShellCorona *corona, QScreen *targetScreen, QWindow *parent) : PlasmaQuick::ContainmentView(corona, parent), m_offset(0), m_maxLength(0), m_minLength(0), m_contentLength(0), m_distance(0), m_thickness(30), m_initCompleted(false), m_alignment(Qt::AlignLeft), m_corona(corona), m_visibilityMode(NormalPanel), m_background(0), m_backgroundHints(Plasma::Types::StandardBackground), m_shellSurface(nullptr) { if (targetScreen) { setPosition(targetScreen->geometry().center()); setScreenToFollow(targetScreen); setScreen(targetScreen); } setResizeMode(QuickViewSharedEngine::SizeRootObjectToView); setClearBeforeRendering(true); setColor(QColor(Qt::transparent)); setFlags(Qt::FramelessWindowHint|Qt::WindowDoesNotAcceptFocus); connect(&m_theme, &Plasma::Theme::themeChanged, this, &PanelView::themeChanged); connect(this, SIGNAL(backgroundHintsChanged()), this, SLOT(themeChanged())); m_positionPaneltimer.setSingleShot(true); m_positionPaneltimer.setInterval(150); connect(&m_positionPaneltimer, &QTimer::timeout, this, &PanelView::restore); m_unhideTimer.setSingleShot(true); m_unhideTimer.setInterval(500); connect(&m_unhideTimer, &QTimer::timeout, this, &PanelView::restoreAutoHide); m_lastScreen = targetScreen; connect(this, SIGNAL(locationChanged(Plasma::Types::Location)), &m_positionPaneltimer, SLOT(start())); connect(this, SIGNAL(containmentChanged()), this, SLOT(containmentChanged())); if (!m_corona->kPackage().isValid()) { qWarning() << "Invalid home screen package"; } m_strutsTimer.setSingleShot(true); connect(&m_strutsTimer, &QTimer::timeout, this, &PanelView::updateStruts); qmlRegisterType(); rootContext()->setContextProperty(QStringLiteral("panel"), this); setSource(QUrl::fromLocalFile(m_corona->kPackage().filePath("views", QStringLiteral("Panel.qml")))); } PanelView::~PanelView() { if (containment()) { m_corona->requestApplicationConfigSync(); } } KConfigGroup PanelView::panelConfig(ShellCorona *corona, Plasma::Containment *containment, QScreen *screen) { + qWarning()<<"AAAA"<applicationConfig(), "PlasmaViews"); views = KConfigGroup(&views, QStringLiteral("Panel %1").arg(containment->id())); if (containment->formFactor() == Plasma::Types::Vertical) { return KConfigGroup(&views, "Vertical" + QString::number(screen->size().height())); //treat everything else as horizontal } else { return KConfigGroup(&views, "Horizontal" + QString::number(screen->size().width())); } } KConfigGroup PanelView::config() const { return panelConfig(m_corona, containment(), m_screenToFollow); } void PanelView::maximize() { int length; if (containment()->formFactor() == Plasma::Types::Vertical) { length = m_screenToFollow->size().height(); } else { length = m_screenToFollow->size().width(); } setOffset(0); setMinimumLength(length); setMaximumLength(length); } Qt::Alignment PanelView::alignment() const { return m_alignment; } void PanelView::setAlignment(Qt::Alignment alignment) { if (m_alignment == alignment) { return; } m_alignment = alignment; //alignment is not resolution dependent config().parent().writeEntry("alignment", (int)m_alignment); emit alignmentChanged(); positionPanel(); } int PanelView::offset() const { return m_offset; } void PanelView::setOffset(int offset) { if (m_offset == offset) { return; } if (formFactor() == Plasma::Types::Vertical) { if (offset + m_maxLength > m_screenToFollow->size().height()) { setMaximumLength( -m_offset + m_screenToFollow->size().height() ); } } else { if (offset + m_maxLength > m_screenToFollow->size().width()) { setMaximumLength( -m_offset + m_screenToFollow->size().width() ); } } m_offset = offset; config().writeEntry("offset", m_offset); positionPanel(); emit offsetChanged(); m_corona->requestApplicationConfigSync(); emit m_corona->availableScreenRegionChanged(); } int PanelView::thickness() const { return m_thickness; } void PanelView::setThickness(int value) { if (value == thickness()) { return; } m_thickness = value; emit thicknessChanged(); config().writeEntry("thickness", value); m_corona->requestApplicationConfigSync(); resizePanel(); } int PanelView::length() const { return qMax(1, m_contentLength); } void PanelView::setLength(int value) { if (value == m_contentLength) { return; } m_contentLength = value; resizePanel(); } int PanelView::maximumLength() const { return m_maxLength; } void PanelView::setMaximumLength(int length) { if (length == m_maxLength) { return; } if (m_minLength > length) { setMinimumLength(length); } config().writeEntry("maxLength", length); m_maxLength = length; emit maximumLengthChanged(); m_corona->requestApplicationConfigSync(); resizePanel(); } int PanelView::minimumLength() const { return m_minLength; } void PanelView::setMinimumLength(int length) { if (length == m_minLength) { return; } if (m_maxLength < length) { setMaximumLength(length); } config().writeEntry("minLength", length); m_minLength = length; emit minimumLengthChanged(); m_corona->requestApplicationConfigSync(); resizePanel(); } int PanelView::distance() const { return m_distance; } void PanelView::setDistance(int dist) { if (m_distance == dist) { return; } m_distance = dist; emit distanceChanged(); positionPanel(); } Plasma::Types::BackgroundHints PanelView::backgroundHints() const { return m_backgroundHints; } void PanelView::setBackgroundHints(Plasma::Types::BackgroundHints hint) { if (m_backgroundHints == hint) { return; } m_backgroundHints = hint; emit backgroundHintsChanged(); } Plasma::FrameSvg::EnabledBorders PanelView::enabledBorders() const { return m_enabledBorders; } void PanelView::setVisibilityMode(PanelView::VisibilityMode mode) { if (m_visibilityMode == mode) { return; } m_visibilityMode = mode; disconnect(containment(), &Plasma::Applet::activated, this, &PanelView::showTemporarily); if (edgeActivated()) { connect(containment(), &Plasma::Applet::activated, this, &PanelView::showTemporarily); } if (config().isValid() && config().parent().isValid()) { //panelVisibility is not resolution dependent config().parent().writeEntry("panelVisibility", (int)mode); m_corona->requestApplicationConfigSync(); } visibilityModeToWayland(); updateStruts(); emit visibilityModeChanged(); restoreAutoHide(); } void PanelView::visibilityModeToWayland() { if (!m_shellSurface) { return; } KWayland::Client::PlasmaShellSurface::PanelBehavior behavior; switch (m_visibilityMode) { case NormalPanel: behavior = KWayland::Client::PlasmaShellSurface::PanelBehavior::AlwaysVisible; break; case AutoHide: behavior = KWayland::Client::PlasmaShellSurface::PanelBehavior::AutoHide; break; case LetWindowsCover: behavior = KWayland::Client::PlasmaShellSurface::PanelBehavior::WindowsCanCover; break; case WindowsGoBelow: behavior = KWayland::Client::PlasmaShellSurface::PanelBehavior::WindowsGoBelow; break; default: Q_UNREACHABLE(); return; } m_shellSurface->setPanelBehavior(behavior); } PanelView::VisibilityMode PanelView::visibilityMode() const { return m_visibilityMode; } void PanelView::positionPanel() { if (!containment()) { return; } if (!m_initCompleted) { return; } KWindowEffects::SlideFromLocation slideLocation = KWindowEffects::NoEdge; switch (containment()->location()) { case Plasma::Types::TopEdge: containment()->setFormFactor(Plasma::Types::Horizontal); slideLocation = KWindowEffects::TopEdge; break; case Plasma::Types::LeftEdge: containment()->setFormFactor(Plasma::Types::Vertical); slideLocation = KWindowEffects::LeftEdge; break; case Plasma::Types::RightEdge: containment()->setFormFactor(Plasma::Types::Vertical); slideLocation = KWindowEffects::RightEdge; break; case Plasma::Types::BottomEdge: default: containment()->setFormFactor(Plasma::Types::Horizontal); slideLocation = KWindowEffects::BottomEdge; break; } const QPoint pos = geometryByDistance(m_distance).topLeft(); setPosition(pos); if (m_shellSurface) { m_shellSurface->setPosition(pos); } KWindowEffects::slideWindow(winId(), slideLocation, -1); } QRect PanelView::geometryByDistance(int distance) const { QScreen *s = m_screenToFollow; QPoint position; const QRect screenGeometry = s->geometry(); switch (containment()->location()) { case Plasma::Types::TopEdge: switch (m_alignment) { case Qt::AlignCenter: position = QPoint(QPoint(screenGeometry.center().x(), screenGeometry.top()) + QPoint(m_offset - width()/2, distance)); break; case Qt::AlignRight: position = QPoint(QPoint(screenGeometry.x() + screenGeometry.width(), screenGeometry.y()) - QPoint(m_offset + width(), distance)); break; case Qt::AlignLeft: default: position = QPoint(screenGeometry.topLeft() + QPoint(m_offset, distance)); } break; case Plasma::Types::LeftEdge: switch (m_alignment) { case Qt::AlignCenter: position = QPoint(QPoint(screenGeometry.left(), screenGeometry.center().y()) + QPoint(distance, m_offset - height()/2)); break; case Qt::AlignRight: position = QPoint(QPoint(screenGeometry.left(), screenGeometry.y() + screenGeometry.height()) - QPoint(distance, m_offset + height())); break; case Qt::AlignLeft: default: position = QPoint(screenGeometry.topLeft() + QPoint(distance, m_offset)); } break; case Plasma::Types::RightEdge: switch (m_alignment) { case Qt::AlignCenter: // Never use rect.right(); for historical reasons it returns left() + width() - 1; see http://doc.qt.io/qt-5/qrect.html#right position = QPoint(QPoint(screenGeometry.x() + screenGeometry.width(), screenGeometry.center().y()) - QPoint(thickness() + distance, 0) + QPoint(0, m_offset - height()/2)); break; case Qt::AlignRight: position = QPoint(QPoint(screenGeometry.x() + screenGeometry.width(), screenGeometry.y() + screenGeometry.height()) - QPoint(thickness() + distance, 0) - QPoint(0, m_offset + height())); break; case Qt::AlignLeft: default: position = QPoint(QPoint(screenGeometry.x() + screenGeometry.width(), screenGeometry.y()) - QPoint(thickness() + distance, 0) + QPoint(0, m_offset)); } break; case Plasma::Types::BottomEdge: default: switch (m_alignment) { case Qt::AlignCenter: position = QPoint(QPoint(screenGeometry.center().x(), screenGeometry.bottom() - thickness() - distance) + QPoint(m_offset - width()/2, 1)); break; case Qt::AlignRight: position = QPoint(screenGeometry.bottomRight() - QPoint(0, thickness() + distance) - QPoint(m_offset + width(), -1)); break; case Qt::AlignLeft: default: position = QPoint(screenGeometry.bottomLeft() - QPoint(0, thickness() + distance) + QPoint(m_offset, 1)); } } QRect ret = formFactor() == Plasma::Types::Vertical ? QRect(position, QSize(thickness(), height())) : QRect(position, QSize(width(), thickness())); ret = ret.intersected(screenGeometry); return ret; } void PanelView::resizePanel() { if (!m_initCompleted) { return; } QSize targetSize; QSize targetMinSize; QSize targetMaxSize; if (formFactor() == Plasma::Types::Vertical) { const int minSize = qMax(MINSIZE, m_minLength); const int maxSize = qMin(m_maxLength, m_screenToFollow->size().height() - m_offset); targetMinSize = QSize(thickness(), minSize); targetMaxSize = QSize(thickness(), maxSize); targetSize = QSize(thickness(), qBound(minSize, m_contentLength, maxSize)); } else { const int minSize = qMax(MINSIZE, m_minLength); const int maxSize = qMin(m_maxLength, m_screenToFollow->size().width() - m_offset); targetMinSize = QSize(minSize, thickness()); targetMaxSize = QSize(maxSize, thickness()); targetSize = QSize(qBound(minSize, m_contentLength, maxSize), thickness()); } if (minimumSize() != targetMinSize) { setMinimumSize(targetMinSize); } if (maximumSize() != targetMaxSize) { setMaximumSize(targetMaxSize); } if (size() != targetSize) { resize(targetSize); } //position will be updated implicitly from resizeEvent } void PanelView::restore() { if (!containment()) { return; } //defaults, may be altered by values written by the scripting in startup phase const int defaultOffset = 0; const int defaultAlignment = Qt::AlignLeft; //alignment is not resolution dependent //but if fails read it from the resolution dependent one as //the place for this config key is changed in Plasma 5.9 setAlignment((Qt::Alignment)config().parent().readEntry("alignment", config().readEntry("alignment", defaultAlignment))); m_offset = config().readEntry("offset", defaultOffset); if (m_alignment != Qt::AlignCenter) { m_offset = qMax(0, m_offset); } const int defaultThickness = 30; setThickness(config().readEntry("thickness", defaultThickness)); const QSize screenSize = m_screenToFollow->size(); setMinimumSize(QSize(-1, -1)); //FIXME: an invalid size doesn't work with QWindows setMaximumSize(screenSize); const int side = containment()->formFactor() == Plasma::Types::Vertical ? screenSize.height() : screenSize.width(); const int maxSize = side - m_offset; m_maxLength = qBound(MINSIZE, config().readEntry("maxLength", side), maxSize); m_minLength = qBound(MINSIZE, config().readEntry("minLength", side), maxSize); //panelVisibility is not resolution dependent //but if fails read it from the resolution dependent one as //the place for this config key is changed in Plasma 5.9 setVisibilityMode((VisibilityMode)config().parent().readEntry("panelVisibility", config().readEntry("panelVisibility", (int)NormalPanel))); m_initCompleted = true; resizePanel(); positionPanel(); emit maximumLengthChanged(); emit minimumLengthChanged(); emit offsetChanged(); emit alignmentChanged(); //::restore might have been called directly before the timer fires // at which point we don't still need the timer m_positionPaneltimer.stop(); } void PanelView::showConfigurationInterface(Plasma::Applet *applet) { if (!applet || !applet->containment()) { return; } Plasma::Containment *cont = qobject_cast(applet); if (m_panelConfigView && cont && cont == containment() && cont->isContainment()) { if (m_panelConfigView.data()->isVisible()) { m_panelConfigView.data()->hide(); } else { m_panelConfigView.data()->show(); KWindowSystem::setState(m_panelConfigView.data()->winId(), NET::SkipTaskbar | NET::SkipPager); } return; } else if (m_panelConfigView) { if (m_panelConfigView->applet() == applet) { m_panelConfigView->show(); m_panelConfigView->requestActivate(); return; } else { m_panelConfigView->hide(); m_panelConfigView->deleteLater(); } } if (cont && cont == containment() && cont->isContainment()) { m_panelConfigView = new PanelConfigView(cont, this); } else { m_panelConfigView = new PlasmaQuick::ConfigView(applet); } m_panelConfigView.data()->init(); m_panelConfigView.data()->show(); if (cont && cont == containment() && cont->isContainment()) { KWindowSystem::setState(m_panelConfigView.data()->winId(), NET::SkipTaskbar | NET::SkipPager); } } void PanelView::restoreAutoHide() { bool autoHide = true; if (!edgeActivated()) { autoHide = false; } else if (geometry().contains(QCursor::pos(screenToFollow()))) { autoHide = false; } else if (containment() && containment()->isUserConfiguring()) { autoHide = false; } else if (containment() && containment()->status() >= Plasma::Types::NeedsAttentionStatus && containment()->status() != Plasma::Types::HiddenStatus) { autoHide = false; } setAutoHideEnabled(autoHide); } void PanelView::setAutoHideEnabled(bool enabled) { #if HAVE_X11 if (KWindowSystem::isPlatformX11()) { xcb_connection_t *c = QX11Info::connection(); const QByteArray effectName = QByteArrayLiteral("_KDE_NET_WM_SCREEN_EDGE_SHOW"); xcb_intern_atom_cookie_t atomCookie = xcb_intern_atom_unchecked(c, false, effectName.length(), effectName.constData()); QScopedPointer atom(xcb_intern_atom_reply(c, atomCookie, NULL)); if (!atom) { return; } if (!enabled) { xcb_delete_property(c, winId(), atom->atom); return; } KWindowEffects::SlideFromLocation slideLocation = KWindowEffects::NoEdge; uint32_t value = 0; switch (location()) { case Plasma::Types::TopEdge: value = 0; slideLocation = KWindowEffects::TopEdge; break; case Plasma::Types::RightEdge: value = 1; slideLocation = KWindowEffects::RightEdge; break; case Plasma::Types::BottomEdge: value = 2; slideLocation = KWindowEffects::BottomEdge; break; case Plasma::Types::LeftEdge: value = 3; slideLocation = KWindowEffects::LeftEdge; break; case Plasma::Types::Floating: default: value = 4; break; } int hideType = 0; if (m_visibilityMode == LetWindowsCover) { hideType = 1; } value |= hideType << 8; xcb_change_property(c, XCB_PROP_MODE_REPLACE, winId(), atom->atom, XCB_ATOM_CARDINAL, 32, 1, &value); KWindowEffects::slideWindow(winId(), slideLocation, -1); } #endif if (m_shellSurface && m_visibilityMode == PanelView::AutoHide) { if (enabled) { m_shellSurface->requestHideAutoHidingPanel(); } else { m_shellSurface->requestShowAutoHidingPanel(); } } } void PanelView::resizeEvent(QResizeEvent *ev) { updateMask(); updateEnabledBorders(); //don't setGeometry() to make really sure we aren't doing a resize loop const QPoint pos = geometryByDistance(m_distance).topLeft(); setPosition(pos); if (m_shellSurface) { m_shellSurface->setPosition(pos); } m_strutsTimer.start(STRUTSTIMERDELAY); emit m_corona->availableScreenRegionChanged(); PlasmaQuick::ContainmentView::resizeEvent(ev); } void PanelView::moveEvent(QMoveEvent *ev) { updateMask(); updateEnabledBorders(); m_strutsTimer.start(STRUTSTIMERDELAY); PlasmaQuick::ContainmentView::moveEvent(ev); } void PanelView::integrateScreen() { connect(m_screenToFollow.data(), &QScreen::geometryChanged, this, &PanelView::restore); themeChanged(); KWindowSystem::setOnAllDesktops(winId(), true); KWindowSystem::setType(winId(), NET::Dock); if (m_shellSurface) { m_shellSurface->setRole(KWayland::Client::PlasmaShellSurface::Role::Panel); m_shellSurface->setSkipTaskbar(true); } setVisibilityMode(m_visibilityMode); if (containment()) { containment()->reactToScreenChange(); } } void PanelView::showEvent(QShowEvent *event) { PlasmaQuick::ContainmentView::showEvent(event); integrateScreen(); } void PanelView::setScreenToFollow(QScreen *screen) { if (screen == m_screenToFollow) { return; } if (!screen) { return; } /*connect(screen, &QObject::destroyed, this, [this]() { if (PanelView::screen()) { m_screenToFollow = PanelView::screen(); adaptToScreen(); } });*/ m_screenToFollow = screen; setScreen(screen); adaptToScreen(); } QScreen *PanelView::screenToFollow() const { return m_screenToFollow; } void PanelView::adaptToScreen() { emit screenToFollowChanged(m_screenToFollow); m_lastScreen = m_screenToFollow; if (!m_screenToFollow) { return; } integrateScreen(); showTemporarily(); m_positionPaneltimer.start(); } bool PanelView::event(QEvent *e) { if (edgeActivated()) { if (e->type() == QEvent::Enter) { m_unhideTimer.stop(); } else if (e->type() == QEvent::Leave) { m_unhideTimer.start(); } } /*Fitt's law: if the containment has margins, and the mouse cursor clicked * on the mouse edge, forward the click in the containment boundaries */ switch (e->type()) { case QEvent::MouseMove: case QEvent::MouseButtonPress: case QEvent::MouseButtonRelease: { QMouseEvent *me = static_cast(e); //first, don't mess with position if the cursor is actually outside the view: //somebody is doing a click and drag that must not break when the cursor i outside if (geometry().contains(QCursor::pos(screenToFollow()))) { if (!containmentContainsPosition(me->windowPos())) { auto me2 = new QMouseEvent(me->type(), positionAdjustedForContainment(me->windowPos()), positionAdjustedForContainment(me->windowPos()), positionAdjustedForContainment(me->windowPos()) + position(), me->button(), me->buttons(), me->modifiers()); QCoreApplication::postEvent(this, me2); return true; } } else { // default handling if current mouse position is outside the panel return ContainmentView::event(e); } break; } case QEvent::Enter: case QEvent::Leave: // QtQuick < 5.6 issue: // QEvent::Leave is triggered on MouseButtonPress Qt::LeftButton break; case QEvent::Wheel: { QWheelEvent *we = static_cast(e); if (!containmentContainsPosition(we->pos())) { auto we2 = new QWheelEvent(positionAdjustedForContainment(we->pos()), positionAdjustedForContainment(we->pos()) + position(), we->pixelDelta(), we->angleDelta(), we->delta(), we->orientation(), we->buttons(), we->modifiers(), we->phase()); QCoreApplication::postEvent(this, we2); return true; } break; } case QEvent::DragEnter: { QDragEnterEvent *de = static_cast(e); if (!containmentContainsPosition(de->pos())) { auto de2 = new QDragEnterEvent(positionAdjustedForContainment(de->pos()).toPoint(), de->possibleActions(), de->mimeData(), de->mouseButtons(), de->keyboardModifiers()); QCoreApplication::postEvent(this, de2); return true; } break; } //DragLeave just works case QEvent::DragLeave: break; case QEvent::DragMove: { QDragMoveEvent *de = static_cast(e); if (!containmentContainsPosition(de->pos())) { auto de2 = new QDragMoveEvent(positionAdjustedForContainment(de->pos()).toPoint(), de->possibleActions(), de->mimeData(), de->mouseButtons(), de->keyboardModifiers()); QCoreApplication::postEvent(this, de2); return true; } break; } case QEvent::Drop: { QDropEvent *de = static_cast(e); if (!containmentContainsPosition(de->pos())) { auto de2 = new QDropEvent(positionAdjustedForContainment(de->pos()).toPoint(), de->possibleActions(), de->mimeData(), de->mouseButtons(), de->keyboardModifiers()); QCoreApplication::postEvent(this, de2); return true; } break; } case QEvent::Hide: { if (m_panelConfigView && m_panelConfigView.data()->isVisible()) { m_panelConfigView.data()->hide(); } break; } case QEvent::PlatformSurface: if (auto pe = dynamic_cast(e)) { switch (pe->surfaceEventType()) { case QPlatformSurfaceEvent::SurfaceCreated: setupWaylandIntegration(); PanelShadows::self()->addWindow(this, enabledBorders()); break; case QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed: delete m_shellSurface; m_shellSurface = nullptr; PanelShadows::self()->removeWindow(this); break; } } break; default: break; } return ContainmentView::event(e); } bool PanelView::containmentContainsPosition(const QPointF &point) const { QQuickItem *containmentItem = containment()->property("_plasma_graphicObject").value(); if (!containmentItem) { return false; } return QRectF(containmentItem->mapToScene(QPoint(0,0)), QSizeF(containmentItem->width(), containmentItem->height())).contains(point); } QPointF PanelView::positionAdjustedForContainment(const QPointF &point) const { QQuickItem *containmentItem = containment()->property("_plasma_graphicObject").value(); if (!containmentItem) { return point; } QRectF containmentRect(containmentItem->mapToScene(QPoint(0,0)), QSizeF(containmentItem->width(), containmentItem->height())); return QPointF(qBound(containmentRect.left() + 2, point.x(), containmentRect.right() - 2), qBound(containmentRect.top() + 2, point.y(), containmentRect.bottom() - 2)); } void PanelView::updateMask() { if (KWindowSystem::compositingActive()) { setMask(QRegion()); } else { if (!m_background) { m_background = new Plasma::FrameSvg(this); m_background->setImagePath(QStringLiteral("widgets/panel-background")); } m_background->setEnabledBorders(enabledBorders()); m_background->resizeFrame(size()); setMask(m_background->mask()); } } bool PanelView::canSetStrut() const { #if HAVE_X11 if (!QX11Info::isPlatformX11()) { return true; } // read the wm name, need to do this every time which means a roundtrip unfortunately // but WM might have changed NETRootInfo rootInfo(QX11Info::connection(), NET::Supported | NET::SupportingWMCheck); if (qstricmp(rootInfo.wmName(), "KWin") == 0) { // KWin since 5.7 can handle this fine, so only exclude for other window managers return true; } const QRect thisScreen = screen()->geometry(); const int numScreens = corona()->numScreens(); if (numScreens < 2) { return true; } //Extended struts against a screen edge near to another screen are really harmful, so windows maximized under the panel is a lesser pain //TODO: force "windows can cover" in those cases? foreach (int id, m_corona->screenIds()) { if (id == containment()->screen()) { continue; } const QRect otherScreen = corona()->screenGeometry(id); if (!otherScreen.isValid()) { continue; } switch (location()) { case Plasma::Types::TopEdge: if (otherScreen.bottom() <= thisScreen.top()) { return false; } break; case Plasma::Types::BottomEdge: if (otherScreen.top() >= thisScreen.bottom()) { return false; } break; case Plasma::Types::RightEdge: if (otherScreen.left() >= thisScreen.right()) { return false; } break; case Plasma::Types::LeftEdge: if (otherScreen.right() <= thisScreen.left()) { return false; } break; default: return false; } } return true; #else return true; #endif } void PanelView::updateStruts() { if (!containment() || containment()->isUserConfiguring() || !m_screenToFollow) { return; } NETExtendedStrut strut; if (m_visibilityMode == NormalPanel) { const QRect thisScreen = m_screenToFollow->geometry(); // QScreen::virtualGeometry() is very unreliable (Qt 5.5) const QRect wholeScreen = QRect(QPoint(0, 0), m_screenToFollow->virtualSize()); if (!canSetStrut()) { KWindowSystem::setExtendedStrut(winId(), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); return; } // extended struts are to the combined screen geoms, not the single screen int leftOffset = thisScreen.x(); int rightOffset = wholeScreen.right() - thisScreen.right(); int bottomOffset = wholeScreen.bottom() - thisScreen.bottom(); // qDebug() << "screen l/r/b/t offsets are:" << leftOffset << rightOffset << bottomOffset << topOffset << location(); int topOffset = thisScreen.top(); switch (location()) { case Plasma::Types::TopEdge: strut.top_width = thickness() + topOffset; strut.top_start = x(); strut.top_end = x() + width() - 1; // qDebug() << "setting top edge to" << strut.top_width << strut.top_start << strut.top_end; break; case Plasma::Types::BottomEdge: strut.bottom_width = thickness() + bottomOffset; strut.bottom_start = x(); strut.bottom_end = x() + width() - 1; // qDebug() << "setting bottom edge to" << strut.bottom_width << strut.bottom_start << strut.bottom_end; break; case Plasma::Types::RightEdge: strut.right_width = thickness() + rightOffset; strut.right_start = y(); strut.right_end = y() + height() - 1; // qDebug() << "setting right edge to" << strut.right_width << strut.right_start << strut.right_end; break; case Plasma::Types::LeftEdge: strut.left_width = thickness() + leftOffset; strut.left_start = y(); strut.left_end = y() + height() - 1; // qDebug() << "setting left edge to" << strut.left_width << strut.left_start << strut.left_end; break; default: //qDebug() << "where are we?"; break; } } KWindowSystem::setExtendedStrut(winId(), strut.left_width, strut.left_start, strut.left_end, strut.right_width, strut.right_start, strut.right_end, strut.top_width, strut.top_start, strut.top_end, strut.bottom_width, strut.bottom_start, strut.bottom_end); } void PanelView::themeChanged() { if (m_backgroundHints == Plasma::Types::NoBackground) { KWindowEffects::enableBlurBehind(winId(), false); KWindowEffects::enableBackgroundContrast(winId(), false); } else { KWindowEffects::enableBlurBehind(winId(), true); KWindowEffects::enableBackgroundContrast(winId(), m_theme.backgroundContrastEnabled(), m_theme.backgroundContrast(), m_theme.backgroundIntensity(), m_theme.backgroundSaturation()); } updateMask(); } void PanelView::containmentChanged() { restore(); connect(containment(), &Plasma::Containment::userConfiguringChanged, this, [this](bool configuring){ if (configuring) { showTemporarily(); } else { m_unhideTimer.start(); updateStruts(); } }); connect(containment(), SIGNAL(statusChanged(Plasma::Types::ItemStatus)), SLOT(statusChanged(Plasma::Types::ItemStatus))); connect(containment(), &Plasma::Applet::appletDeleted, this, [this] { //containment()->destroyed() is true only when the user deleted it //so the config is to be thrown away, not during shutdown if (containment()->destroyed()) { KConfigGroup views(m_corona->applicationConfig(), "PlasmaViews"); for (auto grp : views.groupList()) { if (grp.contains(QRegExp("Panel " + QString::number(containment()->id()) + "$"))) { qDebug() << "Panel" << containment()->id() << "removed by user"; views.deleteGroup(grp); } views.sync(); } } }); } void PanelView::statusChanged(Plasma::Types::ItemStatus status) { if (status == Plasma::Types::NeedsAttentionStatus) { showTemporarily(); } else if (status == Plasma::Types::AcceptingInputStatus) { KWindowSystem::forceActiveWindow(winId()); } else { restoreAutoHide(); } } void PanelView::showTemporarily() { setAutoHideEnabled(false); QTimer * t = new QTimer(this); t->setSingleShot(true); t->setInterval(3000); connect(t, &QTimer::timeout, this, &PanelView::restoreAutoHide); connect(t, &QTimer::timeout, t, &QObject::deleteLater); t->start(); } void PanelView::screenDestroyed(QObject* ) { // NOTE: this is overriding the screen destroyed slot, we need to do this because // otherwise Qt goes mental and starts moving our panels. See: // https://codereview.qt-project.org/#/c/88351/ // if(screen == this->m_screenToFollow) { // DO NOTHING, panels are moved by ::readaptToScreen // } } void PanelView::setupWaylandIntegration() { if (m_shellSurface) { // already setup return; } if (ShellCorona *c = qobject_cast(corona())) { using namespace KWayland::Client; PlasmaShell *interface = c->waylandPlasmaShellInterface(); if (!interface) { return; } Surface *s = Surface::fromWindow(this); if (!s) { return; } m_shellSurface = interface->createSurface(s, this); } } bool PanelView::edgeActivated() const { return m_visibilityMode == PanelView::AutoHide || m_visibilityMode == LetWindowsCover; } void PanelView::updateEnabledBorders() { Plasma::FrameSvg::EnabledBorders borders = Plasma::FrameSvg::AllBorders; if (m_backgroundHints == Plasma::Types::NoBackground) { borders = Plasma::FrameSvg::NoBorder; } else { switch (location()) { case Plasma::Types::TopEdge: borders &= ~Plasma::FrameSvg::TopBorder; break; case Plasma::Types::LeftEdge: borders &= ~Plasma::FrameSvg::LeftBorder; break; case Plasma::Types::RightEdge: borders &= ~Plasma::FrameSvg::RightBorder; break; case Plasma::Types::BottomEdge: borders &= ~Plasma::FrameSvg::BottomBorder; break; default: break; } if (x() <= m_screenToFollow->geometry().x()) { borders &= ~Plasma::FrameSvg::LeftBorder; } if (x() + width() >= m_screenToFollow->geometry().x() + m_screenToFollow->geometry().width()) { borders &= ~Plasma::FrameSvg::RightBorder; } if (y() <= m_screenToFollow->geometry().y()) { borders &= ~Plasma::FrameSvg::TopBorder; } if (y() + height() >= m_screenToFollow->geometry().y() + m_screenToFollow->geometry().height()) { borders &= ~Plasma::FrameSvg::BottomBorder; } } if (m_enabledBorders != borders) { if (m_backgroundHints == Plasma::Types::NoBackground) { PanelShadows::self()->removeWindow(this); } else { PanelShadows::self()->setEnabledBorders(this, borders); } m_enabledBorders = borders; emit enabledBordersChanged(); } } #include "moc_panelview.cpp" diff --git a/shell/shellcorona.cpp b/shell/shellcorona.cpp index 49767f1a8..d947b2e3c 100644 --- a/shell/shellcorona.cpp +++ b/shell/shellcorona.cpp @@ -1,2155 +1,2159 @@ /* * 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 #include #include #include "config-ktexteditor.h" // HAVE_KTEXTEDITOR #include "alternativeshelper.h" #include "desktopview.h" #include "panelview.h" #include "scripting/scriptengine.h" #include "shellmanager.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); connect(this, &Plasma::Corona::startupCompleted, this, []() { 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); //TODO: remove }); // 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("preferences-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); } 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); } //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); if (m_activityController->serviceStatus() == KActivities::Controller::Running) { 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()); } } } foreach (const QString &groupName, cg.groupList()) { if (groupName == QStringLiteral("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() != QStringLiteral("org.kde.plasma.private.systemtray"); }; auto isDesktop = [] (Plasma::Containment *cont) { return !cont->activity().isEmpty(); }; const auto containments = ShellCorona::containments(); // Collecting panels QJsonArray panelsJsonArray; foreach (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()) { foreach (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 { foreach (Plasma::Applet *applet, cont->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(); foreach (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; foreach (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; foreach (Plasma::Applet *applet, cont->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(QStringLiteral("plasma-") + m_shell + QStringLiteral("-appletsrc"), KConfig::SimpleConfig); m_lookAndFeelPackage.setPath(packageName); //get rid of old config for (const QString &group : conf->groupList()) { conf->deleteGroup(group); } conf->sync(); unload(); load(); } QString ShellCorona::shell() const { return m_shell; } void ShellCorona::load() { if (m_shell.isEmpty() || m_activityController->serviceStatus() != KActivities::Controller::Running) { 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(); foreach(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 oder 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::screenRemoved, 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", (int)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); } foreach (PanelView *panel, 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); - Q_ASSERT(panel->isVisible()); + //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 QString alternativesQML = package().filePath("appletalternativesui"); if (alternativesQML.isEmpty()) { return; } KDeclarative::QmlObject *qmlObj = new KDeclarative::QmlObject(this); qmlObj->setInitializationDelayed(true); qmlObj->setSource(QUrl::fromLocalFile(alternativesQML)); AlternativesHelper *helper = new AlternativesHelper(applet, qmlObj); qmlObj->rootContext()->setContextProperty(QStringLiteral("alternativesHelper"), helper); m_alternativesObjects << qmlObj; qmlObj->completeInitialization(); connect(qmlObj->rootObject(), SIGNAL(visibleChanged(bool)), this, SLOT(alternativesVisibilityChanged(bool))); connect(applet, &Plasma::Applet::destroyedChanged, this, [this, qmlObj] (bool destroyed) { if (!destroyed) { return; } QMutableListIterator it(m_alternativesObjects); while (it.hasNext()) { KDeclarative::QmlObject *obj = it.next(); if (obj == qmlObj) { it.remove(); obj->deleteLater(); } } }); } void ShellCorona::alternativesVisibilityChanged(bool visible) { if (visible) { return; } QObject *root = sender(); QMutableListIterator it(m_alternativesObjects); while (it.hasNext()) { KDeclarative::QmlObject *obj = it.next(); if (obj->rootObject() == root) { it.remove(); obj->deleteLater(); } } } void ShellCorona::unload() { if (m_shell.isEmpty()) { return; } qDeleteAll(m_desktopViewforId); m_desktopViewforId.clear(); qDeleteAll(m_panelViews); m_panelViews.clear(); m_desktopContainments.clear(); m_waitingPanels.clear(); m_activityContainmentPlugins.clear(); while (!containments().isEmpty()) { //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 delete containments().first(); } } 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 = ShellManager::s_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 QStringList existingActivities = m_activityController->activities(); foreach (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() { 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; }); foreach (const QString &script, WorkspaceScripting::ScriptEngine::pendingUpdateScripts(this)) { 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 { if (!m_desktopViewforId.contains(id)) { qWarning() << "requesting unexisting screen" << id; QScreen *s = qGuiApp->primaryScreen(); return s ? s->geometry() : QRect(); } return m_desktopViewforId.value(id)->geometry(); } QRegion ShellCorona::availableScreenRegion(int id) const { if (!m_desktopViewforId.contains(id)) { //each screen should have a view qWarning() << "requesting unexisting screen" << id; QScreen *s = qGuiApp->primaryScreen(); return s ? s->availableGeometry() : QRegion(); } DesktopView *view = m_desktopViewforId.value(id); QRegion r = view->geometry(); foreach (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 { if (!m_desktopViewforId.contains(id)) { //each screen should have a view qWarning() << "requesting unexisting screen" << id; QScreen *s = qGuiApp->primaryScreen(); return s ? s->availableGeometry() : QRect(); } DesktopView *view = m_desktopViewforId.value(id); QRect r = view->geometry(); foreach (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()); if (!m_desktopViewforId.contains(idx)) { return; } 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; } } Q_ASSERT(m_desktopViewforId.value(idx) == desktopView); m_desktopViewforId.remove(idx); delete desktopView; } PanelView *ShellCorona::panelView(Plasma::Containment *containment) const { return m_panelViews.value(containment); } ///// SLOTS QList ShellCorona::panelsForScreen(QScreen *screen) const { QList ret; foreach (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::screenRemoved(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) foreach (QScreen* s, qGuiApp->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() { foreach (QScreen* screen, qGuiApp->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); connect(view, &QQuickWindow::sceneGraphError, this, &ShellCorona::showOpenGLNotCompatibleWarning); 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(); } 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(); CHECK_SCREEN_INVARIANTS } Plasma::Containment *ShellCorona::createContainmentForActivity(const QString& activity, int screenNum) { if (m_desktopContainments.contains(activity)) { for (Plasma::Containment *cont : m_desktopContainments.value(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 && cont->activity() == activity) { return cont; } } } QString plugin = m_activityContainmentPlugins.value(activity); if (plugin.isEmpty()) { plugin = defaultContainmentPlugin(); } Plasma::Containment *containment = containmentForScreen(screenNum, plugin, QVariantList()); Q_ASSERT(containment); if (containment) { containment->setActivity(activity); insertContainment(activity, screenNum, containment); } return containment; } void ShellCorona::createWaitingPanels() { QList stillWaitingPanels; foreach (Plasma::Containment *cont, m_waitingPanels) { //ignore non existing (yet?) screens int requestedScreen = cont->lastScreen(); if (requestedScreen < 0) { requestedScreen = 0; } if (!m_desktopViewforId.contains(requestedScreen)) { stillWaitingPanels << cont; continue; } //TODO: does a similar check make sense? //Q_ASSERT(qBound(0, requestedScreen, m_screenPool->count() - 1) == requestedScreen); QScreen *screen = m_desktopViewforId.value(requestedScreen)->screenToFollow(); PanelView* panel = new PanelView(this, screen); connect(panel, &QQuickWindow::sceneGraphError, this, &ShellCorona::showOpenGLNotCompatibleWarning); 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); // Why queued? this is usually triggered after a context menu closes // due to its sync,modal nature it may eat some mouse event from the scene // waiting a bit to create a new window, the dialog seems to reliably // avoid the eating of one click in the panel after the context menu is gone connect(c, &Plasma::Containment::appletAlternativesRequested, this, &ShellCorona::showAlternativesForApplet, Qt::QueuedConnection); 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(); foreach (DesktopView *view, 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(); foreach (DesktopView *view, 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 = 0; return; } if (!m_interactiveConsole) { const QString consoleQML = package().filePath("interactiveconsole"); if (consoleQML.isEmpty()) { return; } m_interactiveConsole = new KDeclarative::QmlObject(this); m_interactiveConsole->setInitializationDelayed(true); m_interactiveConsole->setSource(QUrl::fromLocalFile(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 (immutability() != Plasma::Types::Mutable) { if (calledFromDBus()) { sendErrorReply(QDBusError::Failed, QStringLiteral("Widgets are locked")); } 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.hasUncaughtException() && calledFromDBus()) { sendErrorReply(QDBusError::Failed, scriptEngine.uncaughtException().toString()); } } 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; } QStringList existingActivities = m_activityController->activities(); foreach (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] != QStringLiteral("00000000-0000-0000-0000-000000000000"), "null uuid", "There is a nulluuid activity present"); // Killing the unassigned containments foreach (Plasma::Containment *cont, containments()) { 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; foreach (int id, m_desktopViewforId.keys()) { Plasma::Containment *c = createContainmentForActivity(newActivity, id); QAction *removeAction = c->actions()->action(QStringLiteral("remove")); if (removeAction) { removeAction->deleteLater(); } m_desktopViewforId.value(id)->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); if (m_desktopContainments.contains(id)) { for (auto cont : m_desktopContainments.value(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); foreach (int screenId, m_desktopViewforId.keys()) { Plasma::Containment *c = createContainmentForActivity(id, screenId); if (c) { c->config().writeEntry("lastScreen", screenId); } } } Plasma::Containment *ShellCorona::setContainmentTypeForScreen(int screen, const QString &plugin) { Plasma::Containment *oldContainment = containmentForScreen(screen); //no valid containment in given screen, giving up if (!oldContainment) { return 0; } if (plugin.isEmpty()) { return oldContainment; } DesktopView *view = 0; foreach (DesktopView *v, 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(); foreach (const QString &group, oldCg.groupList()) { 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 foreach (Plasma::Applet *applet, oldContainment->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()); m_desktopContainments.remove(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(QStringLiteral("services"))) { return; } delete m_addPanelAction; m_addPanelAction = 0; delete m_addPanelsMenu; m_addPanelsMenu = 0; KPluginInfo::List panelContainmentPlugins = Plasma::PluginLoader::listContainmentsOfType(QStringLiteral("Panel")); auto filter = [](const KPluginMetaData &md) -> bool { return md.value(QStringLiteral("NoDisplay")) != QStringLiteral("true") && KPluginMetaData::readStringList(md.rawData(), QStringLiteral("X-Plasma-ContainmentCategories")).contains(QStringLiteral("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 = new QMenu; m_addPanelAction = m_addPanelsMenu->menuAction(); m_addPanelAction->setText(i18n("Add Panel")); m_addPanelAction->setData(Plasma::Types::AddAction); connect(m_addPanelsMenu, &QMenu::aboutToShow, this, &ShellCorona::populateAddPanelsMenu); connect(m_addPanelsMenu, 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; KPluginInfo::List panelContainmentPlugins = Plasma::PluginLoader::listContainmentsOfType(QStringLiteral("Panel")); QMap > sorted; foreach (const KPluginInfo &plugin, panelContainmentPlugins) { if (plugin.property(QStringLiteral("NoDisplay")).toString() == QStringLiteral("true")) { continue; } sorted.insert(plugin.name(), qMakePair(plugin, KPluginMetaData())); } auto filter = [](const KPluginMetaData &md) -> bool { return md.value(QStringLiteral("NoDisplay")) != QStringLiteral("true") && KPluginMetaData::readStringList(md.rawData(), QStringLiteral("X-Plasma-ContainmentCategories")).contains(QStringLiteral("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 0; } QList availableLocations; availableLocations << Plasma::Types::LeftEdge << Plasma::Types::TopEdge << Plasma::Types::RightEdge << Plasma::Types::BottomEdge; foreach (const Plasma::Containment *cont, m_panelViews.keys()) { availableLocations.removeAll(cont->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()); foreach (QScreen *screen, QGuiApplication::screens()) { //m_panelViews.contains(panel) == false iff addPanel is executed in a startup script if (screen->geometry().contains(cursorPos) && m_panelViews.contains(panel)) { m_panelViews[panel]->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; for (Plasma::Containment *c : m_desktopContainments.value(activity)) { //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(!m_desktopContainments.value(activity).values().contains(containment)); if (cont) { cont->destroy(); } m_desktopContainments[activity].insert(containment); //when a containment gets deleted update our map of containments connect(containment, SIGNAL(destroyed(QObject*)), this, SLOT(desktopContainmentDestroyed(QObject*))); } void ShellCorona::desktopContainmentDestroyed(QObject *obj) { // when QObject::destroyed arrives, ~Plasma::Containment has run, // members of Containment are not accessible anymore, // so keep ugly bookeeping by hand auto containment = static_cast(obj); //explicitly specify the range by reference, as we need to remove stuff from the sets for (QSet &a : m_desktopContainments) { QMutableSetIterator it(a); while (it.hasNext()) { it.next(); if (it.value() == containment) { it.remove(); return; } } } } void ShellCorona::showOpenGLNotCompatibleWarning() { static bool s_multipleInvokations = false; if (s_multipleInvokations) { return; } s_multipleInvokations = true; QCoreApplication::setAttribute(Qt::AA_ForceRasterWidgets); QMessageBox::critical(nullptr, i18n("Plasma Failed To Start"), i18n("Plasma is unable to start as it could not correctly use OpenGL 2.\n Please check that your graphic drivers are set up correctly.")); qCritical("Open GL context could not be created"); // this doesn't work and I have no idea why. QCoreApplication::exit(1); } 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(); } 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() { foreach(PanelView* view, 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 = [this](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"