Introducing QSplitter::replaceWidget()

This new API addresses the use case where we want to replace
a widget by another one inside the splitter. Up to now, the
way of doing would include removing one widget and add the
new one at the same place. However, this triggers a series
of resize and paint events because of the successive changes
in the splitter's children leading to a relayout of the
remaining children.

The new widget inherits the same properties as in the previous
slot: geometry, visibility, and collapsed states. The previous
widget, returned by the function, loses its parent and is hidden.

Change-Id: I3dddf6b582d5ce2db8cff3c40bc46084263123ac
Reviewed-by: Friedemann Kleint <Friedemann.Kleint@qt.io>
Reviewed-by: Paul Olav Tvete <paul.tvete@qt.io>
This commit is contained in:
Gabriel de Dietrich 2016-11-16 17:21:32 -08:00
parent 9f2f3cb90b
commit 2c634a1326
4 changed files with 242 additions and 3 deletions

View File

@ -731,6 +731,12 @@ void QSplitterPrivate::setSizes_helper(const QList<int> &sizes, bool clampNegati
doResize();
}
bool QSplitterPrivate::shouldShowWidget(const QWidget *w) const
{
Q_Q(const QSplitter);
return q->isVisible() && !(w->isHidden() && w->testAttribute(Qt::WA_WState_ExplicitShowHide));
}
void QSplitterPrivate::setGeo(QSplitterLayoutStruct *sls, int p, int s, bool allowCollapse)
{
Q_Q(QSplitter);
@ -827,8 +833,7 @@ void QSplitterPrivate::insertWidget_helper(int index, QWidget *widget, bool show
{
Q_Q(QSplitter);
QBoolBlocker b(blockChildAdd);
bool needShow = show && q->isVisible() &&
!(widget->isHidden() && widget->testAttribute(Qt::WA_WState_ExplicitShowHide));
const bool needShow = show && shouldShowWidget(widget);
if (widget->parentWidget() != q)
widget->setParent(q);
if (needShow)
@ -1124,6 +1129,66 @@ void QSplitter::insertWidget(int index, QWidget *widget)
d->insertWidget_helper(index, widget, true);
}
/*!
\since 5.9
Replaces the widget in the splitter's layout at the given \a index by \a widget.
Returns the widget that has just been replaced if \a index is valid and \a widget
is not already a child of the splitter. Otherwise, it returns null and no replacement
or addition is made.
The geometry of the newly inserted widget will be the same as the widget it replaces.
Its visible and collapsed states are also inherited.
\note The splitter takes ownership of \a widget and sets the parent of the
replaced widget to null.
\sa insertWidget(), indexOf()
*/
QWidget *QSplitter::replaceWidget(int index, QWidget *widget)
{
Q_D(QSplitter);
if (!widget) {
qWarning("QSplitter::replaceWidget: Widget can't be null");
return nullptr;
}
if (index < 0 || index >= d->list.count()) {
qWarning("QSplitter::replaceWidget: Index %d out of range", index);
return nullptr;
}
QSplitterLayoutStruct *s = d->list.at(index);
QWidget *current = s->widget;
if (current == widget) {
qWarning("QSplitter::replaceWidget: Trying to replace a widget with itself");
return nullptr;
}
if (widget->parentWidget() == this) {
qWarning("QSplitter::replaceWidget: Trying to replace a widget with one of its siblings");
return nullptr;
}
QBoolBlocker b(d->blockChildAdd);
const QRect geom = current->geometry();
const bool shouldShow = d->shouldShowWidget(current);
s->widget = widget;
current->setParent(nullptr);
widget->setParent(this);
// The splitter layout struct's geometry is already set and
// should not change. Only set the geometry on the new widget
widget->setGeometry(geom);
widget->lower();
widget->setVisible(shouldShow);
return current;
}
/*!
\fn int QSplitter::indexOf(QWidget *widget) const
@ -1232,7 +1297,7 @@ void QSplitter::childEvent(QChildEvent *c)
if (c->added() && !d->blockChildAdd && !d->findWidget(w)) {
d->insertWidget_helper(d->list.count(), w, false);
} else if (c->polished() && !d->blockChildAdd) {
if (isVisible() && !(w->isHidden() && w->testAttribute(Qt::WA_WState_ExplicitShowHide)))
if (d->shouldShowWidget(w))
w->show();
} else if (c->type() == QEvent::ChildRemoved) {
for (int i = 0; i < d->list.size(); ++i) {

View File

@ -71,6 +71,7 @@ public:
void addWidget(QWidget *widget);
void insertWidget(int index, QWidget *widget);
QWidget *replaceWidget(int index, QWidget *widget);
void setOrientation(Qt::Orientation);
Qt::Orientation orientation() const;

View File

@ -125,6 +125,7 @@ public:
int findWidgetJustBeforeOrJustAfter(int index, int delta, int &collapsibleSize) const;
void updateHandles();
void setSizes_helper(const QList<int> &sizes, bool clampNegativeSize = false);
bool shouldShowWidget(const QWidget *w) const;
};

View File

@ -77,6 +77,10 @@ private slots:
void rubberBandNotInSplitter();
void saveAndRestoreStateOfNotYetShownSplitter();
void saveAndRestoreHandleWidth();
void replaceWidget_data();
void replaceWidget();
void replaceWidgetWithSplitterChild_data();
void replaceWidgetWithSplitterChild();
// task-specific tests below me:
void task187373_addAbstractScrollAreas();
@ -645,9 +649,177 @@ public:
MyFriendlySplitter(QWidget *parent = 0) : QSplitter(parent) {}
void setRubberBand(int pos) { QSplitter::setRubberBand(pos); }
void moveSplitter(int pos, int index) { QSplitter::moveSplitter(pos, index); }
friend class tst_QSplitter;
};
class EventCounterSpy : public QObject
{
public:
EventCounterSpy(QWidget *parentWidget) : QObject(parentWidget)
{ }
bool eventFilter(QObject *watched, QEvent *event) override
{
// Watch for events in the parent widget and all its children
if (watched == parent() || watched->parent() == parent()) {
if (event->type() == QEvent::Resize)
resizeCount++;
else if (event->type() == QEvent::Paint)
paintCount++;
}
return QObject::eventFilter(watched, event);
}
int resizeCount = 0;
int paintCount = 0;
};
void tst_QSplitter::replaceWidget_data()
{
QTest::addColumn<int>("index");
QTest::addColumn<bool>("visible");
QTest::addColumn<bool>("collapsed");
QTest::newRow("negative index") << -1 << true << false;
QTest::newRow("index too large") << 80 << true << false;
QTest::newRow("visible, not collapsed") << 3 << true << false;
QTest::newRow("visible, collapsed") << 3 << true << true;
QTest::newRow("not visible, not collapsed") << 3 << false << false;
QTest::newRow("not visible, collapsed") << 3 << false << true;
}
void tst_QSplitter::replaceWidget()
{
QFETCH(int, index);
QFETCH(bool, visible);
QFETCH(bool, collapsed);
// Setup
MyFriendlySplitter sp;
const int count = 7;
for (int i = 0; i < count; i++) {
// We use labels instead of plain widgets to
// make it easier to fix eventual regressions.
QLabel *w = new QLabel(QString::asprintf("WIDGET #%d", i));
sp.addWidget(w);
}
sp.setWindowTitle(QString::asprintf("index %d, visible %d, collapsed %d", index, visible, collapsed));
sp.show();
QVERIFY(QTest::qWaitForWindowExposed(&sp));
// Configure splitter
QWidget *oldWidget = sp.widget(index);
const QRect oldGeom = oldWidget ? oldWidget->geometry() : QRect();
if (oldWidget) {
// Collapse first, then hide, if necessary
if (collapsed) {
sp.setCollapsible(index, true);
sp.moveSplitter(oldWidget->x() + 1, index + 1);
}
if (!visible)
oldWidget->hide();
}
// Replace widget
QTest::qWait(100); // Flush event queue
const QList<int> sizes = sp.sizes();
// Shorter label: The important thing is to ensure we can set
// the same size on the new widget. Because of QLabel's sizing
// constraints (they can expand but not shrink) the easiest is
// to set a shorter label.
QLabel *newWidget = new QLabel(QLatin1String("<b>NEW</b>"));
EventCounterSpy *ef = new EventCounterSpy(&sp);
qApp->installEventFilter(ef);
const QWidget *res = sp.replaceWidget(index, newWidget);
QTest::qWait(100); // Give visibility and resizing some time
qApp->removeEventFilter(ef);
// Check
if (index < 0 || index >= count) {
QVERIFY(!res);
QVERIFY(!newWidget->parentWidget());
QCOMPARE(ef->resizeCount, 0);
QCOMPARE(ef->paintCount, 0);
} else {
QCOMPARE(res, oldWidget);
QVERIFY(!res->parentWidget());
QVERIFY(!res->isVisible());
QCOMPARE(newWidget->parentWidget(), &sp);
QCOMPARE(newWidget->isVisible(), visible);
if (visible && !collapsed)
QCOMPARE(newWidget->geometry(), oldGeom);
QCOMPARE(newWidget->size().isEmpty(), !visible || collapsed);
const int expectedResizeCount = visible ? 1 : 0; // new widget only
const int expectedPaintCount = visible && !collapsed ? 2 : 0; // splitter and new widget
QCOMPARE(ef->resizeCount, expectedResizeCount);
QCOMPARE(ef->paintCount, expectedPaintCount);
delete res;
}
QCOMPARE(sp.count(), count);
QCOMPARE(sp.sizes(), sizes);
}
void tst_QSplitter::replaceWidgetWithSplitterChild_data()
{
QTest::addColumn<int>("srcIndex");
QTest::addColumn<int>("dstIndex");
QTest::newRow("replace with null widget") << -2 << 3;
QTest::newRow("replace with itself") << 3 << 3;
QTest::newRow("replace with sibling, after recalc") << 1 << 4;
QTest::newRow("replace with sibling, before recalc") << -1 << 4;
}
void tst_QSplitter::replaceWidgetWithSplitterChild()
{
QFETCH(int, srcIndex);
QFETCH(int, dstIndex);
// Setup
MyFriendlySplitter sp;
const int count = 7;
for (int i = 0; i < count; i++) {
// We use labels instead of plain widgets to
// make it easier to fix eventual regressions.
QLabel *w = new QLabel(QString::asprintf("WIDGET #%d", i));
sp.addWidget(w);
}
sp.setWindowTitle(QLatin1String(QTest::currentTestFunction()) + QLatin1Char(' ') + QLatin1String(QTest::currentDataTag()));
sp.show();
QVERIFY(QTest::qWaitForWindowExposed(&sp));
QTest::qWait(100); // Flush event queue before new widget creation
const QList<int> sizes = sp.sizes();
QWidget *sibling = srcIndex == -1 ? (new QLabel("<b>NEW</b>", &sp)) : sp.widget(srcIndex);
EventCounterSpy *ef = new EventCounterSpy(&sp);
qApp->installEventFilter(ef);
const QWidget *res = sp.replaceWidget(dstIndex, sibling);
QTest::qWait(100); // Give visibility and resizing some time
qApp->removeEventFilter(ef);
QVERIFY(!res);
if (srcIndex == -1) {
// Create and replace before recalc. The sibling is scheduled to be
// added after replaceWidget(), when QSplitter receives a child event.
QVERIFY(ef->resizeCount > 0);
QVERIFY(ef->paintCount > 0);
QCOMPARE(sp.count(), count + 1);
QCOMPARE(sp.sizes().mid(0, count), sizes);
QCOMPARE(sp.sizes().last(), sibling->width());
} else {
// No-op for the rest
QCOMPARE(ef->resizeCount, 0);
QCOMPARE(ef->paintCount, 0);
QCOMPARE(sp.count(), count);
QCOMPARE(sp.sizes(), sizes);
}
}
void tst_QSplitter::rubberBandNotInSplitter()
{
MyFriendlySplitter split;