Implement QFormLayout::set/isRowVisible

Hiding a row in a form layout is inconvenient to do as access to the
widgets in each row is cumbersome. In addition, a row might include a
layout for the label or the field column, and we can't hide layouts and
instead need to navigate to the widgets inside the layout. And even if
an application developer does all that, the spacing calculation doesn't
ignore hidden rows.

Add setRowVisible and isRowVisible APIs with the usual overloads.
Implement the logic to traverse a layout item to its contained widgets,
so that they are explicitly hidden when a row is hidden, and skip hidden
rows in the spacing calculation.

[ChangeLog][Widgets][QFormLayout] New APIs setRowVisible and isRowVisible
to hide and show rows in a form layout.

Fixes: QTBUG-6864
Change-Id: I6af98409802f331c4523e91d7dac8a97762c579d
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Axel Spoerl <axel.spoerl@qt.io>
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
bb10
Volker Hilsheimer 2022-02-22 21:48:58 +01:00
parent 599c648b77
commit a74cdf778c
3 changed files with 330 additions and 7 deletions

View File

@ -127,11 +127,15 @@ struct QFormLayoutItem
void setGeometry(const QRect& r) { item->setGeometry(r); }
QRect geometry() const { return item->geometry(); }
void setVisible(bool on);
bool isHidden() const { return !isVisible || (widget() && widget()->isHidden()); }
// For use with FixedColumnMatrix
bool operator==(const QFormLayoutItem& other) { return item == other.item; }
QLayoutItem *item;
bool fullRow;
bool isVisible = true;
// set by updateSizes
bool isHfw;
@ -152,6 +156,33 @@ struct QFormLayoutItem
int layoutWidth;
};
static void hideOrShowWidgetsInLayout(QLayout *layout, bool on)
{
for (int i = 0; i < layout->count(); ++i) {
QLayoutItem *item = layout->itemAt(i);
if (QWidget *widget = item->widget())
widget->setVisible(on);
else if (item->layout())
hideOrShowWidgetsInLayout(item->layout(), on);
}
}
void QFormLayoutItem::setVisible(bool on)
{
isVisible = on;
// Explicitly hide the widget so that it loses focus and
// doesn't automatically get shown again when this layout
// hides and shows.
if (widget()) {
widget()->setVisible(on);
return;
}
// Layouts can't be hidden, so we have to traverse the widgets
// inside and hide all of them so that they also lose focus.
if (layout())
hideOrShowWidgetsInLayout(layout(), on);
}
class QFormLayoutPrivate : public QLayoutPrivate
{
Q_DECLARE_PUBLIC(QFormLayout)
@ -378,23 +409,25 @@ void QFormLayoutPrivate::updateSizes()
QSizePolicy::ControlTypes(fldtop ? fldtop->controlTypes() : QSizePolicy::DefaultType);
// To be compatible to QGridLayout, we have to compare solitary labels & fields with both predecessors
if (label) {
if (label && !label->isHidden()) {
if (!field) {
int lblspacing = style->combinedLayoutSpacing(lbltoptypes, lbltypes, Qt::Vertical, nullptr, parent);
int fldspacing = style->combinedLayoutSpacing(fldtoptypes, lbltypes, Qt::Vertical, nullptr, parent);
label->vSpace = qMax(lblspacing, fldspacing);
} else
} else {
label->vSpace = style->combinedLayoutSpacing(lbltoptypes, lbltypes, Qt::Vertical, nullptr, parent);
}
}
if (field) {
if (field && !field->isHidden()) {
// check spacing against both the previous label and field
if (!label) {
int lblspacing = style->combinedLayoutSpacing(lbltoptypes, fldtypes, Qt::Vertical, nullptr, parent);
int fldspacing = style->combinedLayoutSpacing(fldtoptypes, fldtypes, Qt::Vertical, nullptr, parent);
field->vSpace = qMax(lblspacing, fldspacing);
} else
} else {
field->vSpace = style->combinedLayoutSpacing(fldtoptypes, fldtypes, Qt::Vertical, nullptr, parent);
}
}
}
}
@ -681,11 +714,11 @@ void QFormLayoutPrivate::setupVerticalLayoutData(int width)
bool prevRowSplit = false;
for (int i = 0; i < rr; ++i) {
QFormLayoutItem *label = m_matrix(i, 0);
QFormLayoutItem *label = m_matrix(i, 0);
QFormLayoutItem *field = m_matrix(i, 1);
// Totally ignore empty rows...
if (!label && !field)
// Totally ignore empty rows or rows with only hidden items
if (!q->isRowVisible(i))
continue;
QSize min1;
@ -2299,6 +2332,158 @@ void QFormLayout::setItem(int row, ItemRole role, QLayoutItem *item)
d->setItem(row, role, item);
}
/*!
\since 6.4
Shows the row \a row if \a on is true, otherwise hides the row.
\a row must be non-negative and less than rowCount().
\sa removeRow(), takeRow()
*/
void QFormLayout::setRowVisible(int row, bool on)
{
Q_D(QFormLayout);
QFormLayoutItem *label = d->m_matrix(row, 0);
QFormLayoutItem *field = d->m_matrix(row, 1);
bool change = false;
if (label) {
change = label->isVisible != on;
label->setVisible(on);
}
if (field) {
change |= field->isVisible != on;
field->setVisible(on);
}
if (change)
invalidate();
}
/*!
\since 6.4
\overload
Shows the row corresponding to \a widget if \a on is true,
otherwise hides the row.
\sa removeRow(), takeRow()
*/
void QFormLayout::setRowVisible(QWidget *widget, bool on)
{
Q_D(QFormLayout);
if (Q_UNLIKELY(!d->checkWidget(widget)))
return;
int row;
ItemRole role;
getWidgetPosition(widget, &row, &role);
if (Q_UNLIKELY(row < 0)) {
qWarning("QFormLayout::setRowVisible: Invalid widget");
return;
}
setRowVisible(row, on);
}
/*!
\since 6.4
\overload
Shows the row corresponding to \a layout if \a on is true,
otherwise hides the row.
\sa removeRow(), takeRow()
*/
void QFormLayout::setRowVisible(QLayout *layout, bool on)
{
Q_D(QFormLayout);
if (Q_UNLIKELY(!d->checkLayout(layout)))
return;
int row;
ItemRole role;
getLayoutPosition(layout, &row, &role);
if (Q_UNLIKELY(row < 0)) {
qWarning("QFormLayout::setRowVisible: Invalid layout");
return;
}
setRowVisible(row, on);
}
/*!
\since 6.4
Returns true if some items in the row \a row are visible,
otherwise returns false.
*/
bool QFormLayout::isRowVisible(int row) const
{
Q_D(const QFormLayout);
QFormLayoutItem *label = d->m_matrix(row, 0);
QFormLayoutItem *field = d->m_matrix(row, 1);
int visibleItemCount = 2;
if (!label || label->isHidden() || (label->widget() && label->widget()->isHidden()))
--visibleItemCount;
if (!field || field->isHidden() || (field->widget() && field->widget()->isHidden()))
--visibleItemCount;
return visibleItemCount > 0;
}
/*!
\since 6.4
\overload
Returns true if some items in the row corresponding to \a widget
are visible, otherwise returns false.
*/
bool QFormLayout::isRowVisible(QWidget *widget) const
{
Q_D(const QFormLayout);
if (Q_UNLIKELY(!d->checkWidget(widget)))
return false;
int row;
ItemRole role;
getWidgetPosition(widget, &row, &role);
if (Q_UNLIKELY(row < 0)) {
qWarning("QFormLayout::takeRow: Invalid widget");
return false;
}
return isRowVisible(row);
}
/*!
\since 6.4
\overload
Returns true if some items in the row corresponding to \a layout
are visible, otherwise returns false.
*/
bool QFormLayout::isRowVisible(QLayout *layout) const
{
Q_D(const QFormLayout);
if (Q_UNLIKELY(!d->checkLayout(layout)))
return false;
int row;
ItemRole role;
getLayoutPosition(layout, &row, &role);
if (Q_UNLIKELY(row < 0)) {
qWarning("QFormLayout::takeRow: Invalid layout");
return false;
}
return isRowVisible(row);
}
/*!
\internal
*/

View File

@ -138,6 +138,14 @@ public:
void setWidget(int row, ItemRole role, QWidget *widget);
void setLayout(int row, ItemRole role, QLayout *layout);
void setRowVisible(int row, bool on);
void setRowVisible(QWidget *widget, bool on);
void setRowVisible(QLayout *layout, bool on);
bool isRowVisible(int row) const;
bool isRowVisible(QWidget *widget) const;
bool isRowVisible(QLayout *layout) const;
QLayoutItem *itemAt(int row, ItemRole role) const;
void getItemPosition(int index, int *rowPtr, ItemRole *rolePtr) const;
void getWidgetPosition(QWidget *widget, int *rowPtr, ItemRole *rolePtr) const;

View File

@ -135,6 +135,7 @@ private slots:
void takeRow_QLayout();
void setWidget();
void setLayout();
void hideShowRow();
/*
QLayoutItem *itemAt(int row, ItemRole role) const;
@ -1123,6 +1124,135 @@ void tst_QFormLayout::setLayout()
}
}
void tst_QFormLayout::hideShowRow()
{
QWidget topLevel;
QFormLayout layout;
const auto makeComplex = []{
QHBoxLayout *hboxField = new QHBoxLayout;
hboxField->addWidget(new QLineEdit("Left"));
hboxField->addWidget(new QLineEdit("Right"));
return hboxField;
};
layout.addRow("Label", new QLineEdit("one"));
layout.addRow("Label", new QLineEdit("two"));
layout.addRow("Label", new QLineEdit("three"));
layout.addRow("Label", makeComplex());
layout.addRow(new QLineEdit("five")); // spanning widget
layout.addRow(makeComplex()); // spanning layout
topLevel.setLayout(&layout);
topLevel.show();
QVERIFY(QTest::qWaitForWindowExposed(&topLevel));
// returns the top-left position of the items in a row
const auto rowPosition = [&layout](int row) {
QRect rect;
if (QLayoutItem *spanningItem = layout.itemAt(row, QFormLayout::SpanningRole)) {
rect = spanningItem->geometry();
} else {
if (QLayoutItem *labelItem = layout.itemAt(row, QFormLayout::LabelRole)) {
rect = labelItem->geometry();
}
if (QLayoutItem *fieldItem = layout.itemAt(row, QFormLayout::FieldRole)) {
rect |= fieldItem->geometry();
}
}
return rect.topLeft();
};
// returns the first widget in a row, even if that row is taken by a layout
const auto rowInputWidget = [&layout](int row) -> QWidget* {
auto fieldItem = layout.itemAt(row, QFormLayout::FieldRole);
if (!fieldItem)
return nullptr;
QWidget *fieldWidget = fieldItem->widget();
// we happen to know our layout structure
if (!fieldWidget)
fieldWidget = fieldItem->layout()->itemAt(0)->widget();
return fieldWidget;
};
// record the reference positions for all rows
QList<QPoint> rowPositions(layout.rowCount());
for (int row = 0; row < layout.rowCount(); ++row)
rowPositions[row] = rowPosition(row);
// hide each row in turn, the next row should take the space of the hidden row
for (int row = 0; row < layout.rowCount(); ++ row) {
layout.setRowVisible(row, false);
QVERIFY(!layout.isRowVisible(row));
if (row < layout.rowCount() - 1)
QTRY_COMPARE(rowPosition(row + 1), rowPositions[row]);
layout.setRowVisible(row, true);
QVERIFY(layout.isRowVisible(row));
}
// Hiding only the label or only the field doesn't hide the row.
for (int row = 0; row < layout.rowCount() - 1; ++row) {
const auto labelItem = layout.itemAt(0, QFormLayout::LabelRole);
if (labelItem) {
labelItem->widget()->hide();
QVERIFY(layout.isRowVisible(row));
QCOMPARE(rowPosition(row), rowPositions[row]);
layout.itemAt(0, QFormLayout::LabelRole)->widget()->show();
}
const auto fieldItem = layout.itemAt(0, QFormLayout::FieldRole);
if (fieldItem) {
fieldItem->widget()->hide();
QVERIFY(layout.isRowVisible(row));
QCOMPARE(rowPosition(row), rowPositions[row]);
layout.itemAt(0, QFormLayout::FieldRole)->widget()->show();
}
}
// If we hide both label and field, then the row should be considered hidden and the
// following row should move up into the space of the hidden row. We can only test
// this if both label and field are widgets, or if there is a spanning widget.
for (int row = 0; row < layout.rowCount() - 1; ++row) {
QWidget *labelWidget = nullptr;
if (auto labelItem = layout.itemAt(row, QFormLayout::LabelRole))
labelWidget = labelItem->widget();
QWidget *fieldWidget = nullptr;
if (auto fieldItem = layout.itemAt(row, QFormLayout::FieldRole))
fieldWidget = fieldItem->widget();
if (!fieldWidget)
continue;
if (labelWidget)
labelWidget->hide();
fieldWidget->hide();
QVERIFY(!layout.isRowVisible(row));
QVERIFY(!layout.isRowVisible(fieldWidget));
if (labelWidget)
QVERIFY(!layout.isRowVisible(labelWidget));
QTRY_COMPARE(rowPosition(row + 1), rowPositions[row]);
if (labelWidget)
labelWidget->show();
fieldWidget->show();
}
// hiding a row where a widget has focus must move focus to a widget in the next row
for (int row = 0; row < layout.rowCount(); ++row) {
QWidget *inputWidget = rowInputWidget(row);
QVERIFY(inputWidget);
inputWidget->setFocus();
layout.setRowVisible(row, false);
QVERIFY(!inputWidget->hasFocus());
}
// Now hide all rows, hide the toplevel widget, and show the toplevel widget again.
// None of the widgets inside must be visible.
for (int row = 0; row < layout.rowCount(); ++row)
layout.setRowVisible(row, false);
topLevel.hide();
topLevel.show();
for (int row = 0; row < layout.rowCount(); ++row)
QVERIFY(rowInputWidget(row)->isHidden());
}
void tst_QFormLayout::itemAt()
{
QWidget topLevel;