As part of eacd58d4e78e7238ba5fcca90ba960aaf3ebd263, a mechanism was added to prevent posting redundant UpdateRequest events to the top level widget, managed by QWidgetRepaintManager. The mechanism relied on a boolean that was set when posting an update request event, and reset when processing the event for the top level widget, as part of QWidgetRepaintManager::sync(). However, for paint-on-screen widgets, we don't post an update request to the top level, we post it to the paint-on-screen widget directly. And when processing that event, we don't paint the widget though the normal QWidgetRepaintManager machinery, but instead call the paint event via QWidgetPrivate::paintOnScreen(). As a result, an update() on a paint-on-screen widget would result in never receiving updates for non-paint-on-screen widgets, as we assumed there was no reason to send further update requests. We could fix this by clearing the updateRequestSent flag as part of the paintOnScreen() code path, but that's incorrect as the flag represents whether the top level QWidgetRepaintManager needs an update request event or not, and would lead to missed updates to normal widgets until the paint-on-screen widget finishes its update. Instead, we only set updateRequestSent if we're posting update requests for non-paint-on-screen widgets, which in practice means the top level widget. The paint on screen logic in QWidgetRepaintManager::markDirty still takes care of rate-limiting the update requests to the paint-on-screen widget, by comparing the dirty area of the widget. There is definite room for improvement here, especially in the direction of handling update requests via QWindow::requestUpdate instead of manually posted events, but for now this will have to do. Fixes: QTBUG-80167 Pick-to: 6.6 6.5 6.2 5.15 Change-Id: Ib5685de7ca2fd7cd7883a25bb7bc0255ea242d30 Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io> (cherry picked from commit 697e1b0397259959e3f555296f87a0d9d923e4b5) Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
832 lines
27 KiB
C++
832 lines
27 KiB
C++
// Copyright (C) 2021 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
|
|
|
|
|
#include <QTest>
|
|
#include <QPainter>
|
|
#include <QScrollArea>
|
|
#include <QScrollBar>
|
|
#include <QApplication>
|
|
|
|
#include <private/qhighdpiscaling_p.h>
|
|
#include <private/qwidget_p.h>
|
|
#include <private/qwidgetrepaintmanager_p.h>
|
|
#include <qpa/qplatformintegration.h>
|
|
#include <qpa/qplatformbackingstore.h>
|
|
#include <private/qguiapplication_p.h>
|
|
|
|
//#define MANUAL_DEBUG
|
|
|
|
class TestWidget : public QWidget
|
|
{
|
|
public:
|
|
TestWidget(QWidget *parent = nullptr)
|
|
: QWidget(parent)
|
|
{
|
|
}
|
|
|
|
QSize sizeHint() const override
|
|
{
|
|
const int screenWidth = QGuiApplication::primaryScreen()->geometry().width();
|
|
const int width = qMax(200, 100 * ((screenWidth + 500) / 1000));
|
|
return isWindow() ? QSize(width, width) : QSize(width - 40, width - 40);
|
|
}
|
|
|
|
void initialShow()
|
|
{
|
|
show();
|
|
if (isWindow()) {
|
|
QVERIFY(QTest::qWaitForWindowExposed(this));
|
|
QVERIFY(waitForPainted());
|
|
}
|
|
paintedRegions = {};
|
|
}
|
|
|
|
bool waitForPainted(int timeout = 5000)
|
|
{
|
|
int remaining = timeout;
|
|
QDeadlineTimer deadline(remaining, Qt::PreciseTimer);
|
|
if (!QTest::qWaitFor([this]{ return !paintedRegions.isEmpty(); }, timeout))
|
|
return false;
|
|
|
|
// In case of multiple paint events:
|
|
// Process events and wait until all have been consumed,
|
|
// i.e. paintedRegions no longer changes.
|
|
QRegion reg;
|
|
while (remaining > 0 && reg != paintedRegions) {
|
|
reg = paintedRegions;
|
|
QCoreApplication::processEvents(QEventLoop::AllEvents, remaining);
|
|
if (reg == paintedRegions)
|
|
return true;
|
|
|
|
remaining = int(deadline.remainingTime());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
QRegion takePaintedRegions()
|
|
{
|
|
QRegion result = paintedRegions;
|
|
paintedRegions = {};
|
|
return result;
|
|
}
|
|
QRegion paintedRegions;
|
|
|
|
bool event(QEvent *event) override
|
|
{
|
|
const auto type = event->type();
|
|
if (type == QEvent::WindowActivate || type == QEvent::WindowDeactivate)
|
|
return true;
|
|
if (type == QEvent::UpdateRequest)
|
|
++updateRequests;
|
|
return QWidget::event(event);
|
|
}
|
|
int updateRequests = 0;
|
|
|
|
protected:
|
|
void paintEvent(QPaintEvent *event) override
|
|
{
|
|
paintedRegions += event->region();
|
|
QPainter painter(this);
|
|
const QBrush patternBrush = isWindow() ? QBrush(Qt::blue, Qt::VerPattern)
|
|
: QBrush(Qt::red, Qt::HorPattern);
|
|
painter.fillRect(rect(), patternBrush);
|
|
}
|
|
};
|
|
|
|
class OpaqueWidget : public QWidget
|
|
{
|
|
public:
|
|
OpaqueWidget(const QColor &col, QWidget *parent = nullptr)
|
|
: QWidget(parent), fillColor(col)
|
|
{
|
|
setAttribute(Qt::WA_OpaquePaintEvent);
|
|
}
|
|
|
|
bool event(QEvent *event) override
|
|
{
|
|
const auto type = event->type();
|
|
if (type == QEvent::WindowActivate || type == QEvent::WindowDeactivate)
|
|
return true;
|
|
return QWidget::event(event);
|
|
}
|
|
|
|
protected:
|
|
void paintEvent(QPaintEvent *e) override
|
|
{
|
|
Q_UNUSED(e);
|
|
QPainter painter(this);
|
|
fillColor.setBlue(paintCount % 255);
|
|
painter.fillRect(e->rect(), fillColor);
|
|
#ifdef MANUAL_DEBUG
|
|
++paintCount;
|
|
painter.drawText(rect(), Qt::AlignCenter, QString::number(paintCount));
|
|
#endif
|
|
}
|
|
|
|
private:
|
|
QColor fillColor;
|
|
int paintCount = 0;
|
|
};
|
|
|
|
class Draggable : public OpaqueWidget
|
|
{
|
|
public:
|
|
Draggable(QWidget *parent = nullptr)
|
|
: OpaqueWidget(Qt::white, parent)
|
|
{
|
|
}
|
|
|
|
Draggable(const QColor &col, QWidget *parent = nullptr)
|
|
: OpaqueWidget(col, parent)
|
|
{
|
|
left = new OpaqueWidget(Qt::gray, this);
|
|
top = new OpaqueWidget(Qt::gray, this);
|
|
right = new OpaqueWidget(Qt::gray, this);
|
|
bottom = new OpaqueWidget(Qt::gray, this);
|
|
}
|
|
|
|
QSize sizeHint() const override {
|
|
return QSize(100, 100);
|
|
}
|
|
|
|
protected:
|
|
void resizeEvent(QResizeEvent *) override
|
|
{
|
|
if (!left)
|
|
return;
|
|
left->setGeometry(0, 0, 10, height());
|
|
top->setGeometry(10, 0, width() - 10, 10);
|
|
right->setGeometry(width() - 10, 10, 10, height() - 10);
|
|
bottom->setGeometry(10, height() - 10, width() - 10, 10);
|
|
}
|
|
|
|
void mousePressEvent(QMouseEvent *e) override
|
|
{
|
|
lastPos = e->position().toPoint();
|
|
}
|
|
void mouseMoveEvent(QMouseEvent *e) override
|
|
{
|
|
QPoint pos = geometry().topLeft();
|
|
pos += e->position().toPoint() - lastPos;
|
|
move(pos);
|
|
}
|
|
void mouseReleaseEvent(QMouseEvent *) override
|
|
{
|
|
lastPos = {};
|
|
}
|
|
|
|
private:
|
|
OpaqueWidget *left = nullptr;
|
|
OpaqueWidget *top = nullptr;
|
|
OpaqueWidget *right = nullptr;
|
|
OpaqueWidget *bottom = nullptr;
|
|
QPoint lastPos;
|
|
};
|
|
|
|
class TestScene : public QWidget
|
|
{
|
|
public:
|
|
TestScene()
|
|
{
|
|
setObjectName("scene");
|
|
|
|
// opaque because it has an opaque background color and autoFillBackground is set
|
|
area = new QWidget(this);
|
|
area->setObjectName("area");
|
|
area->setAutoFillBackground(true);
|
|
QPalette palette;
|
|
palette.setColor(QPalette::Window, QColor::fromRgb(0, 0, 0));
|
|
area->setPalette(palette);
|
|
|
|
// all these children set WA_OpaquePaintEvent
|
|
redChild = new Draggable(Qt::red, area);
|
|
redChild->setObjectName("redChild");
|
|
|
|
greenChild = new Draggable(Qt::green, area);
|
|
greenChild->setObjectName("greenChild");
|
|
|
|
yellowChild = new Draggable(Qt::yellow, this);
|
|
yellowChild->setObjectName("yellowChild");
|
|
|
|
nakedChild = new Draggable(this);
|
|
nakedChild->move(300, 0);
|
|
nakedChild->setObjectName("nakedChild");
|
|
|
|
bar = new OpaqueWidget(Qt::darkGray, this);
|
|
bar->setObjectName("bar");
|
|
}
|
|
|
|
QWidget *area;
|
|
QWidget *redChild;
|
|
QWidget *greenChild;
|
|
QWidget *yellowChild;
|
|
QWidget *nakedChild;
|
|
QWidget *bar;
|
|
|
|
QSize sizeHint() const override { return QSize(400, 400); }
|
|
|
|
bool event(QEvent *event) override
|
|
{
|
|
const auto type = event->type();
|
|
if (type == QEvent::WindowActivate || type == QEvent::WindowDeactivate)
|
|
return true;
|
|
return QWidget::event(event);
|
|
}
|
|
|
|
protected:
|
|
void resizeEvent(QResizeEvent *) override
|
|
{
|
|
area->setGeometry(50, 50, width() - 100, height() - 100);
|
|
bar->setGeometry(width() / 2 - 25, height() / 2, 50, height() / 2);
|
|
}
|
|
};
|
|
|
|
class tst_QWidgetRepaintManager : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
public:
|
|
tst_QWidgetRepaintManager();
|
|
|
|
public slots:
|
|
void initTestCase();
|
|
void cleanup();
|
|
|
|
private slots:
|
|
void basic();
|
|
void children();
|
|
void opaqueChildren();
|
|
void staticContents();
|
|
void scroll();
|
|
void paintOnScreenUpdates();
|
|
|
|
#if defined(QT_BUILD_INTERNAL)
|
|
void scrollWithOverlap();
|
|
void overlappedRegion();
|
|
void fastMove();
|
|
void moveAccross();
|
|
void moveInOutOverlapped();
|
|
|
|
protected:
|
|
/*
|
|
This helper compares the widget as rendered into the backingstore with the widget
|
|
as rendered via QWidget::grab. The latter always produces a fully rendered image,
|
|
so differences indicate bugs in QWidgetRepaintManager's or QWidget's painting code.
|
|
*/
|
|
bool compareWidget(QWidget *w)
|
|
{
|
|
QBackingStore *backingStore = w->window()->backingStore();
|
|
Q_ASSERT(backingStore && backingStore->handle());
|
|
QPlatformBackingStore *platformBackingStore = backingStore->handle();
|
|
|
|
if (!waitForFlush(w)) {
|
|
qWarning() << "Widget" << w << "failed to flush";
|
|
return false;
|
|
}
|
|
|
|
QImage backingstoreContent = platformBackingStore->toImage();
|
|
if (!w->isWindow()) {
|
|
const qreal dpr = w->devicePixelRatioF();
|
|
const QPointF offset = w->mapTo(w->window(), QPointF(0, 0)) * dpr;
|
|
backingstoreContent = backingstoreContent.copy(offset.x(), offset.y(), w->width() * dpr, w->height() * dpr);
|
|
}
|
|
const QImage widgetRender = w->grab().toImage().convertToFormat(backingstoreContent.format());
|
|
|
|
const bool result = backingstoreContent == widgetRender;
|
|
|
|
#ifdef MANUAL_DEBUG
|
|
if (!result) {
|
|
backingstoreContent.save(QString("/tmp/backingstore_%1_%2.png").arg(QTest::currentTestFunction(), QTest::currentDataTag()));
|
|
widgetRender.save(QString("/tmp/grab_%1_%2.png").arg(QTest::currentTestFunction(), QTest::currentDataTag()));
|
|
}
|
|
#endif
|
|
return result;
|
|
};
|
|
|
|
QRegion dirtyRegion(QWidget *widget) const
|
|
{
|
|
return QWidgetPrivate::get(widget)->dirty;
|
|
}
|
|
bool waitForFlush(QWidget *widget) const
|
|
{
|
|
if (!widget)
|
|
return true;
|
|
|
|
auto *repaintManager = QWidgetPrivate::get(widget->window())->maybeRepaintManager();
|
|
|
|
if (!repaintManager)
|
|
return true;
|
|
|
|
return QTest::qWaitFor([repaintManager]{ return !repaintManager->isDirty(); } );
|
|
};
|
|
#endif // QT_BUILD_INTERNAL
|
|
|
|
|
|
private:
|
|
const int m_fuzz;
|
|
bool m_implementsScroll = false;
|
|
};
|
|
|
|
tst_QWidgetRepaintManager::tst_QWidgetRepaintManager() :
|
|
m_fuzz(int(QHighDpiScaling::factor(QGuiApplication::primaryScreen())))
|
|
{
|
|
}
|
|
|
|
void tst_QWidgetRepaintManager::initTestCase()
|
|
{
|
|
QWidget widget;
|
|
widget.show();
|
|
QVERIFY(QTest::qWaitForWindowExposed(&widget));
|
|
|
|
m_implementsScroll = widget.backingStore()->handle()->scroll(QRegion(widget.rect()), 1, 1);
|
|
qInfo() << QGuiApplication::platformName() << "QPA backend implements scroll:" << m_implementsScroll;
|
|
}
|
|
|
|
void tst_QWidgetRepaintManager::cleanup()
|
|
{
|
|
QVERIFY(QApplication::topLevelWidgets().isEmpty());
|
|
}
|
|
|
|
void tst_QWidgetRepaintManager::basic()
|
|
{
|
|
TestWidget widget;
|
|
widget.show();
|
|
QVERIFY(QTest::qWaitForWindowExposed(&widget));
|
|
|
|
QCOMPARE(widget.takePaintedRegions(), QRegion(0, 0, widget.width(), widget.height()));
|
|
|
|
widget.update();
|
|
QVERIFY(widget.waitForPainted());
|
|
QCOMPARE(widget.takePaintedRegions(), QRegion(0, 0, widget.width(), widget.height()));
|
|
|
|
widget.repaint();
|
|
QCOMPARE(widget.takePaintedRegions(), QRegion(0, 0, widget.width(), widget.height()));
|
|
}
|
|
|
|
/*!
|
|
Children cannot assumed to be fully opaque, so the parent will repaint when the
|
|
child repaints.
|
|
*/
|
|
void tst_QWidgetRepaintManager::children()
|
|
{
|
|
if (QStringList{"android"}.contains(QGuiApplication::platformName()))
|
|
QSKIP("This test fails on Android");
|
|
|
|
TestWidget widget;
|
|
widget.initialShow();
|
|
|
|
TestWidget *child1 = new TestWidget(&widget);
|
|
child1->move(20, 20);
|
|
child1->show();
|
|
QVERIFY(QTest::qWaitForWindowExposed(child1));
|
|
QVERIFY(child1->waitForPainted());
|
|
QCOMPARE(widget.takePaintedRegions(), QRegion(child1->geometry()));
|
|
QCOMPARE(child1->takePaintedRegions(), QRegion(child1->rect()));
|
|
|
|
child1->move(20, 30);
|
|
QVERIFY(widget.waitForPainted());
|
|
// both the old and the new area covered by child1 need to be repainted
|
|
QCOMPARE(widget.takePaintedRegions(), QRegion(20, 20, child1->width(), child1->height() + 10));
|
|
QCOMPARE(child1->takePaintedRegions(), QRegion(child1->rect()));
|
|
|
|
TestWidget *child2 = new TestWidget(&widget);
|
|
child2->move(30, 30);
|
|
child2->raise();
|
|
child2->show();
|
|
|
|
QVERIFY(child2->waitForPainted());
|
|
QCOMPARE(widget.takePaintedRegions(), QRegion(child2->geometry()));
|
|
QCOMPARE(child1->takePaintedRegions(), QRegion(10, 0, child2->width() - 10, child2->height()));
|
|
QCOMPARE(child2->takePaintedRegions(), QRegion(child2->rect()));
|
|
|
|
child1->hide();
|
|
QVERIFY(widget.waitForPainted());
|
|
QCOMPARE(widget.paintedRegions, QRegion(child1->geometry()));
|
|
}
|
|
|
|
void tst_QWidgetRepaintManager::opaqueChildren()
|
|
{
|
|
if (QStringList{"android"}.contains(QGuiApplication::platformName()))
|
|
QSKIP("This test fails on Android");
|
|
|
|
TestWidget widget;
|
|
widget.initialShow();
|
|
|
|
TestWidget *child1 = new TestWidget(&widget);
|
|
child1->move(20, 20);
|
|
child1->setAttribute(Qt::WA_OpaquePaintEvent);
|
|
child1->show();
|
|
|
|
QVERIFY(child1->waitForPainted());
|
|
QCOMPARE(widget.takePaintedRegions(), QRegion());
|
|
QCOMPARE(child1->takePaintedRegions(), child1->rect());
|
|
|
|
child1->move(20, 30);
|
|
QVERIFY(widget.waitForPainted());
|
|
QCOMPARE(widget.takePaintedRegions(), QRegion(20, 20, child1->width(), 10));
|
|
if (!m_implementsScroll)
|
|
QEXPECT_FAIL("", "child1 shouldn't get painted, we can just move the area of the backingstore", Continue);
|
|
QCOMPARE(child1->takePaintedRegions(), QRegion());
|
|
}
|
|
|
|
/*!
|
|
When resizing to be larger, a widget with Qt::WA_StaticContents set
|
|
should only repaint the newly revealed areas.
|
|
*/
|
|
void tst_QWidgetRepaintManager::staticContents()
|
|
{
|
|
const auto *integration = QGuiApplicationPrivate::platformIntegration();
|
|
if (!integration->hasCapability(QPlatformIntegration::BackingStoreStaticContents))
|
|
QSKIP("Platform does not support static backingstore content");
|
|
|
|
TestWidget widget;
|
|
widget.setAttribute(Qt::WA_StaticContents);
|
|
widget.initialShow();
|
|
|
|
// Trigger resize via QWindow (similar to QWSI code path)
|
|
QVERIFY(widget.windowHandle());
|
|
QSize oldSize = widget.size();
|
|
widget.windowHandle()->resize(widget.width(), widget.height() + 10);
|
|
QVERIFY(widget.waitForPainted());
|
|
QCOMPARE(widget.takePaintedRegions(), QRegion(0, oldSize.width(), widget.width(), 10));
|
|
|
|
// Trigger resize via QWidget
|
|
oldSize = widget.size();
|
|
widget.resize(widget.width() + 10, widget.height());
|
|
QVERIFY(widget.waitForPainted());
|
|
QEXPECT_FAIL("", "QWidgetPrivate::setGeometry_sys wrongly triggers full update", Continue);
|
|
QCOMPARE(widget.takePaintedRegions(), QRegion(oldSize.width(), 0, 10, widget.height()));
|
|
}
|
|
|
|
/*!
|
|
Scrolling a widget.
|
|
*/
|
|
void tst_QWidgetRepaintManager::scroll()
|
|
{
|
|
if (QStringList{"android"}.contains(QGuiApplication::platformName()))
|
|
QSKIP("This test fails on Android");
|
|
|
|
TestWidget widget;
|
|
widget.initialShow();
|
|
|
|
widget.scroll(10, 0);
|
|
QVERIFY(widget.waitForPainted());
|
|
if (!m_implementsScroll)
|
|
QEXPECT_FAIL("", "This should just repaint the newly exposed region", Continue);
|
|
QCOMPARE(widget.takePaintedRegions(), QRegion(0, 0, 10, widget.height()));
|
|
|
|
TestWidget *child = new TestWidget(&widget);
|
|
child->move(20, 20);
|
|
child->initialShow();
|
|
|
|
// a potentially semi-transparent child scrolling needs a full repaint
|
|
child->scroll(10, 0);
|
|
QVERIFY(child->waitForPainted());
|
|
QCOMPARE(child->takePaintedRegions(), child->rect());
|
|
QCOMPARE(widget.takePaintedRegions(), child->geometry());
|
|
|
|
// a explicitly opaque child scrolling only needs the child to repaint newly exposed regions
|
|
child->setAttribute(Qt::WA_OpaquePaintEvent);
|
|
child->scroll(10, 0);
|
|
QVERIFY(child->waitForPainted());
|
|
if (!m_implementsScroll)
|
|
QEXPECT_FAIL("", "This should just repaint the newly exposed region", Continue);
|
|
QCOMPARE(child->takePaintedRegions(), QRegion(0, 0, 10, child->height()));
|
|
QCOMPARE(widget.takePaintedRegions(), QRegion());
|
|
}
|
|
|
|
class PaintOnScreenWidget : public TestWidget
|
|
{
|
|
public:
|
|
using TestWidget::TestWidget;
|
|
|
|
// Explicit override to prevent noPaintOnScreen on Windows
|
|
QPaintEngine *paintEngine() const override
|
|
{
|
|
return nullptr;
|
|
}
|
|
};
|
|
|
|
void tst_QWidgetRepaintManager::paintOnScreenUpdates()
|
|
{
|
|
{
|
|
TestWidget topLevel;
|
|
topLevel.setObjectName("TopLevel");
|
|
topLevel.resize(500, 500);
|
|
TestWidget renderToTextureWidget(&topLevel);
|
|
renderToTextureWidget.setObjectName("RenderToTexture");
|
|
renderToTextureWidget.setGeometry(0, 0, 200, 200);
|
|
QWidgetPrivate::get(&renderToTextureWidget)->setRenderToTexture();
|
|
|
|
PaintOnScreenWidget paintOnScreenWidget(&topLevel);
|
|
paintOnScreenWidget.setObjectName("PaintOnScreen");
|
|
paintOnScreenWidget.setGeometry(200, 200, 300, 300);
|
|
|
|
topLevel.initialShow();
|
|
|
|
// Updating before toggling WA_PaintOnScreen should work fine
|
|
paintOnScreenWidget.update();
|
|
paintOnScreenWidget.waitForPainted();
|
|
QVERIFY(paintOnScreenWidget.waitForPainted());
|
|
|
|
#ifdef Q_OS_ANDROID
|
|
QEXPECT_FAIL("", "This test fails on Android", Abort);
|
|
#endif
|
|
QCOMPARE(paintOnScreenWidget.takePaintedRegions(), paintOnScreenWidget.rect());
|
|
|
|
renderToTextureWidget.update();
|
|
QVERIFY(renderToTextureWidget.waitForPainted());
|
|
QCOMPARE(renderToTextureWidget.takePaintedRegions(), renderToTextureWidget.rect());
|
|
|
|
// Then toggle WA_PaintOnScreen
|
|
paintOnScreenWidget.setAttribute(Qt::WA_PaintOnScreen);
|
|
|
|
// The render-to-texture widget updates fine
|
|
renderToTextureWidget.update();
|
|
QVERIFY(renderToTextureWidget.waitForPainted());
|
|
QCOMPARE(renderToTextureWidget.takePaintedRegions(), renderToTextureWidget.rect());
|
|
|
|
// Updating the paint-on-screen texture widget will not result
|
|
// in a paint event, but should result in an update request.
|
|
paintOnScreenWidget.updateRequests = 0;
|
|
paintOnScreenWidget.update();
|
|
QVERIFY(QTest::qWaitFor([&]{ return paintOnScreenWidget.updateRequests > 0; }));
|
|
|
|
// And should not prevent the render-to-texture widget from receiving updates
|
|
renderToTextureWidget.update();
|
|
QVERIFY(renderToTextureWidget.waitForPainted());
|
|
QCOMPARE(renderToTextureWidget.takePaintedRegions(), renderToTextureWidget.rect());
|
|
}
|
|
|
|
{
|
|
TestWidget paintOnScreenTopLevel;
|
|
paintOnScreenTopLevel.setObjectName("PaintOnScreenTopLevel");
|
|
paintOnScreenTopLevel.setAttribute(Qt::WA_PaintOnScreen);
|
|
|
|
paintOnScreenTopLevel.initialShow();
|
|
|
|
paintOnScreenTopLevel.updateRequests = 0;
|
|
paintOnScreenTopLevel.update();
|
|
QVERIFY(QTest::qWaitFor([&]{ return paintOnScreenTopLevel.updateRequests > 0; }));
|
|
|
|
// Turn off paint on screen and make it a render-to-texture widget.
|
|
// This will lead us into a QWidgetRepaintManager::markDirty() code
|
|
// path that checks updateRequestSent, which is still set from the
|
|
// update above since paint-on-screen handling doesn't reset it.
|
|
paintOnScreenTopLevel.setAttribute(Qt::WA_PaintOnScreen, false);
|
|
QWidgetPrivate::get(&paintOnScreenTopLevel)->setRenderToTexture();
|
|
paintOnScreenTopLevel.update();
|
|
QVERIFY(QTest::qWaitFor([&]{ return paintOnScreenTopLevel.updateRequests > 1; }));
|
|
}
|
|
}
|
|
|
|
#if defined(QT_BUILD_INTERNAL)
|
|
|
|
/*!
|
|
Verify that overlapping children are repainted correctly when
|
|
a widget is moved (via a scroll area) for such a distance that
|
|
none of the old area is still visible. QTBUG-26269
|
|
*/
|
|
void tst_QWidgetRepaintManager::scrollWithOverlap()
|
|
{
|
|
if (QStringList{"android"}.contains(QGuiApplication::platformName()))
|
|
QSKIP("This test fails on Android");
|
|
|
|
class MainWindow : public QWidget
|
|
{
|
|
public:
|
|
MainWindow(QWidget *parent = 0)
|
|
: QWidget(parent, Qt::WindowStaysOnTopHint)
|
|
{
|
|
m_scrollArea = new QScrollArea(this);
|
|
m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
QWidget *w = new QWidget;
|
|
w->setPalette(QPalette(Qt::gray));
|
|
w->setAutoFillBackground(true);
|
|
m_scrollArea->setWidget(w);
|
|
m_scrollArea->resize(500, 100);
|
|
w->resize(5000, 600);
|
|
|
|
m_topWidget = new QWidget(this);
|
|
m_topWidget->setPalette(QPalette(Qt::red));
|
|
m_topWidget->setAutoFillBackground(true);
|
|
m_topWidget->resize(300, 200);
|
|
|
|
resize(600, 300);
|
|
}
|
|
|
|
void resizeEvent(QResizeEvent *e) override
|
|
{
|
|
QWidget::resizeEvent(e);
|
|
// move scroll area and top widget to the center of the main window
|
|
scrollArea()->move((width() - scrollArea()->width()) / 2, (height() - scrollArea()->height()) / 2);
|
|
topWidget()->move((width() - topWidget()->width()) / 2, (height() - topWidget()->height()) / 2);
|
|
}
|
|
|
|
|
|
inline QScrollArea *scrollArea() const { return m_scrollArea; }
|
|
inline QWidget *topWidget() const { return m_topWidget; }
|
|
|
|
private:
|
|
QScrollArea *m_scrollArea;
|
|
QWidget *m_topWidget;
|
|
};
|
|
|
|
MainWindow w;
|
|
w.show();
|
|
|
|
QVERIFY(QTest::qWaitForWindowActive(&w));
|
|
|
|
bool result = compareWidget(w.topWidget());
|
|
// if this fails already, then the system we test on can't compare screenshots from grabbed widgets,
|
|
// and we have to skip this test. Possible reasons are differences in surface formats or DPI, or
|
|
// unrelated bugs in QPlatformBackingStore::toImage or QWidget::grab.
|
|
if (!result)
|
|
QSKIP("Cannot compare QWidget::grab with QScreen::grabWindow on this machine");
|
|
|
|
// scroll the horizontal slider to the right side
|
|
{
|
|
w.scrollArea()->horizontalScrollBar()->setValue(w.scrollArea()->horizontalScrollBar()->maximum());
|
|
QVERIFY(compareWidget(w.topWidget()));
|
|
}
|
|
|
|
// scroll the vertical slider down
|
|
{
|
|
w.scrollArea()->verticalScrollBar()->setValue(w.scrollArea()->verticalScrollBar()->maximum());
|
|
QVERIFY(compareWidget(w.topWidget()));
|
|
}
|
|
|
|
// hide the top widget
|
|
{
|
|
w.topWidget()->hide();
|
|
QVERIFY(compareWidget(w.scrollArea()->viewport()));
|
|
}
|
|
|
|
// scroll the horizontal slider to the left side
|
|
{
|
|
w.scrollArea()->horizontalScrollBar()->setValue(w.scrollArea()->horizontalScrollBar()->minimum());
|
|
QVERIFY(compareWidget(w.scrollArea()->viewport()));
|
|
}
|
|
|
|
// scroll the vertical slider up
|
|
{
|
|
w.scrollArea()->verticalScrollBar()->setValue(w.scrollArea()->verticalScrollBar()->minimum());
|
|
QVERIFY(compareWidget(w.scrollArea()->viewport()));
|
|
}
|
|
}
|
|
|
|
/*!
|
|
This tests QWidgetPrivate::overlappedRegion, which however is only used in the
|
|
QWidgetRepaintManager, so the test is here.
|
|
*/
|
|
void tst_QWidgetRepaintManager::overlappedRegion()
|
|
{
|
|
TestScene scene;
|
|
|
|
if (scene.screen()->availableSize().width() < scene.sizeHint().width()
|
|
|| scene.screen()->availableSize().height() < scene.sizeHint().height()) {
|
|
QSKIP("The screen on this system is too small for this test");
|
|
}
|
|
|
|
scene.show();
|
|
QVERIFY(QTest::qWaitForWindowExposed(&scene));
|
|
|
|
auto overlappedRegion = [](QWidget *widget, bool breakAfterFirst = false){
|
|
auto *priv = QWidgetPrivate::get(widget);
|
|
// overlappedRegion works on parent coordinates (crect, i.e. QWidget::geometry)
|
|
return priv->overlappedRegion(widget->geometry(), breakAfterFirst);
|
|
};
|
|
|
|
// the yellow child is not overlapped
|
|
QVERIFY(overlappedRegion(scene.yellowChild).isEmpty());
|
|
// the green child is partially overlapped by the yellow child, which
|
|
// is at position -50, -50 relative to the green child (and 100x100 large)
|
|
QRegion overlap = overlappedRegion(scene.greenChild);
|
|
QVERIFY(!overlap.isEmpty());
|
|
QCOMPARE(overlap, QRegion(QRect(-50, -50, 100, 100)));
|
|
// the red child is completely obscured by the green child, and partially
|
|
// obscured by the yellow child. How exactly this is divided into rects is
|
|
// irrelevant for the test.
|
|
overlap = overlappedRegion(scene.redChild);
|
|
QVERIFY(!overlap.isEmpty());
|
|
QCOMPARE(overlap.boundingRect(), QRect(-50, -50, 150, 150));
|
|
|
|
// moving the red child out of obscurity
|
|
scene.redChild->move(100, 0);
|
|
overlap = overlappedRegion(scene.redChild);
|
|
QTRY_VERIFY(overlap.isEmpty());
|
|
|
|
// moving the red child down so it's partially behind the bar
|
|
scene.redChild->move(100, 100);
|
|
overlap = overlappedRegion(scene.redChild);
|
|
QTRY_VERIFY(!overlap.isEmpty());
|
|
|
|
// moving the yellow child so it is partially overlapped by the bar
|
|
scene.yellowChild->move(200, 200);
|
|
overlap = overlappedRegion(scene.yellowChild);
|
|
QTRY_VERIFY(!overlap.isEmpty());
|
|
}
|
|
|
|
void tst_QWidgetRepaintManager::fastMove()
|
|
{
|
|
TestScene scene;
|
|
scene.show();
|
|
QVERIFY(QTest::qWaitForWindowExposed(&scene));
|
|
|
|
QWidgetRepaintManager *repaintManager = QWidgetPrivate::get(&scene)->maybeRepaintManager();
|
|
QVERIFY(repaintManager->dirtyRegion().isEmpty());
|
|
|
|
// moving yellow; nothing obscured
|
|
scene.yellowChild->move(QPoint(25, 0));
|
|
QVERIFY(repaintManager->dirtyRegion().isEmpty()); // fast move
|
|
if (m_implementsScroll) {
|
|
QCOMPARE(repaintManager->dirtyWidgetList(), QList<QWidget *>() << &scene);
|
|
QVERIFY(dirtyRegion(scene.yellowChild).isEmpty());
|
|
} else {
|
|
QCOMPARE(repaintManager->dirtyWidgetList(), QList<QWidget *>() << scene.yellowChild << &scene);
|
|
QCOMPARE(dirtyRegion(scene.yellowChild), QRect(0, 0, 100, 100));
|
|
}
|
|
QCOMPARE(dirtyRegion(&scene), QRect(0, 0, 25, 100));
|
|
QTRY_VERIFY(dirtyRegion(&scene).isEmpty());
|
|
QVERIFY(compareWidget(&scene));
|
|
}
|
|
|
|
void tst_QWidgetRepaintManager::moveAccross()
|
|
{
|
|
TestScene scene;
|
|
scene.show();
|
|
QVERIFY(QTest::qWaitForWindowExposed(&scene));
|
|
|
|
QWidgetRepaintManager *repaintManager = QWidgetPrivate::get(&scene)->maybeRepaintManager();
|
|
QVERIFY(repaintManager->dirtyRegion().isEmpty());
|
|
|
|
for (int i = 0; i < 4; ++i) {
|
|
scene.greenChild->move(scene.greenChild->pos() + QPoint(25, 0));
|
|
waitForFlush(&scene);
|
|
}
|
|
QVERIFY(compareWidget(&scene));
|
|
|
|
for (int i = 0; i < 16; ++i) {
|
|
scene.redChild->move(scene.redChild->pos() + QPoint(25, 0));
|
|
waitForFlush(&scene);
|
|
}
|
|
QVERIFY(compareWidget(&scene));
|
|
|
|
for (int i = 0; i < qMin(scene.area->width(), scene.area->height()); i += 25) {
|
|
scene.yellowChild->move(scene.yellowChild->pos() + QPoint(25, 25));
|
|
waitForFlush(&scene);
|
|
}
|
|
QVERIFY(compareWidget(&scene));
|
|
}
|
|
|
|
void tst_QWidgetRepaintManager::moveInOutOverlapped()
|
|
{
|
|
TestScene scene;
|
|
scene.show();
|
|
QVERIFY(QTest::qWaitForWindowExposed(&scene));
|
|
|
|
QWidgetRepaintManager *repaintManager = QWidgetPrivate::get(&scene)->maybeRepaintManager();
|
|
QVERIFY(repaintManager->dirtyRegion().isEmpty());
|
|
|
|
// yellow out
|
|
scene.yellowChild->move(QPoint(-100, 0));
|
|
QVERIFY(!repaintManager->dirtyRegion().isEmpty()); // invalid dest rect
|
|
QVERIFY(repaintManager->dirtyWidgetList().isEmpty());
|
|
QVERIFY(waitForFlush(&scene));
|
|
QVERIFY(compareWidget(&scene));
|
|
|
|
// yellow in, obscured by bar
|
|
scene.yellowChild->move(QPoint(scene.width() / 2, scene.height() / 2));
|
|
QVERIFY(!repaintManager->dirtyRegion().isEmpty()); // invalid source rect
|
|
QVERIFY(repaintManager->dirtyWidgetList().isEmpty());
|
|
QVERIFY(waitForFlush(&scene));
|
|
QVERIFY(compareWidget(&scene));
|
|
|
|
// green out
|
|
scene.greenChild->move(QPoint(-100, 0));
|
|
QVERIFY(!repaintManager->dirtyRegion().isEmpty()); // invalid dest rect
|
|
QVERIFY(repaintManager->dirtyWidgetList().isEmpty());
|
|
QVERIFY(waitForFlush(&scene));
|
|
QVERIFY(compareWidget(&scene));
|
|
|
|
// green back in, obscured by bar
|
|
scene.greenChild->move(QPoint(scene.area->width() / 2 - 50, scene.area->height() / 2 - 50));
|
|
QVERIFY(!repaintManager->dirtyRegion().isEmpty()); // invalid source rect
|
|
QVERIFY(repaintManager->dirtyWidgetList().isEmpty());
|
|
QVERIFY(waitForFlush(&scene));
|
|
QVERIFY(compareWidget(&scene));
|
|
|
|
// red back under green
|
|
scene.redChild->move(scene.greenChild->pos());
|
|
QVERIFY(!repaintManager->dirtyRegion().isEmpty()); // destination rect obscured
|
|
QVERIFY(repaintManager->dirtyWidgetList().isEmpty());
|
|
QVERIFY(waitForFlush(&scene));
|
|
QVERIFY(compareWidget(&scene));
|
|
}
|
|
#endif //# defined(QT_BUILD_INTERNAL)
|
|
|
|
QTEST_MAIN(tst_QWidgetRepaintManager)
|
|
#include "tst_qwidgetrepaintmanager.moc"
|