diff --git a/components/containmentlayoutmanager/appletslayout.cpp b/components/containmentlayoutmanager/appletslayout.cpp index 7e3bdea4e..6ac980175 100644 --- a/components/containmentlayoutmanager/appletslayout.cpp +++ b/components/containmentlayoutmanager/appletslayout.cpp @@ -1,669 +1,670 @@ /* * Copyright 2019 by Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "appletslayout.h" #include "appletcontainer.h" #include "gridlayoutmanager.h" #include #include #include #include // Plasma #include #include #include AppletsLayout::AppletsLayout(QQuickItem *parent) : QQuickItem(parent) { m_layoutManager = new GridLayoutManager(this); setFlags(QQuickItem::ItemIsFocusScope); setAcceptedMouseButtons(Qt::LeftButton); m_saveLayoutTimer = new QTimer(this); m_saveLayoutTimer->setSingleShot(true); m_saveLayoutTimer->setInterval(100); connect(m_layoutManager, &AbstractLayoutManager::layoutNeedsSaving, m_saveLayoutTimer, QOverload<>::of(&QTimer::start)); connect(m_saveLayoutTimer, &QTimer::timeout, this, [this] () { if (!m_configKey.isEmpty() && m_containment && m_containment->corona()->isStartupCompleted()) { const QString serializedConfig = m_layoutManager->serializeLayout(); m_containment->config().writeEntry(m_configKey, serializedConfig); //FIXME: something more efficient m_layoutManager->parseLayout(serializedConfig); m_savedSize = size(); m_containment->corona()->requireConfigSync(); } }); m_configKeyChangeTimer = new QTimer(this); m_configKeyChangeTimer->setSingleShot(true); m_configKeyChangeTimer->setInterval(100); connect(m_configKeyChangeTimer, &QTimer::timeout, this, [this] () { if (!m_configKey.isEmpty() && m_containment) { m_layoutManager->parseLayout(m_containment->config().readEntry(m_configKey, "")); if (width() > 0 && height() > 0) { m_layoutManager->resetLayoutFromConfig(); m_savedSize = size(); } } }); m_pressAndHoldTimer = new QTimer(this); m_pressAndHoldTimer->setSingleShot(true); connect(m_pressAndHoldTimer, &QTimer::timeout, this, [this]() { setEditMode(true); }); m_sizeSyncTimer = new QTimer(this); m_sizeSyncTimer->setSingleShot(true); m_sizeSyncTimer->setInterval(150); connect(m_sizeSyncTimer, &QTimer::timeout, this, [this]() { const QRect newGeom(x(), y(), width(), height()); // The size has been restored from the last one it has been saved: restore that exact same layout if (newGeom.size() == m_savedSize) { m_layoutManager->resetLayoutFromConfig(); // If the resize is consequence of a screen resolution change, queue a relayout maintaining the distance between screen edges } else if (!m_geometryBeforeResolutionChange.isEmpty()) { m_layoutManager->layoutGeometryChanged(newGeom, m_geometryBeforeResolutionChange); m_geometryBeforeResolutionChange = QRectF(); // Heuristically relayout items only when the plasma startup is fully completed } else { polish(); } }); connect(this, &QQuickItem::focusChanged, this, [this]() { if (!hasFocus()) { setEditMode(false); } }); } AppletsLayout::~AppletsLayout() { } PlasmaQuick::AppletQuickItem *AppletsLayout::containment() const { return m_containmentItem; } void AppletsLayout::setContainment(PlasmaQuick::AppletQuickItem *containmentItem) { // Forbid changing containmentItem at runtime if (m_containmentItem || containmentItem == m_containmentItem || !containmentItem->applet() || !containmentItem->applet()->isContainment()) { qWarning() << "Error: cannot change the containment to AppletsLayout"; return; } // Can't assign containments that aren't parents QQuickItem *candidate = parentItem(); while (candidate) { if (candidate == m_containmentItem) { break; } candidate = candidate->parentItem(); } if (candidate != m_containmentItem) { return; } m_containmentItem = containmentItem; m_containment = static_cast(m_containmentItem->applet()); connect(m_containmentItem, SIGNAL(appletAdded(QObject *, int, int)), this, SLOT(appletAdded(QObject *, int, int))); connect(m_containmentItem, SIGNAL(appletRemoved(QObject *)), this, SLOT(appletRemoved(QObject *))); emit containmentChanged(); } QString AppletsLayout::configKey() const { return m_configKey; } void AppletsLayout::setConfigKey(const QString &key) { if (m_configKey == key) { return; } m_configKey = key; // Reloading everything from the new config is expansive, event compress it m_configKeyChangeTimer->start(); emit configKeyChanged(); } QJSValue AppletsLayout::acceptsAppletCallback() const { return m_acceptsAppletCallback; } qreal AppletsLayout::minimumItemWidth() const { return m_minimumItemSize.width(); } void AppletsLayout::setMinimumItemWidth(qreal width) { if (qFuzzyCompare(width, m_minimumItemSize.width())) { return; } m_minimumItemSize.setWidth(width); emit minimumItemWidthChanged(); } qreal AppletsLayout::minimumItemHeight() const { return m_minimumItemSize.height(); } void AppletsLayout::setMinimumItemHeight(qreal height) { if (qFuzzyCompare(height, m_minimumItemSize.height())) { return; } m_minimumItemSize.setHeight(height); emit minimumItemHeightChanged(); } qreal AppletsLayout::defaultItemWidth() const { return m_defaultItemSize.width(); } void AppletsLayout::setDefaultItemWidth(qreal width) { if (qFuzzyCompare(width, m_defaultItemSize.width())) { return; } m_defaultItemSize.setWidth(width); emit defaultItemWidthChanged(); } qreal AppletsLayout::defaultItemHeight() const { return m_defaultItemSize.height(); } void AppletsLayout::setDefaultItemHeight(qreal height) { if (qFuzzyCompare(height, m_defaultItemSize.height())) { return; } m_defaultItemSize.setHeight(height); emit defaultItemHeightChanged(); } qreal AppletsLayout::cellWidth() const { return m_layoutManager->cellSize().width(); } void AppletsLayout::setCellWidth(qreal width) { if (qFuzzyCompare(width, m_layoutManager->cellSize().width())) { return; } m_layoutManager->setCellSize(QSizeF(width, m_layoutManager->cellSize().height())); emit cellWidthChanged(); } qreal AppletsLayout::cellHeight() const { return m_layoutManager->cellSize().height(); } void AppletsLayout::setCellHeight(qreal height) { if (qFuzzyCompare(height, m_layoutManager->cellSize().height())) { return; } m_layoutManager->setCellSize(QSizeF(m_layoutManager->cellSize().width(), height)); emit cellHeightChanged(); } void AppletsLayout::setAcceptsAppletCallback(const QJSValue& callback) { if (m_acceptsAppletCallback.strictlyEquals(callback)) { return; } if (!callback.isNull() && !callback.isCallable()) { return; } m_acceptsAppletCallback = callback; Q_EMIT acceptsAppletCallbackChanged(); } QQmlComponent *AppletsLayout::appletContainerComponent() const { return m_appletContainerComponent; } void AppletsLayout::setAppletContainerComponent(QQmlComponent *component) { if (m_appletContainerComponent == component) { return; } m_appletContainerComponent = component; emit appletContainerComponentChanged(); } AppletsLayout::EditModeCondition AppletsLayout::editModeCondition() const { return m_editModeCondition; } void AppletsLayout::setEditModeCondition(AppletsLayout::EditModeCondition condition) { if (m_editModeCondition == condition) { return; } if (m_editModeCondition == Locked) { setEditMode(false); } m_editModeCondition = condition; emit editModeConditionChanged(); } bool AppletsLayout::editMode() const { return m_editMode; } void AppletsLayout::setEditMode(bool editMode) { if (m_editMode == editMode) { return; } m_editMode = editMode; emit editModeChanged(); } ItemContainer *AppletsLayout::placeHolder() const { return m_placeHolder; } void AppletsLayout::setPlaceHolder(ItemContainer *placeHolder) { if (m_placeHolder == placeHolder) { return; } m_placeHolder = placeHolder; m_placeHolder->setParentItem(this); m_placeHolder->setZ(9999); m_placeHolder->setOpacity(false); emit placeHolderChanged(); } 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(QQuickItem *item) +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) { return m_layoutManager->itemIsManaged(item); } void AppletsLayout::positionItem(ItemContainer *item) { item->setParent(this); m_layoutManager->positionItemAndAssign(item); } void AppletsLayout::releaseSpace(ItemContainer *item) { 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 anythin happening before startup completion if (!m_containment || !m_containment->corona() || !m_containment->corona()->isStartupCompleted()) { QQuickItem::geometryChanged(newGeometry, oldGeometry); return; } // Only do a layouting procedure if we received a valid size if (!newGeometry.isEmpty()) { m_sizeSyncTimer->start(); } QQuickItem::geometryChanged(newGeometry, oldGeometry); } void AppletsLayout::updatePolish() { m_layoutManager->resetLayout(); m_savedSize = size(); } void AppletsLayout::componentComplete() { if (!m_containment || !m_containmentItem) { QQuickItem::componentComplete(); return; } if (!m_configKey.isEmpty()) { m_layoutManager->parseLayout(m_containment->config().readEntry(m_configKey, "")); } QList appletObjects = m_containmentItem->property("applets").value >(); for (auto *obj : appletObjects) { PlasmaQuick::AppletQuickItem *appletItem = qobject_cast(obj); if (!obj) { continue; } AppletContainer *container = createContainerForApplet(appletItem); if (width() > 0 && height() > 0) { m_layoutManager->positionItemAndAssign(container); } } //layout all extra non applet items if (width() > 0 && height() > 0) { for (auto *child : childItems()) { ItemContainer *item = qobject_cast(child); if (item && item != m_placeHolder && !m_layoutManager->itemIsManaged(item)) { m_layoutManager->positionItemAndAssign(item); } } } if (m_containment && m_containment->corona()) { connect(m_containment->corona(), &Plasma::Corona::startupCompleted, this, [this](){ // m_savedSize = size(); }); // When the screen geometry changes, we need to know the geometry just before it did, so we can apply out heuristic of keeping the distance with borders constant connect(m_containment->corona(), &Plasma::Corona::screenGeometryChanged, this, [this](int id){ if (m_containment->screen() == id) { m_geometryBeforeResolutionChange = QRectF(x(), y(), width(), height()); } }); } QQuickItem::componentComplete(); } 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 && 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::appletAdded(QObject *applet, int x, int y) { PlasmaQuick::AppletQuickItem *appletItem = qobject_cast(applet); //maybe even an assert? if (!appletItem) { return; } if (m_acceptsAppletCallback.isCallable()) { QQmlEngine *engine = QQmlEngine::contextForObject(this)->engine(); Q_ASSERT(engine); QJSValueList args; args << engine->newQObject(applet) << QJSValue(x) << QJSValue(y); if (!m_acceptsAppletCallback.call(args).toBool()) { emit appletRefused(applet, x, y); return; } } AppletContainer *container = createContainerForApplet(appletItem); container->setPosition(QPointF(x, y)); container->setVisible(true); m_layoutManager->positionItemAndAssign(container); } void AppletsLayout::appletRemoved(QObject *applet) { PlasmaQuick::AppletQuickItem *appletItem = qobject_cast(applet); //maybe even an assert? if (!appletItem) { return; } AppletContainer *container = m_containerForApplet.value(appletItem); if (!container) { return; } m_layoutManager->releaseSpace(container); m_containerForApplet.remove(appletItem); appletItem->setParentItem(this); container->deleteLater(); } AppletContainer *AppletsLayout::createContainerForApplet(PlasmaQuick::AppletQuickItem *appletItem) { AppletContainer *container = m_containerForApplet.value(appletItem); if (container) { return container; } bool createdFromQml = true; if (m_appletContainerComponent) { QQmlContext *context = QQmlEngine::contextForObject(this); Q_ASSERT(context); QObject *instance = m_appletContainerComponent->beginCreate(context); container = qobject_cast(instance); if (container) { container->setParentItem(this); } else { qWarning() << "Error: provided component not an AppletContainer instance"; if (instance) { instance->deleteLater(); } createdFromQml = false; } } if (!container) { container = new AppletContainer(this); } container->setVisible(false); const QSizeF appletSize = appletItem->size(); container->setContentItem(appletItem); m_containerForApplet[appletItem] = container; container->setLayout(this); container->setKey(QLatin1String("Applet-") + QString::number(appletItem->applet()->id())); const bool geometryWasSaved = m_layoutManager->restoreItem(container); if (!geometryWasSaved) { container->setPosition(QPointF(appletItem->x() - container->leftPadding(), appletItem->y() - container->topPadding())); if (!appletSize.isEmpty()) { container->setSize(QSizeF(qMax(m_minimumItemSize.width(), appletSize.width() + container->leftPadding() + container->rightPadding()), qMax(m_minimumItemSize.height(), appletSize.height() + container->topPadding() + container->bottomPadding()))); } } if (m_appletContainerComponent && createdFromQml) { m_appletContainerComponent->completeCreate(); } //NOTE: This has to be done here as we need the component completed to have all the bindings evaluated if (!geometryWasSaved && appletSize.isEmpty()) { if (container->initialSize().width() > m_minimumItemSize.width() && container->initialSize().height() > m_minimumItemSize.height()) { const QSizeF size = m_layoutManager->cellAlignedContainingSize( container->initialSize()); container->setSize(size); } else { container->setSize(QSizeF(qMax(m_minimumItemSize.width(), m_defaultItemSize.width()), qMax(m_minimumItemSize.height(), m_defaultItemSize.height()))); } } container->setVisible(true); appletItem->setVisible(true); return container; } #include "moc_appletslayout.cpp" diff --git a/components/containmentlayoutmanager/appletslayout.h b/components/containmentlayoutmanager/appletslayout.h index 43a6d16fb..781f2e007 100644 --- a/components/containmentlayoutmanager/appletslayout.h +++ b/components/containmentlayoutmanager/appletslayout.h @@ -1,210 +1,210 @@ /* * Copyright 2019 by Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #pragma once #include #include #include class QTimer; namespace Plasma { class Containment; } namespace PlasmaQuick { class AppletQuickItem; } class AbstractLayoutManager; class AppletContainer; class ItemContainer; class AppletsLayout: public QQuickItem { Q_OBJECT Q_INTERFACES(QQmlParserStatus) Q_PROPERTY(QString configKey READ configKey WRITE setConfigKey NOTIFY configKeyChanged) Q_PROPERTY(PlasmaQuick::AppletQuickItem *containment READ containment WRITE setContainment NOTIFY containmentChanged) Q_PROPERTY(QJSValue acceptsAppletCallback READ acceptsAppletCallback WRITE setAcceptsAppletCallback NOTIFY acceptsAppletCallbackChanged) Q_PROPERTY(qreal minimumItemWidth READ minimumItemWidth WRITE setMinimumItemWidth NOTIFY minimumItemWidthChanged) Q_PROPERTY(qreal minimumItemHeight READ minimumItemHeight WRITE setMinimumItemHeight NOTIFY minimumItemHeightChanged) Q_PROPERTY(qreal defaultItemWidth READ defaultItemWidth WRITE setDefaultItemWidth NOTIFY defaultItemWidthChanged) Q_PROPERTY(qreal defaultItemHeight READ defaultItemHeight WRITE setDefaultItemHeight NOTIFY defaultItemHeightChanged) Q_PROPERTY(qreal cellWidth READ cellWidth WRITE setCellWidth NOTIFY cellWidthChanged) Q_PROPERTY(qreal cellHeight READ cellHeight WRITE setCellHeight NOTIFY cellHeightChanged) Q_PROPERTY(QQmlComponent *appletContainerComponent READ appletContainerComponent WRITE setAppletContainerComponent NOTIFY appletContainerComponentChanged) Q_PROPERTY(ItemContainer *placeHolder READ placeHolder WRITE setPlaceHolder NOTIFY placeHolderChanged); Q_PROPERTY(AppletsLayout::EditModeCondition editModeCondition READ editModeCondition WRITE setEditModeCondition NOTIFY editModeConditionChanged) Q_PROPERTY(bool editMode READ editMode WRITE setEditMode NOTIFY editModeChanged) public: enum PreferredLayoutDirection { Closest = 0, LeftToRight, RightToLeft, TopToBottom, BottomToTop }; Q_ENUM(PreferredLayoutDirection) enum EditModeCondition { Locked = 0, Manual, AfterPressAndHold, }; Q_ENUM(EditModeCondition) AppletsLayout(QQuickItem *parent = nullptr); ~AppletsLayout(); // QML setters and getters QString configKey() const; void setConfigKey(const QString &key); PlasmaQuick::AppletQuickItem *containment() const; void setContainment(PlasmaQuick::AppletQuickItem *containment); QJSValue acceptsAppletCallback() const; void setAcceptsAppletCallback(const QJSValue& callback); qreal minimumItemWidth() const; void setMinimumItemWidth(qreal width); qreal minimumItemHeight() const; void setMinimumItemHeight(qreal height); qreal defaultItemWidth() const; void setDefaultItemWidth(qreal width); qreal defaultItemHeight() const; void setDefaultItemHeight(qreal height); qreal cellWidth() const; void setCellWidth(qreal width); qreal cellHeight() const; void setCellHeight(qreal height); QQmlComponent *appletContainerComponent() const; void setAppletContainerComponent(QQmlComponent *component); ItemContainer *placeHolder() const; void setPlaceHolder(ItemContainer *placeHolder); EditModeCondition editModeCondition() const; void setEditModeCondition(EditModeCondition condition); bool editMode() const; void setEditMode(bool edit); Q_INVOKABLE void save(); Q_INVOKABLE void showPlaceHolderAt(const QRectF &geom); - Q_INVOKABLE void showPlaceHolderForItem(QQuickItem *item); + Q_INVOKABLE void showPlaceHolderForItem(ItemContainer *item); Q_INVOKABLE void hidePlaceHolder(); Q_INVOKABLE bool isRectAvailable(qreal x, qreal y, qreal width, qreal height); Q_INVOKABLE bool itemIsManaged(ItemContainer *item); Q_INVOKABLE void positionItem(ItemContainer *item); Q_INVOKABLE void releaseSpace(ItemContainer *item); Q_SIGNALS: /** * An applet has been refused by the layout: acceptsAppletCallback * returned false and will need to be managed in a different way */ void appletRefused(QObject *applet, int x, int y); void configKeyChanged(); void containmentChanged(); void minimumItemWidthChanged(); void minimumItemHeightChanged(); void defaultItemWidthChanged(); void defaultItemHeightChanged(); void cellWidthChanged(); void cellHeightChanged(); void acceptsAppletCallbackChanged(); void appletContainerComponentChanged(); void placeHolderChanged(); void editModeConditionChanged(); void editModeChanged(); protected: void updatePolish() override; void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; //void classBegin() override; void componentComplete() override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; private Q_SLOTS: void appletAdded(QObject *applet, int x, int y); void appletRemoved(QObject *applet); private: AppletContainer *createContainerForApplet(PlasmaQuick::AppletQuickItem *appletItem); QString m_configKey; QTimer *m_saveLayoutTimer; QTimer *m_configKeyChangeTimer; PlasmaQuick::AppletQuickItem *m_containmentItem = nullptr; Plasma::Containment *m_containment = nullptr; QQmlComponent *m_appletContainerComponent = nullptr; AbstractLayoutManager *m_layoutManager = nullptr; QPointer m_placeHolder; QTimer *m_pressAndHoldTimer; QTimer *m_sizeSyncTimer; QJSValue m_acceptsAppletCallback; AppletsLayout::EditModeCondition m_editModeCondition = AppletsLayout::Manual; QHash m_containerForApplet; QSizeF m_minimumItemSize; QSizeF m_defaultItemSize; QSizeF m_savedSize; QRectF m_geometryBeforeResolutionChange; QPointF m_mouseDownPosition = QPoint(-1, -1); bool m_mouseDownWasEditMode = false; bool m_editMode = false; }; diff --git a/components/containmentlayoutmanager/gridlayoutmanager.cpp b/components/containmentlayoutmanager/gridlayoutmanager.cpp index 7574882c6..fb8a126bc 100644 --- a/components/containmentlayoutmanager/gridlayoutmanager.cpp +++ b/components/containmentlayoutmanager/gridlayoutmanager.cpp @@ -1,551 +1,587 @@ /* * Copyright 2019 by Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "gridlayoutmanager.h" #include "appletslayout.h" #include GridLayoutManager::GridLayoutManager(AppletsLayout *layout) : AbstractLayoutManager(layout) { } GridLayoutManager::~GridLayoutManager() { } QString GridLayoutManager::serializeLayout() const { QString result; for (auto *item : layout()->childItems()) { ItemContainer *itemCont = qobject_cast(item); if (itemCont && itemCont != layout()->placeHolder()) { result += itemCont->key() + QLatin1Char(':') + QString::number(itemCont->x()) + QLatin1Char(',') + QString::number(itemCont->y()) + QLatin1Char(',') + QString::number(itemCont->width()) + QLatin1Char(',') + QString::number(itemCont->height()) + QLatin1Char(',') + QString::number(itemCont->rotation()) + QLatin1Char(';'); } } return result; } void GridLayoutManager::parseLayout(const QString &savedLayout) { m_parsedConfig.clear(); QStringList itemsConfigs = savedLayout.split(QLatin1Char(';')); for (const auto &itemString : itemsConfigs) { QStringList itemConfig = itemString.split(QLatin1Char(':')); if (itemConfig.count() != 2) { continue; } QString id = itemConfig[0]; QStringList itemGeom = itemConfig[1].split(QLatin1Char(',')); if (itemGeom.count() != 5) { continue; } m_parsedConfig[id] = {itemGeom[0].toInt(), itemGeom[1].toInt(), itemGeom[2].toInt(), itemGeom[3].toInt(), itemGeom[4].toInt()}; } } bool GridLayoutManager::itemIsManaged(ItemContainer *item) { return m_pointsForItem.contains(item); } inline void maintainItemEdgeAlignment(QQuickItem *item, const QRectF &newRect, const QRectF &oldRect) { const qreal leftDist = item->x() - oldRect.x(); const qreal hCenterDist = item->x() + item->width()/2 - oldRect.center().x(); const qreal rightDist = oldRect.right() - item->x() - item->width(); qreal hMin = qMin(qMin(qAbs(leftDist), qAbs(hCenterDist)), qAbs(rightDist)); if (qFuzzyCompare(hMin, qAbs(leftDist))) { // Right alignment, do nothing } else if (qFuzzyCompare(hMin, qAbs(hCenterDist))) { item->setX(newRect.center().x() - item->width()/2 + hCenterDist); } else if (qFuzzyCompare(hMin, qAbs(rightDist))) { item->setX(newRect.right() - item->width() - rightDist ); } const qreal topDist = item->y() - oldRect.y(); const qreal vCenterDist = item->y() + item->height()/2 - oldRect.center().y(); const qreal bottomDist = oldRect.bottom() - item->y() - item->height(); qreal vMin = qMin(qMin(qAbs(topDist), qAbs(vCenterDist)), qAbs(bottomDist)); if (qFuzzyCompare(vMin, qAbs(topDist))) { // Top alignment, do nothing } else if (qFuzzyCompare(vMin, qAbs(vCenterDist))) { item->setY(newRect.center().y() - item->height()/2 + vCenterDist); } else if (qFuzzyCompare(vMin, qAbs(bottomDist))) { item->setY(newRect.bottom() - item->height() - bottomDist ); } } void GridLayoutManager::layoutGeometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) { for (auto *item : layout()->childItems()) { // Stash the old config //m_parsedConfig[item->key()] = {item->x(), item->y(), item->width(), item->height(), item->rotation()}; // Move the item to maintain the distance with the anchors point maintainItemEdgeAlignment(item, newGeometry, oldGeometry); } } void GridLayoutManager::resetLayout() { m_grid.clear(); m_pointsForItem.clear(); for (auto *item : layout()->childItems()) { ItemContainer *itemCont = qobject_cast(item); if (itemCont && itemCont != layout()->placeHolder()) { // NOTE: do not use positionItemAndAssign here, because we do not want to emit layoutNeedsSaving, to not save after resize positionItem(itemCont); assignSpaceImpl(itemCont); } } } void GridLayoutManager::resetLayoutFromConfig() { m_grid.clear(); m_pointsForItem.clear(); QList missingItems; for (auto *item : layout()->childItems()) { ItemContainer *itemCont = qobject_cast(item); if (itemCont && itemCont != layout()->placeHolder()) { if (!restoreItem(itemCont)) { missingItems << itemCont; } } } for (auto *item : missingItems) { // NOTE: do not use positionItemAndAssign here, because we do not want to emit layoutNeedsSaving, to not save after resize positionItem(item); assignSpaceImpl(item); } } bool GridLayoutManager::restoreItem(ItemContainer *item) { auto it = m_parsedConfig.find(item->key()); if (it != m_parsedConfig.end()) { // Actual restore item->setPosition(QPointF(it.value().x, it.value().y)); item->setSize(QSizeF(it.value().width, it.value().height)); item->setRotation(it.value().rotation); // NOTE: do not use positionItemAndAssign here, because we do not want to emit layoutNeedsSaving, to not save after resize // If size is empty the layout is not in a valid state and probably startup is not completed yet if (!layout()->size().isEmpty()) { releaseSpaceImpl(item); positionItem(item); assignSpaceImpl(item); } return true; } return false; } bool GridLayoutManager::isRectAvailable(const QRectF &rect) { //TODO: define directions in which it can grow if (rect.x() < 0 || rect.y() < 0 || rect.x() + rect.width() > layout()->width() || rect.y() + rect.height() > layout()->height()) { return false; } const QRect cellItemGeom = cellBasedGeometry(rect); for (int row = cellItemGeom.top(); row <= cellItemGeom.bottom(); ++row) { for (int column = cellItemGeom.left(); column <= cellItemGeom.right(); ++column) { if (!isCellAvailable(QPair(row, column))) { return false; } } } return true; } bool GridLayoutManager::assignSpaceImpl(ItemContainer *item) { // Don't emit extra layoutneedssaving signals releaseSpaceImpl(item); if (!isRectAvailable(itemGeometry(item))) { qWarning()<<"Trying to take space not available" << item; return false; } const QRect cellItemGeom = cellBasedGeometry(itemGeometry(item)); for (int row = cellItemGeom.top(); row <= cellItemGeom.bottom(); ++row) { for (int column = cellItemGeom.left(); column <= cellItemGeom.right(); ++column) { QPair cell(row, column); m_grid.insert(cell, item); m_pointsForItem[item].insert(cell); } } // Reorder items tab order for (auto *i2 : layout()->childItems()) { ItemContainer *item2 = qobject_cast(i2); if (item2 && item != item2 && item2 != layout()->placeHolder() && item->y() < item2->y() + item2->height() && item->x() <= item2->x()) { item->stackBefore(item2); break; } } if (item->layoutAttached()) { connect(item, &ItemContainer::sizeHintsChanged, this, [this, item]() { adjustToItemSizeHints(item); }); } return true; } void GridLayoutManager::releaseSpaceImpl(ItemContainer *item) { auto it = m_pointsForItem.find(item); if (it == m_pointsForItem.end()) { return; } for (const auto &point : it.value()) { m_grid.remove(point); } m_pointsForItem.erase(it); disconnect(item, &ItemContainer::sizeHintsChanged, this, nullptr); } int GridLayoutManager::rows() const { return layout()->height() / cellSize().height(); } int GridLayoutManager::columns() const { return layout()->width() / cellSize().width(); } void GridLayoutManager::adjustToItemSizeHints(ItemContainer *item) { if (!item->layoutAttached() || item->editMode()) { return; } bool changed = false; // Minimum const qreal newMinimumHeight = item->layoutAttached()->property("minimumHeight").toReal(); const qreal newMinimumWidth = item->layoutAttached()->property("minimumWidth").toReal(); if (newMinimumHeight > item->height()) { item->setHeight(newMinimumHeight); changed = true; } if (newMinimumWidth > item->width()) { item->setWidth(newMinimumWidth); changed = true; } // Preferred const qreal newPreferredHeight = item->layoutAttached()->property("preferredHeight").toReal(); const qreal newPreferredWidth = item->layoutAttached()->property("preferredWidth").toReal(); if (newPreferredHeight > item->height()) { item->setHeight(layout()->cellHeight() * ceil(newPreferredHeight / layout()->cellHeight())); changed = true; } if (newPreferredWidth > item->width()) { item->setWidth(layout()->cellWidth() * ceil(newPreferredWidth / layout()->cellWidth())); changed = true; } /*// Maximum : IGNORE? const qreal newMaximumHeight = item->layoutAttached()->property("preferredHeight").toReal(); const qreal newMaximumWidth = item->layoutAttached()->property("preferredWidth").toReal(); if (newMaximumHeight > 0 && newMaximumHeight < height()) { item->setHeight(newMaximumHeight); changed = true; } if (newMaximumHeight > 0 && newMaximumWidth < width()) { item->setWidth(newMaximumWidth); changed = true; }*/ // Relayout if anything changed if (changed && itemIsManaged(item)) { releaseSpace(item); positionItem(item); } } QRect GridLayoutManager::cellBasedGeometry(const QRectF &geom) const { return QRect( round(qBound(0.0, geom.x(), layout()->width() - geom.width()) / cellSize().width()), round(qBound(0.0, geom.y(), layout()->height() - geom.height()) / cellSize().height()), round((qreal)geom.width() / cellSize().width()), round((qreal)geom.height() / cellSize().height()) ); } QRect GridLayoutManager::cellBasedBoundingGeometry(const QRectF &geom) const { return QRect( floor(qBound(0.0, geom.x(), layout()->width() - geom.width()) / cellSize().width()), floor(qBound(0.0, geom.y(), layout()->height() - geom.height()) / cellSize().height()), ceil((qreal)geom.width() / cellSize().width()), ceil((qreal)geom.height() / cellSize().height()) ); } bool GridLayoutManager::isOutOfBounds(const QPair &cell) const { return cell.first < 0 || cell.second < 0 || cell.first >= rows() || cell.second >= columns(); } bool GridLayoutManager::isCellAvailable(const QPair &cell) const { return !isOutOfBounds(cell) && !m_grid.contains(cell); } QRectF GridLayoutManager::itemGeometry(QQuickItem *item) const { return QRectF(item->x(), item->y(), item->width(), item->height()); } QPair GridLayoutManager::nextCell(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const { QPair nCell = cell; switch (direction) { case AppletsLayout::AppletsLayout::BottomToTop: --nCell.first; break; case AppletsLayout::AppletsLayout::TopToBottom: ++nCell.first; break; case AppletsLayout::AppletsLayout::RightToLeft: --nCell.second; break; case AppletsLayout::AppletsLayout::LeftToRight: default: ++nCell.second; break; } return nCell; } QPair GridLayoutManager::nextAvailableCell(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const { QPair nCell = cell; while (!isOutOfBounds(nCell)) { nCell = nextCell(nCell, direction); if (isOutOfBounds(nCell)) { switch (direction) { case AppletsLayout::AppletsLayout::BottomToTop: nCell.first = rows() - 1; --nCell.second; break; case AppletsLayout::AppletsLayout::TopToBottom: nCell.first = 0; ++nCell.second; break; case AppletsLayout::AppletsLayout::RightToLeft: --nCell.first; nCell.second = columns() - 1; break; case AppletsLayout::AppletsLayout::LeftToRight: default: ++nCell.first; nCell.second = 0; break; } } if (isCellAvailable(nCell)) { return nCell; } } return QPair(-1, -1); } +QPair GridLayoutManager::nextTakenCell(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const +{ + QPair nCell = cell; + while (!isOutOfBounds(nCell)) { + nCell = nextCell(nCell, direction); + + if (isOutOfBounds(nCell)) { + switch (direction) { + case AppletsLayout::AppletsLayout::BottomToTop: + nCell.first = rows() - 1; + --nCell.second; + break; + case AppletsLayout::AppletsLayout::TopToBottom: + nCell.first = 0; + ++nCell.second; + break; + case AppletsLayout::AppletsLayout::RightToLeft: + --nCell.first; + nCell.second = columns() - 1; + break; + case AppletsLayout::AppletsLayout::LeftToRight: + default: + ++nCell.first; + nCell.second = 0; + break; + } + } + + if (!isCellAvailable(nCell)) { + return nCell; + } + } + + return QPair(-1, -1); +} + int GridLayoutManager::freeSpaceInDirection(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const { QPair nCell = cell; int avail = 0; while (isCellAvailable(nCell)) { ++avail; nCell = nextCell(nCell, direction); } return avail; } QRectF GridLayoutManager::nextAvailableSpace(ItemContainer *item, const QSizeF &minimumSize, AppletsLayout::PreferredLayoutDirection direction) const { // The mionimum size in grid units const QSize minimumGridSize( ceil((qreal)minimumSize.width() / cellSize().width()), ceil((qreal)minimumSize.height() / cellSize().height()) ); QRect itemCellGeom = cellBasedGeometry(itemGeometry(item)); itemCellGeom.setWidth(qMax(itemCellGeom.width(), minimumGridSize.width())); itemCellGeom.setHeight(qMax(itemCellGeom.height(), minimumGridSize.height())); QSize partialSize; QPair cell(itemCellGeom.y(), itemCellGeom.x()); if (direction == AppletsLayout::AppletsLayout::RightToLeft) { cell.second += itemCellGeom.width(); } else if (direction == AppletsLayout::AppletsLayout::BottomToTop) { cell.first += itemCellGeom.height(); } - while (!isOutOfBounds(cell)) { + if (!isCellAvailable(cell)) { + cell = nextAvailableCell(cell, direction); + } - if (!isCellAvailable(cell)) { - cell = nextAvailableCell(cell, direction); - } + while (!isOutOfBounds(cell)) { if (direction == AppletsLayout::LeftToRight || direction == AppletsLayout::RightToLeft) { partialSize = QSize(INT_MAX, 0); int currentRow = cell.first; for (; currentRow < cell.first + itemCellGeom.height(); ++currentRow) { const int freeRow = freeSpaceInDirection(QPair(currentRow, cell.second), direction); partialSize.setWidth(qMin(partialSize.width(), freeRow)); if (freeRow > 0) { partialSize.setHeight(partialSize.height() + 1); } else if (partialSize.height() < minimumGridSize.height()) { break; } if (partialSize.width() >= itemCellGeom.width() && partialSize.height() >= itemCellGeom.height()) { break; } else if (partialSize.width() < minimumGridSize.width()) { break; } } if (partialSize.width() >= minimumGridSize.width() && partialSize.height() >= minimumGridSize.height()) { const int width = qMin(itemCellGeom.width(), partialSize.width()) * cellSize().width(); const int height = qMin(itemCellGeom.height(), partialSize.height()) * cellSize().height(); if (direction == AppletsLayout::RightToLeft) { return QRectF((cell.second + 1) * cellSize().width() - width, cell.first * cellSize().height(), width, height); // AppletsLayout::LeftToRight } else { return QRectF(cell.second * cellSize().width(), cell.first * cellSize().height(), width, height); } } else { - cell.first = currentRow + 1; + cell = nextAvailableCell(nextTakenCell(cell, direction), direction); } } else if (direction == AppletsLayout::TopToBottom || direction == AppletsLayout::BottomToTop) { partialSize = QSize(0, INT_MAX); int currentColumn = cell.second; for (; currentColumn < cell.second + itemCellGeom.width(); ++currentColumn) { const int freeColumn = freeSpaceInDirection(QPair(cell.first, currentColumn), direction); partialSize.setHeight(qMin(partialSize.height(), freeColumn)); if (freeColumn > 0) { partialSize.setWidth(partialSize.width() + 1); } else if (partialSize.width() < minimumGridSize.width()) { break; } if (partialSize.width() >= itemCellGeom.width() && partialSize.height() >= itemCellGeom.height()) { break; } else if (partialSize.height() < minimumGridSize.height()) { break; } } if (partialSize.width() >= minimumGridSize.width() && partialSize.height() >= minimumGridSize.height()) { const int width = qMin(itemCellGeom.width(), partialSize.width()) * cellSize().width(); const int height = qMin(itemCellGeom.height(), partialSize.height()) * cellSize().height(); if (direction == AppletsLayout::BottomToTop) { return QRectF(cell.second * cellSize().width(), (cell.first + 1) * cellSize().height() - height, width, height); // AppletsLayout::TopToBottom: } else { return QRectF(cell.second * cellSize().width(), cell.first * cellSize().height(), width, height); } } else { - cell.second = currentColumn + 1; + cell = nextAvailableCell(nextTakenCell(cell, direction), direction); } } } //We didn't manage to find layout space, return invalid geometry return QRectF(); } #include "moc_gridlayoutmanager.cpp" diff --git a/components/containmentlayoutmanager/gridlayoutmanager.h b/components/containmentlayoutmanager/gridlayoutmanager.h index a5f1b0cd4..0c5a7810e 100644 --- a/components/containmentlayoutmanager/gridlayoutmanager.h +++ b/components/containmentlayoutmanager/gridlayoutmanager.h @@ -1,111 +1,114 @@ /* * Copyright 2019 by Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #pragma once #include "abstractlayoutmanager.h" #include "appletcontainer.h" class AppletsLayout; class ItemContainer; struct Geom { int x; int y; int width; int height; int rotation; }; class GridLayoutManager : public AbstractLayoutManager { Q_OBJECT public: GridLayoutManager(AppletsLayout *layout); ~GridLayoutManager(); void layoutGeometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; QString serializeLayout() const override; void parseLayout(const QString &savedLayout) override; bool itemIsManaged(ItemContainer *item) override; void resetLayout() override; void resetLayoutFromConfig() override; bool restoreItem(ItemContainer *item) override; bool isRectAvailable(const QRectF &rect) override; protected: // The rectangle as near as possible to the current item geometry which can fit it QRectF nextAvailableSpace(ItemContainer *item, const QSizeF &minimumSize, AppletsLayout::PreferredLayoutDirection direction) const override; bool assignSpaceImpl(ItemContainer *item) override; void releaseSpaceImpl(ItemContainer *item) override; private: // Total cell rows inline int rows() const; // Total cell columns inline int columns() const; // Converts the item pixel-based geometry to a cellsize-based geometry inline QRect cellBasedGeometry(const QRectF &geom) const; // Converts the item pixel-based geometry to a cellsize-based geometry // This is the bounding geometry, usually larger than cellBasedGeometry inline QRect cellBasedBoundingGeometry(const QRectF &geom) const; // true if the cell is out of the bounds of the containment inline bool isOutOfBounds(const QPair &cell) const; // True if the space for the given cell is available inline bool isCellAvailable(const QPair &cell) const; // Returns the qrect geometry for an item inline QRectF itemGeometry(QQuickItem *item) const; // The next cell given the direction QPair nextCell(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const; // The next cell that is available given the direction QPair nextAvailableCell(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const; + // The next cell that is has something in it given the direction + QPair nextTakenCell(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const; + // How many cells are available in the row starting from the given cell and direction int freeSpaceInDirection(const QPair &cell, AppletsLayout::PreferredLayoutDirection direction) const; /** * This reacts to changes in size hints by an item */ void adjustToItemSizeHints(ItemContainer *item); // What is the item that occupies the point. The point is expressed in cells rather than pixels. a qpair rather a QPointF as QHash doesn't support indicization by QPointF QHash , ItemContainer *> m_grid; QHash > > m_pointsForItem; QHash m_parsedConfig; };