Keep scene updates flowing on the dark canvas and soften its grid

On a dark palette the folio view paints through QGraphicsView::render()
instead of onto its viewport. In that case QGraphicsView never clears
the scene's "update everything" flag, and while the flag is set every
further QGraphicsScene::update() and item update is dropped: from the
second Diagram::update() on, the grid toggle, the white/gray toggle and
even a selection waited for an unrelated repaint. With a receiver on
QGraphicsScene::changed() the scene clears the flag before it emits, so
DiagramView now connects an empty receiver in its constructor.

While the view paints for inversion, Diagram draws the grid dots a
third of the way from the sheet color to black, so they come out as a
soft gray on the dark sheet instead of as bright as the ink. Printing
and export never take that path.

Test in tst_qetpalette: sceneUpdatesReachARenderedView.
This commit is contained in:
Jeff Patterson
2026-09-19 14:15:29 -05:00
parent 85dc638a42
commit b8c9e670c4
4 changed files with 102 additions and 2 deletions
+12 -2
View File
@@ -283,10 +283,20 @@ void Diagram::drawBackground(QPainter *p, const QRectF &r) {
* if background color is black,
* then grid spots shall be white,
* else they shall be black in color.
* A view that shows the sheet with its lightness inverted
* would turn black dots as bright as the ink, so it gets
* dots a third of the way from the sheet color to black,
* which come out as a soft gray.
*/
QPen pen;
Diagram::background_color == Qt::black? pen.setColor(Qt::white)
: pen.setColor(Qt::black);
if (Diagram::background_color == Qt::black)
pen.setColor(Qt::white);
else if (m_inverted_lightness)
pen.setColor(QColor(Diagram::background_color.red() * 2 / 3,
Diagram::background_color.green() * 2 / 3,
Diagram::background_color.blue() * 2 / 3));
else
pen.setColor(Qt::black);
pen.setCosmetic(true);
p->setPen(pen);
+14
View File
@@ -123,6 +123,7 @@ class Diagram : public QGraphicsScene
qreal diagram_qet_version_;
bool draw_grid_;
bool m_inverted_lightness = false;
bool use_border_;
bool draw_guides_;
QList<Diagram::Guide> m_guides_list;
@@ -222,6 +223,7 @@ class Diagram : public QGraphicsScene
ExportProperties applyProperties(const ExportProperties &);
void setDisplayGrid(bool);
bool displayGrid();
void setInvertedLightness(bool);
void setDisplayGuides(bool);
bool displayGuides();
void updateProjectGuides(const QList<GuideProperties> &guides);
@@ -355,6 +357,18 @@ inline void Diagram::setDisplayGrid(bool dg) {
draw_grid_ = dg;
}
/**
@brief Diagram::setInvertedLightness
Tell the diagram whether the view painting it will show the result
with its lightness inverted (DiagramView::paintInverted on a dark
palette). drawBackground draws a softer grid in that case. Printing
and export never set this.
@param inverted
*/
inline void Diagram::setInvertedLightness(bool inverted) {
m_inverted_lightness = inverted;
}
/**
@brief Diagram::displayGrid
@return draw_grid_ true if the grid is drawn, false otherwise.
+13
View File
@@ -108,6 +108,17 @@ DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
connect(m_diagram, &Diagram::showDiagram, this, &DiagramView::showDiagram);
connect(m_diagram, &QGraphicsScene::sceneRectChanged, this, &DiagramView::adjustSceneRect);
/* On a dark palette this view paints the scene into an image (see
* paintInverted). QGraphicsView delivers scene updates straight to
* its viewport when nobody listens to QGraphicsScene::changed(),
* and in that mode the scene clears its "update everything" flag
* only when the items are painted straight onto the viewport,
* which never happens here: from the second update on, grid and
* background toggles and even a selection would wait for an
* unrelated repaint. With a receiver connected the scene sends its
* updates through the signal, and clears the flag before it
* emits. Any receiver does; this one has nothing to do. */
connect(m_diagram, &QGraphicsScene::changed, this, [](const QList<QRectF> &) {});
connect(&(m_diagram -> border_and_titleblock), &BorderTitleBlock::informationChanged, this, &DiagramView::updateWindowTitle);
connect(diagram, &Diagram::findElementRequired, this, &DiagramView::findElementRequired);
@@ -1120,8 +1131,10 @@ void DiagramView::paintInverted(const QRect &area)
QPainter buffer_painter(&buffer);
buffer_painter.setRenderHints(renderHints());
m_diagram->setInvertedLightness(true);
render(&buffer_painter, QRectF(QPointF(0, 0), QSizeF(rect.size())),
rect, Qt::IgnoreAspectRatio);
m_diagram->setInvertedLightness(false);
buffer_painter.end();
QET::Palette::invertLightness(buffer, palette().color(QPalette::Base),
+63
View File
@@ -19,6 +19,7 @@
#include <QApplication>
#include <QCheckBox>
#include <QComboBox>
#include <QGraphicsItem>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QHBoxLayout>
@@ -75,6 +76,7 @@ class tst_qetpalette : public QObject
void invertLightnessMapsSheetAndInk();
void invertedViewReadsOnDarkSheet();
void invertLightnessSpeed();
void sceneUpdatesReachARenderedView();
private:
static void addPaletteRows();
@@ -529,6 +531,67 @@ void tst_qetpalette::invertedViewReadsOnDarkSheet()
qPrintable(QString("the blue box became %1").arg(box.name())));
}
namespace {
/**
A view that paints the way DiagramView does on a dark palette:
the exposed rectangle goes through QGraphicsView::render() into an
image, which is then blitted, so Qt never paints the items straight
onto the viewport.
*/
class RenderedView : public QGraphicsView
{
public:
int paints = 0;
using QGraphicsView::QGraphicsView;
protected:
void paintEvent(QPaintEvent *event) override
{
++paints;
const QRect rect = event->rect().intersected(viewport()->rect());
QImage buffer(rect.size(), QImage::Format_RGB32);
QPainter buffer_painter(&buffer);
render(&buffer_painter, QRectF(QPointF(0, 0), QSizeF(rect.size())), rect);
buffer_painter.end();
QPainter painter(viewport());
painter.drawImage(rect.topLeft(), buffer);
}
};
}
/**
QGraphicsView clears the scene's "update everything" flag only when
it paints the items straight onto its viewport, and while the flag is
set every further QGraphicsScene::update() and item update is dropped.
A view that paints through render() therefore needs a receiver on
QGraphicsScene::changed(), which makes the scene clear the flag before
it emits. This checks that with the receiver, three whole-scene
updates and a selection each repaint the view.
*/
void tst_qetpalette::sceneUpdatesReachARenderedView()
{
QGraphicsScene scene(0, 0, 100, 100);
QGraphicsRectItem *item = scene.addRect(10, 10, 30, 30, QPen(Qt::black), QBrush(Qt::white));
item->setFlag(QGraphicsItem::ItemIsSelectable);
QObject::connect(&scene, &QGraphicsScene::changed, &scene, [](const QList<QRectF> &) {});
RenderedView view(&scene);
view.resize(120, 120);
view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view));
QTRY_VERIFY(view.paints >= 1);
for (int round = 1; round <= 3; ++round)
{
const int before = view.paints;
scene.update();
QTRY_VERIFY2(view.paints > before, qPrintable(QString("scene update %1 was dropped").arg(round)));
}
const int before = view.paints;
item->setSelected(true);
QTRY_VERIFY2(view.paints > before, "the selection change was dropped");
}
/**
The inversion runs on every repaint of the folio, so a 4K viewport
has to cost a few milliseconds. Reported, not asserted: the bound