/* SPDX-FileCopyrightText: 2019 Marco Martin SPDX-License-Identifier: LGPL-2.0-or-later */ #include "appletslayout.h" #include "appletcontainer.h" #include "containmentlayoutmanager_debug.h" #include "gridlayoutmanager.h" #include #include #include #include // Plasma #include #include #include AppletsLayout::AppletsLayout(QQuickItem *parent) : QQuickItem(parent) { m_layoutManager = new GridLayoutManager(this); setFlags(QQuickItem::ItemIsFocusScope); setAcceptedMouseButtons(Qt::LeftButton); m_saveLayoutTimer = new QTimer(this); m_saveLayoutTimer->setSingleShot(true); m_saveLayoutTimer->setInterval(100); connect(m_layoutManager, &AbstractLayoutManager::layoutNeedsSaving, m_saveLayoutTimer, QOverload<>::of(&QTimer::start)); connect(m_saveLayoutTimer, &QTimer::timeout, this, [this]() { // We can't assume m_containment to be valid: if we load in a plasmoid that can run also // in "applet" mode, m_containment will never be valid if (!m_containment) { return; } // We can't save the layout during bootup, for performance reasons and to avoid race consitions as much as possible, so if we needto save and still // starting up, don't actually savenow, but we will when Corona::startupCompleted is emitted if (!m_configKey.isEmpty() && m_containment && m_containment->corona()->isStartupCompleted()) { const QString serializedConfig = m_layoutManager->serializeLayout(); m_containment->config().writeEntry(m_configKey, serializedConfig); m_containment->config().writeEntry(m_fallbackConfigKey, serializedConfig); // FIXME: something more efficient m_layoutManager->parseLayout(serializedConfig); m_savedSize = size(); m_containment->corona()->requireConfigSync(); } }); m_layoutChangeTimer = new QTimer(this); m_layoutChangeTimer->setSingleShot(true); m_layoutChangeTimer->setInterval(100); connect(m_layoutChangeTimer, &QTimer::timeout, this, [this]() { // We can't assume m_containment to be valid: if we load in a plasmoid that can run also // in "applet" mode, m_containment will never be valid if (!m_containment) { return; } const QString &serializedConfig = m_containment->config().readEntry(m_configKey, ""); if ((m_layoutChanges & ConfigKeyChange) && !serializedConfig.isEmpty()) { if (!m_configKey.isEmpty() && m_containment) { m_layoutManager->parseLayout(serializedConfig); if (width() > 0 && height() > 0) { m_layoutManager->resetLayoutFromConfig(); m_savedSize = size(); } } } else if (m_layoutChanges & SizeChange) { const QRect newGeom(x(), y(), width(), height()); // The size has been restored from the last one it has been saved: restore that exact same layout if (newGeom.size() == m_savedSize) { m_layoutManager->resetLayoutFromConfig(); // If the resize is consequence of a screen resolution change, queue a relayout maintaining the distance between screen edges } else if (!m_geometryBeforeResolutionChange.isEmpty()) { m_layoutManager->layoutGeometryChanged(newGeom, m_geometryBeforeResolutionChange); m_geometryBeforeResolutionChange = QRectF(); // Heuristically relayout items only when the plasma startup is fully completed } else { polish(); } } m_layoutChanges = NoChange; }); m_pressAndHoldTimer = new QTimer(this); m_pressAndHoldTimer->setSingleShot(true); connect(m_pressAndHoldTimer, &QTimer::timeout, this, [this]() { setEditMode(true); }); } AppletsLayout::~AppletsLayout() { } PlasmaQuick::AppletQuickItem *AppletsLayout::containment() const { return m_containmentItem; } void AppletsLayout::setContainment(PlasmaQuick::AppletQuickItem *containmentItem) { // Forbid changing containmentItem at runtime if (m_containmentItem || containmentItem == m_containmentItem || !containmentItem->applet() || !containmentItem->applet()->isContainment()) { qCWarning(CONTAINMENTLAYOUTMANAGER_DEBUG) << "Error: cannot change the containment to AppletsLayout"; return; } // Can't assign containments that aren't parents QQuickItem *candidate = parentItem(); while (candidate) { if (candidate == m_containmentItem) { break; } candidate = candidate->parentItem(); } if (candidate != m_containmentItem) { return; } m_containmentItem = containmentItem; m_containment = static_cast(m_containmentItem->applet()); connect(m_containmentItem, SIGNAL(appletAdded(QObject *, int, int)), this, SLOT(appletAdded(QObject *, int, int))); connect(m_containmentItem, SIGNAL(appletRemoved(QObject *)), this, SLOT(appletRemoved(QObject *))); Q_EMIT containmentChanged(); } QString AppletsLayout::configKey() const { return m_configKey; } void AppletsLayout::setConfigKey(const QString &key) { if (m_configKey == key) { return; } m_configKey = key; // Reloading everything from the new config is expansive, event compress it m_layoutChanges |= ConfigKeyChange; m_layoutChangeTimer->start(); Q_EMIT configKeyChanged(); } QString AppletsLayout::fallbackConfigKey() const { return m_fallbackConfigKey; } void AppletsLayout::setFallbackConfigKey(const QString &key) { if (m_fallbackConfigKey == key) { return; } m_fallbackConfigKey = key; Q_EMIT fallbackConfigKeyChanged(); } QJSValue AppletsLayout::acceptsAppletCallback() const { return m_acceptsAppletCallback; } qreal AppletsLayout::minimumItemWidth() const { return m_minimumItemSize.width(); } void AppletsLayout::setMinimumItemWidth(qreal width) { if (qFuzzyCompare(width, m_minimumItemSize.width())) { return; } m_minimumItemSize.setWidth(width); Q_EMIT minimumItemWidthChanged(); } qreal AppletsLayout::minimumItemHeight() const { return m_minimumItemSize.height(); } void AppletsLayout::setMinimumItemHeight(qreal height) { if (qFuzzyCompare(height, m_minimumItemSize.height())) { return; } m_minimumItemSize.setHeight(height); Q_EMIT minimumItemHeightChanged(); } qreal AppletsLayout::defaultItemWidth() const { return m_defaultItemSize.width(); } void AppletsLayout::setDefaultItemWidth(qreal width) { if (qFuzzyCompare(width, m_defaultItemSize.width())) { return; } m_defaultItemSize.setWidth(width); Q_EMIT defaultItemWidthChanged(); } qreal AppletsLayout::defaultItemHeight() const { return m_defaultItemSize.height(); } void AppletsLayout::setDefaultItemHeight(qreal height) { if (qFuzzyCompare(height, m_defaultItemSize.height())) { return; } m_defaultItemSize.setHeight(height); Q_EMIT defaultItemHeightChanged(); } qreal AppletsLayout::cellWidth() const { return m_layoutManager->cellSize().width(); } void AppletsLayout::setCellWidth(qreal width) { if (qFuzzyCompare(width, m_layoutManager->cellSize().width())) { return; } m_layoutManager->setCellSize(QSizeF(width, m_layoutManager->cellSize().height())); Q_EMIT cellWidthChanged(); } qreal AppletsLayout::cellHeight() const { return m_layoutManager->cellSize().height(); } void AppletsLayout::setCellHeight(qreal height) { if (qFuzzyCompare(height, m_layoutManager->cellSize().height())) { return; } m_layoutManager->setCellSize(QSizeF(m_layoutManager->cellSize().width(), height)); Q_EMIT cellHeightChanged(); } void AppletsLayout::setAcceptsAppletCallback(const QJSValue &callback) { if (m_acceptsAppletCallback.strictlyEquals(callback)) { return; } if (!callback.isNull() && !callback.isCallable()) { return; } m_acceptsAppletCallback = callback; Q_EMIT acceptsAppletCallbackChanged(); } QQmlComponent *AppletsLayout::appletContainerComponent() const { return m_appletContainerComponent; } void AppletsLayout::setAppletContainerComponent(QQmlComponent *component) { if (m_appletContainerComponent == component) { return; } m_appletContainerComponent = component; Q_EMIT appletContainerComponentChanged(); } AppletsLayout::EditModeCondition AppletsLayout::editModeCondition() const { return m_editModeCondition; } void AppletsLayout::setEditModeCondition(AppletsLayout::EditModeCondition condition) { if (m_editModeCondition == condition) { return; } if (m_editModeCondition == Locked) { setEditMode(false); } m_editModeCondition = condition; Q_EMIT editModeConditionChanged(); } bool AppletsLayout::editMode() const { return m_editMode; } void AppletsLayout::setEditMode(bool editMode) { if (m_editMode == editMode) { return; } m_editMode = editMode; Q_EMIT editModeChanged(); } ItemContainer *AppletsLayout::placeHolder() const { return m_placeHolder; } void AppletsLayout::setPlaceHolder(ItemContainer *placeHolder) { if (m_placeHolder == placeHolder) { return; } m_placeHolder = placeHolder; m_placeHolder->setParentItem(this); m_placeHolder->setZ(9999); m_placeHolder->setOpacity(false); Q_EMIT placeHolderChanged(); } QQuickItem *AppletsLayout::eventManagerToFilter() const { return m_eventManagerToFilter; } void AppletsLayout::setEventManagerToFilter(QQuickItem *item) { if (m_eventManagerToFilter == item) { return; } m_eventManagerToFilter = item; setFiltersChildMouseEvents(m_eventManagerToFilter); Q_EMIT eventManagerToFilterChanged(); } void AppletsLayout::save() { m_saveLayoutTimer->start(); } void AppletsLayout::showPlaceHolderAt(const QRectF &geom) { if (!m_placeHolder) { return; } m_placeHolder->setPosition(geom.topLeft()); m_placeHolder->setSize(geom.size()); m_layoutManager->positionItem(m_placeHolder); m_placeHolder->setProperty("opacity", 1); } void AppletsLayout::showPlaceHolderForItem(ItemContainer *item) { if (!m_placeHolder) { return; } m_placeHolder->setPreferredLayoutDirection(item->preferredLayoutDirection()); m_placeHolder->setPosition(item->position()); m_placeHolder->setSize(item->size()); m_layoutManager->positionItem(m_placeHolder); m_placeHolder->setProperty("opacity", 1); } void AppletsLayout::hidePlaceHolder() { if (!m_placeHolder) { return; } m_placeHolder->setProperty("opacity", 0); } bool AppletsLayout::isRectAvailable(qreal x, qreal y, qreal width, qreal height) { return m_layoutManager->isRectAvailable(QRectF(x, y, width, height)); } bool AppletsLayout::itemIsManaged(ItemContainer *item) { if (!item) { return false; } return m_layoutManager->itemIsManaged(item); } void AppletsLayout::positionItem(ItemContainer *item) { if (!item) { return; } item->setParent(this); m_layoutManager->positionItemAndAssign(item); } void AppletsLayout::restoreItem(ItemContainer *item) { m_layoutManager->restoreItem(item); } void AppletsLayout::releaseSpace(ItemContainer *item) { if (!item) { return; } m_layoutManager->releaseSpace(item); } void AppletsLayout::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) { // Ignore completely moves without resize if (newGeometry.size() == oldGeometry.size()) { QQuickItem::geometryChanged(newGeometry, oldGeometry); return; } // Don't care for anything happening before startup completion if (!m_containment || !m_containment->corona() || !m_containment->corona()->isStartupCompleted()) { QQuickItem::geometryChanged(newGeometry, oldGeometry); return; } // Only do a layouting procedure if we received a valid size if (!newGeometry.isEmpty()) { m_layoutChanges |= SizeChange; m_layoutChangeTimer->start(); } QQuickItem::geometryChanged(newGeometry, oldGeometry); } void AppletsLayout::updatePolish() { m_layoutManager->resetLayout(); m_savedSize = size(); } void AppletsLayout::componentComplete() { if (!m_containment || !m_containmentItem) { QQuickItem::componentComplete(); return; } if (!m_configKey.isEmpty()) { const QString &serializedConfig = m_containment->config().readEntry(m_configKey, ""); if (!serializedConfig.isEmpty()) { m_layoutManager->parseLayout(serializedConfig); } else { m_layoutManager->parseLayout(m_containment->config().readEntry(m_fallbackConfigKey, "")); } } const QList appletObjects = m_containmentItem->property("applets").value>(); for (auto *obj : appletObjects) { PlasmaQuick::AppletQuickItem *appletItem = qobject_cast(obj); if (!obj) { continue; } AppletContainer *container = createContainerForApplet(appletItem); if (width() > 0 && height() > 0) { m_layoutManager->positionItemAndAssign(container); } } // layout all extra non applet items if (width() > 0 && height() > 0) { for (auto *child : childItems()) { ItemContainer *item = qobject_cast(child); if (item && item != m_placeHolder && !m_layoutManager->itemIsManaged(item)) { m_layoutManager->positionItemAndAssign(item); } } } if (m_containment && m_containment->corona()) { // We inhibit save during startup, so actually save now that startup is completed connect(m_containment->corona(), &Plasma::Corona::startupCompleted, this, [this]() { save(); }); // When the screen geometry changes, we need to know the geometry just before it did, so we can apply out heuristic of keeping the distance with borders // constant connect(m_containment->corona(), &Plasma::Corona::screenGeometryChanged, this, [this](int id) { if (m_containment->screen() == id) { m_geometryBeforeResolutionChange = QRectF(x(), y(), width(), height()); } }); } QQuickItem::componentComplete(); } bool AppletsLayout::childMouseEventFilter(QQuickItem *item, QEvent *event) { if (item != m_eventManagerToFilter) { return QQuickItem::childMouseEventFilter(item, event); } switch (event->type()) { case QEvent::MouseButtonPress: { QMouseEvent *me = static_cast(event); if (me->buttons() & Qt::LeftButton) { mousePressEvent(me); } break; } case QEvent::MouseMove: { QMouseEvent *me = static_cast(event); mouseMoveEvent(me); break; } case QEvent::MouseButtonRelease: { QMouseEvent *me = static_cast(event); mouseReleaseEvent(me); break; } case QEvent::UngrabMouse: mouseUngrabEvent(); break; default: break; } return QQuickItem::childMouseEventFilter(item, event); } void AppletsLayout::mousePressEvent(QMouseEvent *event) { forceActiveFocus(Qt::MouseFocusReason); if (!m_editMode && m_editModeCondition == AppletsLayout::Manual) { return; } if (!m_editMode && m_editModeCondition == AppletsLayout::AfterPressAndHold) { m_pressAndHoldTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval()); } m_mouseDownWasEditMode = m_editMode; m_mouseDownPosition = event->windowPos(); // event->setAccepted(false); } void AppletsLayout::mouseMoveEvent(QMouseEvent *event) { if (!m_editMode && m_editModeCondition == AppletsLayout::Manual) { return; } if (!m_editMode && QPointF(event->windowPos() - m_mouseDownPosition).manhattanLength() >= QGuiApplication::styleHints()->startDragDistance()) { m_pressAndHoldTimer->stop(); } } void AppletsLayout::mouseReleaseEvent(QMouseEvent *event) { if (m_editMode && m_mouseDownWasEditMode // By only accepting synthetyzed events, this makes the // close by tapping in any empty area only work with real // touch events, as we want a different behavior between desktop // and tablet mode && (event->source() == Qt::MouseEventSynthesizedBySystem || event->source() == Qt::MouseEventSynthesizedByQt) && QPointF(event->windowPos() - m_mouseDownPosition).manhattanLength() < QGuiApplication::styleHints()->startDragDistance()) { setEditMode(false); } m_pressAndHoldTimer->stop(); if (!m_editMode) { for (auto *child : childItems()) { ItemContainer *item = qobject_cast(child); if (item && item != m_placeHolder) { item->setEditMode(false); } } } } void AppletsLayout::mouseUngrabEvent() { m_pressAndHoldTimer->stop(); } void AppletsLayout::appletAdded(QObject *applet, int x, int y) { PlasmaQuick::AppletQuickItem *appletItem = qobject_cast(applet); // maybe even an assert? if (!appletItem) { return; } if (m_acceptsAppletCallback.isCallable()) { QQmlEngine *engine = QQmlEngine::contextForObject(this)->engine(); Q_ASSERT(engine); QJSValueList args; args << engine->newQObject(applet) << QJSValue(x) << QJSValue(y); if (!m_acceptsAppletCallback.call(args).toBool()) { Q_EMIT appletRefused(applet, x, y); return; } } AppletContainer *container = createContainerForApplet(appletItem); container->setPosition(QPointF(x, y)); container->setVisible(true); m_layoutManager->positionItemAndAssign(container); } void AppletsLayout::appletRemoved(QObject *applet) { PlasmaQuick::AppletQuickItem *appletItem = qobject_cast(applet); // maybe even an assert? if (!appletItem) { return; } AppletContainer *container = m_containerForApplet.value(appletItem); if (!container) { return; } m_layoutManager->releaseSpace(container); m_containerForApplet.remove(appletItem); appletItem->setParentItem(this); container->deleteLater(); } AppletContainer *AppletsLayout::createContainerForApplet(PlasmaQuick::AppletQuickItem *appletItem) { AppletContainer *container = m_containerForApplet.value(appletItem); if (container) { return container; } bool createdFromQml = true; if (m_appletContainerComponent) { QQmlContext *context = QQmlEngine::contextForObject(this); Q_ASSERT(context); QObject *instance = m_appletContainerComponent->beginCreate(context); container = qobject_cast(instance); if (container) { container->setParentItem(this); } else { qCWarning(CONTAINMENTLAYOUTMANAGER_DEBUG) << "Error: provided component not an AppletContainer instance"; if (instance) { instance->deleteLater(); } createdFromQml = false; } } if (!container) { container = new AppletContainer(this); } container->setVisible(false); const QSizeF appletSize = appletItem->size(); container->setContentItem(appletItem); m_containerForApplet[appletItem] = container; container->setLayout(this); container->setKey(QLatin1String("Applet-") + QString::number(appletItem->applet()->id())); const bool geometryWasSaved = m_layoutManager->restoreItem(container); if (!geometryWasSaved) { container->setPosition(QPointF(appletItem->x() - container->leftPadding(), appletItem->y() - container->topPadding())); if (!appletSize.isEmpty()) { container->setSize(QSizeF(qMax(m_minimumItemSize.width(), appletSize.width() + container->leftPadding() + container->rightPadding()), qMax(m_minimumItemSize.height(), appletSize.height() + container->topPadding() + container->bottomPadding()))); } } if (m_appletContainerComponent && createdFromQml) { m_appletContainerComponent->completeCreate(); } // NOTE: This has to be done here as we need the component completed to have all the bindings evaluated if (!geometryWasSaved && appletSize.isEmpty()) { if (container->initialSize().width() > m_minimumItemSize.width() && container->initialSize().height() > m_minimumItemSize.height()) { const QSizeF size = m_layoutManager->cellAlignedContainingSize(container->initialSize()); container->setSize(size); } else { container->setSize( QSizeF(qMax(m_minimumItemSize.width(), m_defaultItemSize.width()), qMax(m_minimumItemSize.height(), m_defaultItemSize.height()))); } } container->setVisible(true); appletItem->setVisible(true); return container; } #include "moc_appletslayout.cpp"