diff options
Diffstat (limited to 'src/quicklayouts/qquicklayout.cpp')
-rw-r--r-- | src/quicklayouts/qquicklayout.cpp | 1392 |
1 files changed, 1392 insertions, 0 deletions
diff --git a/src/quicklayouts/qquicklayout.cpp b/src/quicklayouts/qquicklayout.cpp new file mode 100644 index 0000000000..97da0a61c9 --- /dev/null +++ b/src/quicklayouts/qquicklayout.cpp @@ -0,0 +1,1392 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qquicklayout_p.h" +#include <QEvent> +#include <QtCore/qcoreapplication.h> +#include <QtCore/private/qnumeric_p.h> +#include <QtCore/qstack.h> +#include <QtCore/qmath.h> +#include <QtQml/qqmlinfo.h> +#include <limits> + +/*! + \qmltype Layout + //! \instantiates QQuickLayoutAttached + \inqmlmodule QtQuick.Layouts + \ingroup layouts + \brief Provides attached properties for items pushed onto a \l GridLayout, + \l RowLayout or \l ColumnLayout. + + An object of type Layout is attached to children of the layout to provide layout specific + information about the item. + The properties of the attached object influence how the layout will arrange the items. + + For instance, you can specify \l minimumWidth, \l preferredWidth, and + \l maximumWidth if the default values are not satisfactory. + + When a layout is resized, items may grow or shrink. Due to this, items have a + \l{Layout::minimumWidth}{minimum size}, \l{Layout::preferredWidth}{preferred size} and a + \l{Layout::maximumWidth}{maximum size}. + + If minimum size has not been explicitly specified on an item, the size is set to \c 0. + If maximum size has not been explicitly specified on an item, the size is set to + \c Number.POSITIVE_INFINITY. + + For layouts, the implicit minimum and maximum sizes depend on the content of the layouts. + + The \l fillWidth and \l fillHeight properties can either be \c true or \c false. If they are \c + false, the item's size will be fixed to its preferred size. Otherwise, it will grow or shrink + between its minimum and maximum size as the layout is resized. If there are multiple items + with \l fillWidth (or \l fillHeight) set to \c true, the layout will grow or shrink the items + relative to the ratio of their preferred size. + + For more details on the layout algorithm, see also the \l {Qt Quick Layouts Overview}. + + \note Do not bind to the x, y, width, or height properties of items in a layout, + as this would conflict with the goals of Layout, and can also cause binding loops. + The width and height properties are used by the layout engine to store the current + size of items as calculated from the minimum/preferred/maximum attached properties, + and can be ovewritten each time the items are laid out. Use + \l {Layout::preferredWidth}{Layout.preferredWidth} and + \l {Layout::preferredHeight}{Layout.preferredHeight}, or \l {Item::}{implicitWidth} + and \l {Item::}{implicitHeight} to specify the preferred size of items. + + \sa GridLayout + \sa RowLayout + \sa ColumnLayout +*/ + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcQuickLayouts, "qt.quick.layouts") + +QQuickLayoutAttached::QQuickLayoutAttached(QObject *parent) + : QObject(parent), + m_minimumWidth(0), + m_minimumHeight(0), + m_preferredWidth(-1), + m_preferredHeight(-1), + m_maximumWidth(std::numeric_limits<qreal>::infinity()), + m_maximumHeight(std::numeric_limits<qreal>::infinity()), + m_defaultMargins(0), + m_fallbackWidth(-1), + m_fallbackHeight(-1), + m_row(-1), + m_column(-1), + m_rowSpan(1), + m_columnSpan(1), + m_fillWidth(false), + m_fillHeight(false), + m_isFillWidthSet(false), + m_isFillHeightSet(false), + m_isUseDefaultSizePolicySet(false), + m_useDefaultSizePolicy(QQuickLayout::SizePolicyExplicit), + m_isMinimumWidthSet(false), + m_isMinimumHeightSet(false), + m_isMaximumWidthSet(false), + m_isMaximumHeightSet(false), + m_changesNotificationEnabled(true), + m_isMarginsSet(false), + m_isLeftMarginSet(false), + m_isTopMarginSet(false), + m_isRightMarginSet(false), + m_isBottomMarginSet(false), + m_isAlignmentSet(false), + m_horizontalStretch(-1), + m_verticalStretch(-1) +{ + +} + +/*! + \qmlattachedproperty real Layout::minimumWidth + + This property holds the minimum width of an item in a layout. + The default value is the item's implicit minimum width. + + If the item is a layout, the implicit minimum width will be the minimum width the layout can + have without any of its items shrinking below their minimum width. + The implicit minimum width for any other item is \c 0. + + Setting this value to -1 will reset the width back to its implicit minimum width. + + + \sa preferredWidth + \sa maximumWidth +*/ +void QQuickLayoutAttached::setMinimumWidth(qreal width) +{ + if (qt_is_nan(width)) + return; + m_isMinimumWidthSet = width >= 0; + if (m_minimumWidth == width) + return; + + m_minimumWidth = width; + invalidateItem(); + emit minimumWidthChanged(); +} + +/*! + \qmlattachedproperty real Layout::minimumHeight + + This property holds the minimum height of an item in a layout. + The default value is the item's implicit minimum height. + + If the item is a layout, the implicit minimum height will be the minimum height the layout can + have without any of its items shrinking below their minimum height. + The implicit minimum height for any other item is \c 0. + + Setting this value to -1 will reset the height back to its implicit minimum height. + + \sa preferredHeight + \sa maximumHeight +*/ +void QQuickLayoutAttached::setMinimumHeight(qreal height) +{ + if (qt_is_nan(height)) + return; + m_isMinimumHeightSet = height >= 0; + if (m_minimumHeight == height) + return; + + m_minimumHeight = height; + invalidateItem(); + emit minimumHeightChanged(); +} + +/*! + \qmlattachedproperty real Layout::preferredWidth + + This property holds the preferred width of an item in a layout. + If the preferred width is \c -1 it will be ignored, and the layout + will use \l{Item::implicitWidth}{implicitWidth} instead. + The default is \c -1. + + \sa minimumWidth + \sa maximumWidth +*/ +void QQuickLayoutAttached::setPreferredWidth(qreal width) +{ + if (qt_is_nan(width) || m_preferredWidth == width) + return; + + m_preferredWidth = width; + invalidateItem(); + emit preferredWidthChanged(); +} + +/*! + \qmlattachedproperty real Layout::preferredHeight + + This property holds the preferred height of an item in a layout. + If the preferred height is \c -1 it will be ignored, and the layout + will use \l{Item::implicitHeight}{implicitHeight} instead. + The default is \c -1. + + \sa minimumHeight + \sa maximumHeight +*/ +void QQuickLayoutAttached::setPreferredHeight(qreal height) +{ + if (qt_is_nan(height) || m_preferredHeight == height) + return; + + m_preferredHeight = height; + invalidateItem(); + emit preferredHeightChanged(); +} + +/*! + \qmlattachedproperty real Layout::maximumWidth + + This property holds the maximum width of an item in a layout. + The default value is the item's implicit maximum width. + + If the item is a layout, the implicit maximum width will be the maximum width the layout can + have without any of its items growing beyond their maximum width. + The implicit maximum width for any other item is \c Number.POSITIVE_INFINITY. + + Setting this value to \c -1 will reset the width back to its implicit maximum width. + + \sa minimumWidth + \sa preferredWidth +*/ +void QQuickLayoutAttached::setMaximumWidth(qreal width) +{ + if (qt_is_nan(width)) + return; + m_isMaximumWidthSet = width >= 0; + if (m_maximumWidth == width) + return; + + m_maximumWidth = width; + invalidateItem(); + emit maximumWidthChanged(); +} + +/*! + \qmlattachedproperty real Layout::maximumHeight + + The default value is the item's implicit maximum height. + + If the item is a layout, the implicit maximum height will be the maximum height the layout can + have without any of its items growing beyond their maximum height. + The implicit maximum height for any other item is \c Number.POSITIVE_INFINITY. + + Setting this value to \c -1 will reset the height back to its implicit maximum height. + + \sa minimumHeight + \sa preferredHeight +*/ +void QQuickLayoutAttached::setMaximumHeight(qreal height) +{ + if (qt_is_nan(height)) + return; + m_isMaximumHeightSet = height >= 0; + if (m_maximumHeight == height) + return; + + m_maximumHeight = height; + invalidateItem(); + emit maximumHeightChanged(); +} + +void QQuickLayoutAttached::setMinimumImplicitSize(const QSizeF &sz) +{ + bool emitWidthChanged = false; + bool emitHeightChanged = false; + if (!m_isMinimumWidthSet && m_minimumWidth != sz.width()) { + m_minimumWidth = sz.width(); + emitWidthChanged = true; + } + if (!m_isMinimumHeightSet && m_minimumHeight != sz.height()) { + m_minimumHeight = sz.height(); + emitHeightChanged = true; + } + // Only invalidate the item once, and make sure we emit signal changed after the call to + // invalidateItem() + if (emitWidthChanged || emitHeightChanged) { + invalidateItem(); + if (emitWidthChanged) + emit minimumWidthChanged(); + if (emitHeightChanged) + emit minimumHeightChanged(); + } +} + +void QQuickLayoutAttached::setMaximumImplicitSize(const QSizeF &sz) +{ + bool emitWidthChanged = false; + bool emitHeightChanged = false; + if (!m_isMaximumWidthSet && m_maximumWidth != sz.width()) { + m_maximumWidth = sz.width(); + emitWidthChanged = true; + } + if (!m_isMaximumHeightSet && m_maximumHeight != sz.height()) { + m_maximumHeight = sz.height(); + emitHeightChanged = true; + } + // Only invalidate the item once, and make sure we emit changed signal after the call to + // invalidateItem() + if (emitWidthChanged || emitHeightChanged) { + invalidateItem(); + if (emitWidthChanged) + emit maximumWidthChanged(); + if (emitHeightChanged) + emit maximumHeightChanged(); + } +} + +/*! + \qmlattachedproperty bool Layout::fillWidth + + If this property is \c true, the item will be as wide as possible while respecting + the given constraints. If the property is \c false, the item will have a fixed width + set to the preferred width. + The default depends on implicit (built-in) size policy of item. + + \sa fillHeight +*/ +void QQuickLayoutAttached::setFillWidth(bool fill) +{ + bool oldFillWidth = fillWidth(); + m_isFillWidthSet = true; + m_fillWidth = fill; + if (oldFillWidth != fill) { + invalidateItem(); + emit fillWidthChanged(); + } +} + +/*! + \qmlattachedproperty bool Layout::fillHeight + + If this property is \c true, the item will be as tall as possible while respecting + the given constraints. If the property is \c false, the item will have a fixed height + set to the preferred height. + The default depends on implicit (built-in) size policy of the item. + + \sa fillWidth +*/ +void QQuickLayoutAttached::setFillHeight(bool fill) +{ + bool oldFillHeight = fillHeight(); + m_isFillHeightSet = true; + m_fillHeight = fill; + if (oldFillHeight != fill) { + invalidateItem(); + emit fillHeightChanged(); + } +} + +/*! + \qmlattachedproperty enumeration Layout::useDefaultSizePolicy + \since 6.8 + + This property allows the user to configure the layout size policy at the component + level. + + The default value will be inherited by querying the application attribute + \l Qt::AA_QtQuickUseDefaultSizePolicy. You can use this property to override that value. + + \value Layout.SizePolicyImplicit + The item in the layout uses implicit or built-in size policy + \value Layout.SizePolicyExplicit + The item in the layout don't use implicit size policies. +*/ +void QQuickLayoutAttached::setUseDefaultSizePolicy(QQuickLayout::SizePolicy sizePolicy) +{ + m_isUseDefaultSizePolicySet = true; + if (m_useDefaultSizePolicy != sizePolicy) { + m_useDefaultSizePolicy = sizePolicy; + emit useDefaultSizePolicyChanged(); + } +} + +/*! + \qmlattachedproperty int Layout::row + + This property allows you to specify the row position of an item in a \l GridLayout. + + If both \l column and this property are not set, it is up to the layout to assign a cell to the item. + + The default value is \c 0. + + \sa column + \sa rowSpan +*/ +void QQuickLayoutAttached::setRow(int row) +{ + if (row >= 0 && row != m_row) { + m_row = row; + invalidateItem(); + emit rowChanged(); + } +} + +/*! + \qmlattachedproperty int Layout::column + + This property allows you to specify the column position of an item in a \l GridLayout. + + If both \l row and this property are not set, it is up to the layout to assign a cell to the item. + + The default value is \c 0. + + \sa row + \sa columnSpan +*/ +void QQuickLayoutAttached::setColumn(int column) +{ + if (column >= 0 && column != m_column) { + m_column = column; + invalidateItem(); + emit columnChanged(); + } +} + + +/*! + \qmlattachedproperty Qt.Alignment Layout::alignment + + This property allows you to specify the alignment of an item within the cell(s) it occupies. + + The default value is \c 0, which means it will be \c{Qt.AlignVCenter | Qt.AlignLeft}. + These defaults also apply if only a horizontal or vertical flag is specified: + if only a horizontal flag is specified, the default vertical flag will be + \c Qt.AlignVCenter, and if only a vertical flag is specified, the default + horizontal flag will be \c Qt.AlignLeft. + + A valid alignment is a combination of the following flags: + \list + \li Qt::AlignLeft + \li Qt::AlignHCenter + \li Qt::AlignRight + \li Qt::AlignTop + \li Qt::AlignVCenter + \li Qt::AlignBottom + \li Qt::AlignBaseline + \endlist + +*/ +void QQuickLayoutAttached::setAlignment(Qt::Alignment align) +{ + m_isAlignmentSet = true; + if (align != m_alignment) { + m_alignment = align; + if (QQuickLayout *layout = parentLayout()) { + layout->setAlignment(item(), align); + invalidateItem(); + } + emit alignmentChanged(); + } +} + +/*! + \qmlattachedproperty int Layout::horizontalStretchFactor + + This property allows you to specify the horizontal stretch factor. By default, two identical + items arranged in a linear layout will have the same size, but if the first item has a + stretch factor of 1 and the second item has a stretch factor of 2, the first item will \e + aim to get 1/3 of the available space, and the second will \e aim to get 2/3 of the available + space. Note that, whether they become exactly 1/3 and 2/3 of the available space depends on + their size hints. This is because when e.g a horizontal layout is shown in its minimum width + all its child items will consequently also have their minimum width. + + Likewise, when a horizontal layout has its preferred width, all child items will have their + preferred widths, and when a horizontal layout has its maximum width, all child items will have + their maximum widths. This strategy is applied regardless of what the individual stretch + factors are. As a consequence of this, stretch factors will only determine the growth rate of + child items \e between the preferredWidth and maximumWidth range. + + The default value is \c -1, which means that no stretch factor is applied. + + \note This requires that Layout::fillWidth is set to true + + \since Qt 6.5 + + \sa verticalStretchFactor +*/ +void QQuickLayoutAttached::setHorizontalStretchFactor(int factor) +{ + if (factor != m_horizontalStretch) { + m_horizontalStretch = factor; + if (QQuickLayout *layout = parentLayout()) { + layout->setStretchFactor(item(), factor, Qt::Horizontal); + invalidateItem(); + } + emit horizontalStretchFactorChanged(); + } +} + +/*! + \qmlattachedproperty int Layout::verticalStretchFactor + + This property allows you to specify the vertical stretch factor. By default, two identical + items arranged in a linear layout will have the same size, but if the first item has a + stretch factor of 1 and the second item has a stretch factor of 2, the first item will \e + aim to get 1/3 of the available space, and the second will \e aim to get 2/3 of the available + space. Note that, whether they become exactly 1/3 and 2/3 of the available space depends on + their size hints. This is because when e.g a vertical layout is shown in its minimum height + all its child items will consequently also have their minimum height. + + Likewise, when a vertical layout has its preferred height, all child items will have their + preferred heights, and when a vertical layout has its maximum height, all child items will have + their maximum heights. This strategy is applied regardless of what the individual stretch + factors are. As a consequence of this, stretch factors will only determine the growth rate of + child items \e between the preferredHeight and maximumHeight range. + + The default value is \c -1, which means that no stretch factor is applied. + + \note This requires that Layout::fillHeight is set to true + + \since Qt 6.5 + + \sa horizontalStretchFactor +*/ +void QQuickLayoutAttached::setVerticalStretchFactor(int factor) +{ + if (factor != m_verticalStretch) { + m_verticalStretch = factor; + if (QQuickLayout *layout = parentLayout()) { + layout->setStretchFactor(item(), factor, Qt::Vertical); + invalidateItem(); + } + emit verticalStretchFactorChanged(); + } +} + +/*! + \qmlattachedproperty real Layout::margins + + Sets the margins outside of an item to all have the same value. The item + itself does not evaluate its own margins. It is the parent's responsibility + to decide if it wants to evaluate the margins. + + Specifically, margins are only evaluated by ColumnLayout, RowLayout, + GridLayout, and other layout-like containers, such as SplitView, where the + effective cell size of an item will be increased as the margins are + increased. + + Therefore, if an item with margins is a child of another \c Item, its + position, size and implicit size will remain unchanged. + + Combining margins with alignment will align the item \e including its + margins. For instance, a vertically-centered Item with a top margin of \c 1 + and a bottom margin of \c 9 will cause the Items effective alignment within + the cell to be 4 pixels above the center. + + The default value is \c 0. + + \sa leftMargin + \sa topMargin + \sa rightMargin + \sa bottomMargin + + \since QtQuick.Layouts 1.2 +*/ +void QQuickLayoutAttached::setMargins(qreal m) +{ + m_isMarginsSet = true; + if (m == m_defaultMargins) + return; + + m_defaultMargins = m; + invalidateItem(); + if (!m_isLeftMarginSet && m_margins.left() != m) + emit leftMarginChanged(); + if (!m_isTopMarginSet && m_margins.top() != m) + emit topMarginChanged(); + if (!m_isRightMarginSet && m_margins.right() != m) + emit rightMarginChanged(); + if (!m_isBottomMarginSet && m_margins.bottom() != m) + emit bottomMarginChanged(); + emit marginsChanged(); +} + +/*! + \qmlattachedproperty real Layout::leftMargin + + Specifies the left margin outside of an item. + If the value is not set, it will use the value from \l margins. + + \sa margins + + \since QtQuick.Layouts 1.2 +*/ +void QQuickLayoutAttached::setLeftMargin(qreal m) +{ + const bool changed = leftMargin() != m; + m_margins.setLeft(m); + m_isLeftMarginSet = true; + if (changed) { + invalidateItem(); + emit leftMarginChanged(); + } +} + +void QQuickLayoutAttached::resetLeftMargin() +{ + const bool changed = m_isLeftMarginSet && (m_defaultMargins != m_margins.left()); + m_isLeftMarginSet = false; + if (changed) { + invalidateItem(); + emit leftMarginChanged(); + } +} + +/*! + \qmlattachedproperty real Layout::topMargin + + Specifies the top margin outside of an item. + If the value is not set, it will use the value from \l margins. + + \sa margins + + \since QtQuick.Layouts 1.2 +*/ +void QQuickLayoutAttached::setTopMargin(qreal m) +{ + const bool changed = topMargin() != m; + m_margins.setTop(m); + m_isTopMarginSet = true; + if (changed) { + invalidateItem(); + emit topMarginChanged(); + } +} + +void QQuickLayoutAttached::resetTopMargin() +{ + const bool changed = m_isTopMarginSet && (m_defaultMargins != m_margins.top()); + m_isTopMarginSet = false; + if (changed) { + invalidateItem(); + emit topMarginChanged(); + } +} + +/*! + \qmlattachedproperty real Layout::rightMargin + + Specifies the right margin outside of an item. + If the value is not set, it will use the value from \l margins. + + \sa margins + + \since QtQuick.Layouts 1.2 +*/ +void QQuickLayoutAttached::setRightMargin(qreal m) +{ + const bool changed = rightMargin() != m; + m_margins.setRight(m); + m_isRightMarginSet = true; + if (changed) { + invalidateItem(); + emit rightMarginChanged(); + } +} + +void QQuickLayoutAttached::resetRightMargin() +{ + const bool changed = m_isRightMarginSet && (m_defaultMargins != m_margins.right()); + m_isRightMarginSet = false; + if (changed) { + invalidateItem(); + emit rightMarginChanged(); + } +} + +/*! + \qmlattachedproperty real Layout::bottomMargin + + Specifies the bottom margin outside of an item. + If the value is not set, it will use the value from \l margins. + + \sa margins + + \since QtQuick.Layouts 1.2 +*/ +void QQuickLayoutAttached::setBottomMargin(qreal m) +{ + const bool changed = bottomMargin() != m; + m_margins.setBottom(m); + m_isBottomMarginSet = true; + if (changed) { + invalidateItem(); + emit bottomMarginChanged(); + } +} + +void QQuickLayoutAttached::resetBottomMargin() +{ + const bool changed = m_isBottomMarginSet && (m_defaultMargins != m_margins.bottom()); + m_isBottomMarginSet = false; + if (changed) { + invalidateItem(); + emit bottomMarginChanged(); + } +} + + +/*! + \qmlattachedproperty int Layout::rowSpan + + This property allows you to specify the row span of an item in a \l GridLayout. + + The default value is \c 1. + + \sa columnSpan + \sa row +*/ +void QQuickLayoutAttached::setRowSpan(int span) +{ + if (span != m_rowSpan) { + m_rowSpan = span; + invalidateItem(); + emit rowSpanChanged(); + } +} + + +/*! + \qmlattachedproperty int Layout::columnSpan + + This property allows you to specify the column span of an item in a \l GridLayout. + + The default value is \c 1. + + \sa rowSpan + \sa column +*/ +void QQuickLayoutAttached::setColumnSpan(int span) +{ + if (span != m_columnSpan) { + m_columnSpan = span; + invalidateItem(); + emit columnSpanChanged(); + } +} + + +qreal QQuickLayoutAttached::sizeHint(Qt::SizeHint which, Qt::Orientation orientation) const +{ + qreal result = 0; + if (QQuickLayout *layout = qobject_cast<QQuickLayout *>(item())) { + const QSizeF sz = layout->sizeHint(which); + result = (orientation == Qt::Horizontal ? sz.width() : sz.height()); + } else { + if (which == Qt::MaximumSize) + result = std::numeric_limits<qreal>::infinity(); + } + return result; +} + +void QQuickLayoutAttached::invalidateItem() +{ + qCDebug(lcQuickLayouts) << "QQuickLayoutAttached::invalidateItem"; + if (QQuickLayout *layout = parentLayout()) { + layout->invalidate(item()); + } +} + +QQuickLayout *QQuickLayoutAttached::parentLayout() const +{ + QQuickItem *parentItem = item(); + if (parentItem) { + parentItem = parentItem->parentItem(); + return qobject_cast<QQuickLayout *>(parentItem); + } else { + qmlWarning(parent()) << "Layout must be attached to Item elements"; + } + return nullptr; +} + +QQuickItem *QQuickLayoutAttached::item() const +{ + return qobject_cast<QQuickItem *>(parent()); +} + +void QQuickLayoutPrivate::applySizeHints() const +{ + Q_Q(const QQuickLayout); + + QQuickLayout *that = const_cast<QQuickLayout*>(q); + QQuickLayoutAttached *info = attachedLayoutObject(that, true); + + const QSizeF min = q->sizeHint(Qt::MinimumSize); + const QSizeF max = q->sizeHint(Qt::MaximumSize); + const QSizeF pref = q->sizeHint(Qt::PreferredSize); + info->setMinimumImplicitSize(min); + info->setMaximumImplicitSize(max); + that->setImplicitSize(pref.width(), pref.height()); +} + +QQuickLayout::QQuickLayout(QQuickLayoutPrivate &dd, QQuickItem *parent) + : QQuickItem(dd, parent) + , m_inUpdatePolish(false) + , m_polishInsideUpdatePolish(0) +{ +} + +static QQuickItemPrivate::ChangeTypes changeTypes = + QQuickItemPrivate::SiblingOrder + | QQuickItemPrivate::ImplicitWidth + | QQuickItemPrivate::ImplicitHeight + | QQuickItemPrivate::Destroyed + | QQuickItemPrivate::Visibility; + +QQuickLayout::~QQuickLayout() +{ + d_func()->m_isReady = false; + + const auto childItems = d_func()->childItems; + for (QQuickItem *child : childItems) + QQuickItemPrivate::get(child)->removeItemChangeListener(this, changeTypes); +} + +QQuickLayoutAttached *QQuickLayout::qmlAttachedProperties(QObject *object) +{ + return new QQuickLayoutAttached(object); +} + +void QQuickLayout::updatePolish() +{ + qCDebug(lcQuickLayouts) << "updatePolish() ENTERING" << this; + m_inUpdatePolish = true; + + // Might have become "undirty" before we reach this updatePolish() + // (e.g. if somebody queried for implicitWidth it will immediately + // calculate size hints) + // Note that we need to call ensureLayoutItemsUpdated() *before* we query width() and height(), + // because width()/height() might return their implicitWidth/implicitHeight (e.g. for a layout + // with no explicitly specified size, (nor anchors.fill: parent)) + ensureLayoutItemsUpdated(QQuickLayout::ApplySizeHints | QQuickLayout::Recursive); + rearrange(QSizeF(width(), height())); + m_inUpdatePolish = false; + qCDebug(lcQuickLayouts) << "updatePolish() LEAVING" << this; +} + +void QQuickLayout::componentComplete() +{ + Q_D(QQuickLayout); + d->m_disableRearrange = true; + QQuickItem::componentComplete(); // will call our geometryChange(), (where isComponentComplete() == true) + d->m_disableRearrange = false; + d->m_isReady = true; +} + +void QQuickLayout::maybeSubscribeToBaseLineOffsetChanges(QQuickItem *item) +{ + QQuickLayoutAttached *info = attachedLayoutObject(item, false); + if (info) { + if (info->alignment() == Qt::AlignBaseline && static_cast<QQuickLayout*>(item->parentItem()) == this) { + qmlobject_connect(item, QQuickItem, SIGNAL(baselineOffsetChanged(qreal)), this, QQuickLayout, SLOT(invalidateSenderItem())); + } else { + qmlobject_disconnect(item, QQuickItem, SIGNAL(baselineOffsetChanged(qreal)), this, QQuickLayout, SLOT(invalidateSenderItem())); + } + } +} + +void QQuickLayout::invalidate(QQuickItem * /*childItem*/) +{ + Q_D(QQuickLayout); + if (invalidated()) + return; + + qCDebug(lcQuickLayouts) << "QQuickLayout::invalidate()" << this; + d->m_dirty = true; + d->m_dirtyArrangement = true; + + if (!qobject_cast<QQuickLayout *>(parentItem())) { + polish(); + + if (m_inUpdatePolish) { + if (++m_polishInsideUpdatePolish > 2) + // allow at most two consecutive loops in order to respond to height-for-width + // (e.g QQuickText changes implicitHeight when its width gets changed) + qCDebug(lcQuickLayouts) << "Layout polish loop detected for " << this + << ". The polish request will still be scheduled."; + } else { + m_polishInsideUpdatePolish = 0; + } + } +} + +bool QQuickLayout::shouldIgnoreItem(QQuickItem *child) const +{ + QQuickItemPrivate *childPrivate = QQuickItemPrivate::get(child); + bool ignoreItem = !childPrivate->explicitVisible; + if (!ignoreItem && childPrivate->isTransparentForPositioner()) + ignoreItem = true; + return ignoreItem; +} + +void QQuickLayout::checkAnchors(QQuickItem *item) const +{ + QQuickAnchors *anchors = QQuickItemPrivate::get(item)->_anchors; + if (anchors && anchors->activeDirections()) + qmlWarning(item) << "Detected anchors on an item that is managed by a layout. This is undefined behavior; use Layout.alignment instead."; +} + +void QQuickLayout::ensureLayoutItemsUpdated(EnsureLayoutItemsUpdatedOptions options) const +{ + Q_D(const QQuickLayout); + if (!invalidated()) + return; + qCDebug(lcQuickLayouts) << "ENTER QQuickLayout::ensureLayoutItemsUpdated()" << this << options; + QQuickLayoutPrivate *priv = const_cast<QQuickLayoutPrivate*>(d); + + // breadth-first + // must update the root first, and continue towards the leaf nodes. + // Otherwise, we wouldn't know which children to traverse to + const_cast<QQuickLayout*>(this)->updateLayoutItems(); + + // make invalidate() return true + d->m_dirty = false; + + if (options & Recursive) { + for (int i = 0; i < itemCount(); ++i) { + QQuickItem *itm = itemAt(i); + if (QQuickLayout *lay = qobject_cast<QQuickLayout*>(itm)) { + lay->ensureLayoutItemsUpdated(options); + } + } + } + + // size hints are updated depth-first (parent size hints depends on their childrens size hints) + if (options & ApplySizeHints) + priv->applySizeHints(); + qCDebug(lcQuickLayouts) << "LEAVE QQuickLayout::ensureLayoutItemsUpdated()" << this; +} + + +void QQuickLayout::itemChange(ItemChange change, const ItemChangeData &value) +{ + if (change == ItemChildAddedChange) { + Q_D(QQuickLayout); + QQuickItem *item = value.item; + maybeSubscribeToBaseLineOffsetChanges(item); + QQuickItemPrivate::get(item)->addItemChangeListener(this, changeTypes); + d->m_hasItemChangeListeners = true; + qCDebug(lcQuickLayouts) << "ChildAdded" << item; + if (isReady()) + invalidate(); + } else if (change == ItemChildRemovedChange) { + QQuickItem *item = value.item; + maybeSubscribeToBaseLineOffsetChanges(item); + QQuickItemPrivate::get(item)->removeItemChangeListener(this, changeTypes); + qCDebug(lcQuickLayouts) << "ChildRemoved" << item; + if (isReady()) + invalidate(); + } + QQuickItem::itemChange(change, value); +} + +void QQuickLayout::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + Q_D(QQuickLayout); + QQuickItem::geometryChange(newGeometry, oldGeometry); + if ((invalidated() && !qobject_cast<QQuickLayout *>(parentItem())) || + d->m_disableRearrange || !isReady()) + return; + + qCDebug(lcQuickLayouts) << "QQuickLayout::geometryChange" << newGeometry << oldGeometry; + rearrange(newGeometry.size()); +} + +void QQuickLayout::invalidateSenderItem() +{ + if (!isReady()) + return; + QQuickItem *item = static_cast<QQuickItem *>(sender()); + Q_ASSERT(item); + invalidate(item); +} + +bool QQuickLayout::isReady() const +{ + return d_func()->m_isReady; +} + +/*! + * \brief QQuickLayout::deactivateRecur + * \internal + * + * Call this from the dtor of the top-level layout. + * Otherwise, it will trigger lots of unneeded item change listeners (itemVisibleChanged()) for all its descendants + * that will have its impact thrown away. + */ +void QQuickLayout::deactivateRecur() +{ + if (d_func()->m_hasItemChangeListeners) { + ensureLayoutItemsUpdated(); + for (int i = 0; i < itemCount(); ++i) { + QQuickItem *item = itemAt(i); + // When deleting a layout with children, there is no reason for the children to inform the layout that their + // e.g. visibility got changed. The layout already knows that all its children will eventually become invisible, so + // we therefore remove its change listener. + QQuickItemPrivate::get(item)->removeItemChangeListener(this, changeTypes); + if (QQuickLayout *layout = qobject_cast<QQuickLayout*>(item)) + layout->deactivateRecur(); + } + d_func()->m_hasItemChangeListeners = false; + } +} + +bool QQuickLayout::invalidated() const +{ + return d_func()->m_dirty; +} + +bool QQuickLayout::invalidatedArrangement() const +{ + return d_func()->m_dirtyArrangement; +} + +bool QQuickLayout::isMirrored() const +{ + return d_func()->isMirrored(); +} + +void QQuickLayout::itemSiblingOrderChanged(QQuickItem *item) +{ + Q_UNUSED(item); + invalidate(); +} + +void QQuickLayout::itemImplicitWidthChanged(QQuickItem *item) +{ + if (!isReady() || item->signalsBlocked()) + return; + invalidate(item); +} + +void QQuickLayout::itemImplicitHeightChanged(QQuickItem *item) +{ + if (!isReady() || item->signalsBlocked()) + return; + invalidate(item); +} + +void QQuickLayout::itemDestroyed(QQuickItem *item) +{ + Q_UNUSED(item); +} + +void QQuickLayout::itemVisibilityChanged(QQuickItem *item) +{ + Q_UNUSED(item); +} + +void QQuickLayout::rearrange(const QSizeF &/*size*/) +{ + d_func()->m_dirtyArrangement = false; +} + + +/* + The layout engine assumes: + 1. minimum <= preferred <= maximum + 2. descent is within minimum and maximum bounds (### verify) + + This function helps to ensure that by the following rules (in the following order): + 1. If minimum > maximum, set minimum = maximum + 2. Clamp preferred to be between the [minimum,maximum] range. + 3. If descent > minimum, set descent = minimum (### verify if this is correct, it might + need some refinements to multiline texts) + + If any values are "not set" (i.e. negative), they will be left untouched, so that we + know which values needs to be fetched from the implicit hints (not user hints). + */ +static void normalizeHints(qreal &minimum, qreal &preferred, qreal &maximum, qreal &descent) +{ + if (minimum >= 0 && maximum >= 0 && minimum > maximum) + minimum = maximum; + + if (preferred >= 0) { + if (minimum >= 0 && preferred < minimum) { + preferred = minimum; + } else if (maximum >= 0 && preferred > maximum) { + preferred = maximum; + } + } + + if (minimum >= 0 && descent > minimum) + descent = minimum; +} + +static void boundSize(QSizeF &result, const QSizeF &size) +{ + if (size.width() >= 0 && size.width() < result.width()) + result.setWidth(size.width()); + if (size.height() >= 0 && size.height() < result.height()) + result.setHeight(size.height()); +} + +static void expandSize(QSizeF &result, const QSizeF &size) +{ + if (size.width() >= 0 && size.width() > result.width()) + result.setWidth(size.width()); + if (size.height() >= 0 && size.height() > result.height()) + result.setHeight(size.height()); +} + +static inline void combineHints(qreal ¤t, qreal fallbackHint) +{ + if (current < 0) + current = fallbackHint; +} + +static inline void combineSize(QSizeF &result, const QSizeF &fallbackSize) +{ + combineHints(result.rwidth(), fallbackSize.width()); + combineHints(result.rheight(), fallbackSize.height()); +} + +static inline void combineImplicitHints(QQuickLayoutAttached *info, Qt::SizeHint which, QSizeF *size) +{ + if (!info) return; + + Q_ASSERT(which == Qt::MinimumSize || which == Qt::MaximumSize); + + const QSizeF constraint(which == Qt::MinimumSize + ? QSizeF(info->minimumWidth(), info->minimumHeight()) + : QSizeF(info->maximumWidth(), info->maximumHeight())); + + if (!info->isExtentExplicitlySet(Qt::Horizontal, which)) + combineHints(size->rwidth(), constraint.width()); + if (!info->isExtentExplicitlySet(Qt::Vertical, which)) + combineHints(size->rheight(), constraint.height()); +} + +typedef qreal (QQuickLayoutAttached::*SizeGetter)() const; + +/*! + \internal + Note: Can potentially return the attached QQuickLayoutAttached object through \a attachedInfo. + + It is like this is because it enables it to be reused. + + The goal of this function is to return the effective minimum, preferred and maximum size hints + that the layout will use for this item. + This function takes care of gathering all explicitly set size hints, normalizes them so + that min < pref < max. + Further, the hints _not_explicitly_ set will then be initialized with the implicit size hints, + which is usually derived from the content of the layouts (or items). + + The following table illustrates the preference of the properties used for measuring layout + items. If present, the USER properties will be preferred. If USER properties are not present, + the HINT properties will be preferred. Finally, the FALLBACK properties will be used as an + ultimate fallback. + + Note that one can query if the value of Layout.minimumWidth or Layout.maximumWidth has been + explicitly or implicitly set with QQuickLayoutAttached::isExtentExplicitlySet(). This + determines if it should be used as a USER or as a HINT value. + + Fractional size hints will be ceiled to the closest integer. This is in order to give some + slack when the items are snapped to the pixel grid. + + | *Minimum* | *Preferred* | *Maximum* | ++----------------+----------------------+-----------------------+--------------------------+ +|USER (explicit) | Layout.minimumWidth | Layout.preferredWidth | Layout.maximumWidth | +|HINT (implicit) | Layout.minimumWidth | implicitWidth | Layout.maximumWidth | +|FALLBACK | 0 | width | Number.POSITIVE_INFINITY | ++----------------+----------------------+-----------------------+--------------------------+ + */ +void QQuickLayout::effectiveSizeHints_helper(QQuickItem *item, QSizeF *cachedSizeHints, QQuickLayoutAttached **attachedInfo, bool useFallbackToWidthOrHeight) +{ + for (int i = 0; i < Qt::NSizeHints; ++i) + cachedSizeHints[i] = QSizeF(); + QQuickLayoutAttached *info = attachedLayoutObject(item, false); + // First, retrieve the user-specified hints from the attached "Layout." properties + if (info) { + struct Getters { + SizeGetter call[NSizes]; + }; + + static Getters horGetters = { + {&QQuickLayoutAttached::minimumWidth, &QQuickLayoutAttached::preferredWidth, &QQuickLayoutAttached::maximumWidth}, + }; + + static Getters verGetters = { + {&QQuickLayoutAttached::minimumHeight, &QQuickLayoutAttached::preferredHeight, &QQuickLayoutAttached::maximumHeight} + }; + for (int i = 0; i < NSizes; ++i) { + SizeGetter getter = horGetters.call[i]; + Q_ASSERT(getter); + + if (info->isExtentExplicitlySet(Qt::Horizontal, (Qt::SizeHint)i)) + cachedSizeHints[i].setWidth((info->*getter)()); + + getter = verGetters.call[i]; + Q_ASSERT(getter); + if (info->isExtentExplicitlySet(Qt::Vertical, (Qt::SizeHint)i)) + cachedSizeHints[i].setHeight((info->*getter)()); + } + } + + QSizeF &minS = cachedSizeHints[Qt::MinimumSize]; + QSizeF &prefS = cachedSizeHints[Qt::PreferredSize]; + QSizeF &maxS = cachedSizeHints[Qt::MaximumSize]; + QSizeF &descentS = cachedSizeHints[Qt::MinimumDescent]; + + // For instance, will normalize the following user-set hints + // from: [10, 5, 60] + // to: [10, 10, 60] + normalizeHints(minS.rwidth(), prefS.rwidth(), maxS.rwidth(), descentS.rwidth()); + normalizeHints(minS.rheight(), prefS.rheight(), maxS.rheight(), descentS.rheight()); + + // All explicit values gathered, now continue to gather the implicit sizes + + //--- GATHER MAXIMUM SIZE HINTS --- + combineImplicitHints(info, Qt::MaximumSize, &maxS); + combineSize(maxS, QSizeF(std::numeric_limits<qreal>::infinity(), std::numeric_limits<qreal>::infinity())); + // implicit max or min sizes should not limit an explicitly set preferred size + expandSize(maxS, prefS); + expandSize(maxS, minS); + + //--- GATHER MINIMUM SIZE HINTS --- + combineImplicitHints(info, Qt::MinimumSize, &minS); + expandSize(minS, QSizeF(0,0)); + boundSize(minS, prefS); + boundSize(minS, maxS); + + //--- GATHER PREFERRED SIZE HINTS --- + // First, from implicitWidth/Height + qreal &prefWidth = prefS.rwidth(); + qreal &prefHeight = prefS.rheight(); + if (prefWidth < 0 && item->implicitWidth() > 0) + prefWidth = qCeil(item->implicitWidth()); + if (prefHeight < 0 && item->implicitHeight() > 0) + prefHeight = qCeil(item->implicitHeight()); + + // If that fails, make an ultimate fallback to width/height + if (useFallbackToWidthOrHeight && !prefS.isValid()) { + /* If we want to support using width/height as preferred size hints in + layouts, (which we think most people expect), we only want to use the + initial width. + This is because the width will change due to layout rearrangement, + and the preferred width should return the same value, regardless of + the current width. + We therefore store this initial width in the attached layout object + and reuse it if needed rather than querying the width another time. + That means we need to ensure that an Layout attached object is available + by creating one if necessary. + */ + if (!info) + info = attachedLayoutObject(item); + + auto updatePreferredSizes = [](qreal &cachedSize, qreal &attachedSize, qreal size) { + if (cachedSize < 0) { + if (attachedSize < 0) + attachedSize = size; + + cachedSize = attachedSize; + } + }; + updatePreferredSizes(prefWidth, info->m_fallbackWidth, item->width()); + updatePreferredSizes(prefHeight, info->m_fallbackHeight, item->height()); + } + + // Normalize again after the implicit hints have been gathered + expandSize(prefS, minS); + boundSize(prefS, maxS); + + //--- GATHER DESCENT + // Minimum descent is only applicable for the effective minimum height, + // so we gather the descent last. + const qreal minimumDescent = minS.height() - item->baselineOffset(); + descentS.setHeight(minimumDescent); + + if (info) { + QMarginsF margins = info->qMargins(); + QSizeF extraMargins(margins.left() + margins.right(), margins.top() + margins.bottom()); + minS += extraMargins; + prefS += extraMargins; + maxS += extraMargins; + descentS += extraMargins; + } + if (attachedInfo) + *attachedInfo = info; +} + +/*! + \internal + + Assumes \a info is set (if the object has an attached property) + */ +QLayoutPolicy::Policy QQuickLayout::effectiveSizePolicy_helper(QQuickItem *item, Qt::Orientation orientation, QQuickLayoutAttached *info) +{ + QLayoutPolicy::Policy pol{QLayoutPolicy::Fixed}; + bool isSet = false; + if (info) { + if (orientation == Qt::Horizontal) { + isSet = info->isFillWidthSet(); + if (isSet && info->fillWidth()) + pol = QLayoutPolicy::Preferred; + } else { + isSet = info->isFillHeightSet(); + if (isSet && info->fillHeight()) + pol = QLayoutPolicy::Preferred; + } + } + if (!isSet && item) { + auto effectiveUseDefaultSizePolicy = [info]() { + return info ? info->useDefaultSizePolicy() == QQuickLayout::SizePolicyImplicit + : QGuiApplication::testAttribute(Qt::AA_QtQuickUseDefaultSizePolicy); + }; + if (qobject_cast<QQuickLayout*>(item)) { + pol = QLayoutPolicy::Preferred; + } else if (effectiveUseDefaultSizePolicy()) { + QLayoutPolicy sizePolicy = QQuickItemPrivate::get(item)->sizePolicy(); + pol = (orientation == Qt::Horizontal) ? sizePolicy.horizontalPolicy() : sizePolicy.verticalPolicy(); + } + } + + return pol; +} + +void QQuickLayout::_q_dumpLayoutTree() const +{ + QString buf; + dumpLayoutTreeRecursive(0, buf); + qDebug("\n%s", qPrintable(buf)); +} + +void QQuickLayout::dumpLayoutTreeRecursive(int level, QString &buf) const +{ + auto formatLine = [&level](const char *fmt) -> QString { + QString ss(level *4, QLatin1Char(' ')); + return ss + QLatin1String(fmt) + QLatin1Char('\n'); + }; + + auto f2s = [](qreal f) { + return QString::number(f); + }; + auto b2s = [](bool b) { + static const char *strBool[] = {"false", "true"}; + return QLatin1String(strBool[int(b)]); + }; + + buf += formatLine("%1 {").arg(QQmlMetaType::prettyTypeName(this)); + ++level; + buf += formatLine("// Effective calculated values:"); + buf += formatLine("sizeHintDirty: %2").arg(invalidated()); + QSizeF min = sizeHint(Qt::MinimumSize); + buf += formatLine("sizeHint.min : [%1, %2]").arg(f2s(min.width()), 5).arg(min.height(), 5); + QSizeF pref = sizeHint(Qt::PreferredSize); + buf += formatLine("sizeHint.pref: [%1, %2]").arg(pref.width(), 5).arg(pref.height(), 5); + QSizeF max = sizeHint(Qt::MaximumSize); + buf += formatLine("sizeHint.max : [%1, %2]").arg(f2s(max.width()), 5).arg(f2s(max.height()), 5); + + for (QQuickItem *item : childItems()) { + buf += QLatin1Char('\n'); + if (QQuickLayout *childLayout = qobject_cast<QQuickLayout*>(item)) { + childLayout->dumpLayoutTreeRecursive(level, buf); + } else { + buf += formatLine("%1 {").arg(QQmlMetaType::prettyTypeName(item)); + ++level; + if (item->implicitWidth() > 0) + buf += formatLine("implicitWidth: %1").arg(f2s(item->implicitWidth())); + if (item->implicitHeight() > 0) + buf += formatLine("implicitHeight: %1").arg(f2s(item->implicitHeight())); + QSizeF min; + QSizeF pref; + QSizeF max; + QQuickLayoutAttached *info = attachedLayoutObject(item, false); + if (info) { + min = QSizeF(info->minimumWidth(), info->minimumHeight()); + pref = QSizeF(info->preferredWidth(), info->preferredHeight()); + max = QSizeF(info->maximumWidth(), info->maximumHeight()); + if (info->isExtentExplicitlySet(Qt::Horizontal, Qt::MinimumSize)) + buf += formatLine("Layout.minimumWidth: %1").arg(f2s(min.width())); + if (info->isExtentExplicitlySet(Qt::Vertical, Qt::MinimumSize)) + buf += formatLine("Layout.minimumHeight: %1").arg(f2s(min.height())); + if (pref.width() >= 0) + buf += formatLine("Layout.preferredWidth: %1").arg(f2s(pref.width())); + if (pref.height() >= 0) + buf += formatLine("Layout.preferredHeight: %1").arg(f2s(pref.height())); + if (info->isExtentExplicitlySet(Qt::Horizontal, Qt::MaximumSize)) + buf += formatLine("Layout.maximumWidth: %1").arg(f2s(max.width())); + if (info->isExtentExplicitlySet(Qt::Vertical, Qt::MaximumSize)) + buf += formatLine("Layout.maximumHeight: %1").arg(f2s(max.height())); + + if (info->isFillWidthSet()) + buf += formatLine("Layout.fillWidth: %1").arg(b2s(info->fillWidth())); + if (info->isFillHeightSet()) + buf += formatLine("Layout.fillHeight: %1").arg(b2s(info->fillHeight())); + } + --level; + buf += formatLine("}"); + } + } + --level; + buf += formatLine("}"); +} + +QT_END_NAMESPACE + +#include "moc_qquicklayout_p.cpp" |