diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index a053d688d..1545622ae 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -248,8 +248,6 @@ set(QET_SRC_FILES ${QET_DIR}/sources/qet.h ${QET_DIR}/sources/qeticons.cpp ${QET_DIR}/sources/qeticons.h - ${QET_DIR}/sources/palettegraphicsview.cpp - ${QET_DIR}/sources/palettegraphicsview.h ${QET_DIR}/sources/qetpalette.cpp ${QET_DIR}/sources/qetpalette.h ${QET_DIR}/sources/qetstyle.cpp diff --git a/sources/diagram.cpp b/sources/diagram.cpp index 26873b801..aa8e92399 100644 --- a/sources/diagram.cpp +++ b/sources/diagram.cpp @@ -26,7 +26,6 @@ #include "diagramposition.h" #include "factory/elementfactory.h" #include "qetapp.h" -#include "qetpalette.h" #include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h" #include "qetgraphicsitem/conductor.h" #include "qetgraphicsitem/conductortextitem.h" @@ -284,11 +283,10 @@ 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 - * gets softer dots, see QET::Palette::gridDotColor. */ QPen pen; - pen.setColor(QET::Palette::gridDotColor(Diagram::background_color, m_inverted_lightness)); + Diagram::background_color == Qt::black? pen.setColor(Qt::white) + : pen.setColor(Qt::black); pen.setCosmetic(true); p->setPen(pen); diff --git a/sources/diagram.h b/sources/diagram.h index 198323bbc..824b6dc38 100644 --- a/sources/diagram.h +++ b/sources/diagram.h @@ -123,7 +123,6 @@ class Diagram : public QGraphicsScene qreal diagram_qet_version_; bool draw_grid_; - bool m_inverted_lightness = false; bool use_border_; bool draw_guides_; QList m_guides_list; @@ -223,7 +222,6 @@ 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 &guides); @@ -357,18 +355,6 @@ 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 (PaletteGraphicsView 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. diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp index caafeb07a..a0e662306 100644 --- a/sources/diagramview.cpp +++ b/sources/diagramview.cpp @@ -39,9 +39,7 @@ #include "ElementsCollection/xmlelementcollection.h" #include "NameList/nameslist.h" #include "elementdialog.h" -#include #include -#include #include /** @@ -50,7 +48,7 @@ @param parent Le QWidget parent de cette vue de schema */ DiagramView::DiagramView(Diagram *diagram, QWidget *parent) : - PaletteGraphicsView (parent), + QGraphicsView (parent), m_diagram (diagram) { grabGesture(Qt::PinchGesture); @@ -1083,16 +1081,6 @@ bool DiagramView::event(QEvent *e) { return(QGraphicsView::event(e)); } -/** - @brief DiagramView::paintingInverted - Reimplemented from PaletteGraphicsView: tell the diagram it is being - drawn for an inverted display, so it softens its grid. -*/ -void DiagramView::paintingInverted(bool inverted) -{ - m_diagram->setInvertedLightness(inverted); -} - /** @brief DiagramView::paintEvent Reimplemented from QGraphicsView @@ -1100,7 +1088,7 @@ void DiagramView::paintingInverted(bool inverted) */ void DiagramView::paintEvent(QPaintEvent *event) { - PaletteGraphicsView::paintEvent(event); + QGraphicsView::paintEvent(event); if (m_free_rubberbanding && m_free_rubberband.count() >= 3) { diff --git a/sources/diagramview.h b/sources/diagramview.h index 3f1e6a812..0550b01d9 100644 --- a/sources/diagramview.h +++ b/sources/diagramview.h @@ -22,7 +22,7 @@ #include "titleblock/templatelocation.h" #include -#include "palettegraphicsview.h" +#include class Conductor; class Diagram; @@ -35,7 +35,7 @@ class QGestureEvent; This class provides a widget to render an electric diagram in an editable, interactive way. */ -class DiagramView : public PaletteGraphicsView +class DiagramView : public QGraphicsView { Q_OBJECT @@ -84,7 +84,6 @@ class DiagramView : public PaletteGraphicsView ///Set for one call only, by the Escape handler, to let focus leave the view. bool m_releasing_focus = false; void paintEvent(QPaintEvent *event) override; - void paintingInverted(bool inverted) override; void mousePressEvent(QMouseEvent *) override; void mouseMoveEvent(QMouseEvent *) override; void mouseReleaseEvent(QMouseEvent *) override; diff --git a/sources/palettegraphicsview.cpp b/sources/palettegraphicsview.cpp deleted file mode 100644 index d6d19c44d..000000000 --- a/sources/palettegraphicsview.cpp +++ /dev/null @@ -1,182 +0,0 @@ -/* - Copyright 2006-2026 The QElectroTech Team - This file is part of QElectroTech. - - QElectroTech is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 2 of the License, or - (at your option) any later version. - - QElectroTech is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with QElectroTech. If not, see . -*/ -#include "palettegraphicsview.h" - -#include "qetpalette.h" - -#include -#include -#include -#include -#include -#include -#include - -PaletteGraphicsView::PaletteGraphicsView(QWidget *parent) : - QGraphicsView(parent) -{ - qApp->installEventFilter(this); -} - -PaletteGraphicsView::PaletteGraphicsView(QGraphicsScene *scene, QWidget *parent) : - QGraphicsView(scene, parent) -{ - qApp->installEventFilter(this); - listenToScene(scene); -} - -/** - @brief PaletteGraphicsView::setScene - Same as QGraphicsView::setScene, and keeps the scene's updates flowing - (see the class description). -*/ -void PaletteGraphicsView::setScene(QGraphicsScene *scene) -{ - QGraphicsView::setScene(scene); - listenToScene(scene); -} - -/** - @brief PaletteGraphicsView::invertsLightness - @return true when the scene is shown with its lightness inverted, - i.e. when the application palette is dark. The application palette, - not the view's own: a style sheet on an ancestor (the folio tab - widget has one) makes QStyleSheetStyle pin the palette of every - widget under it to the application palette in force when the sheet - was applied, so after a live light/dark switch palette() is stale. -*/ -bool PaletteGraphicsView::invertsLightness() const -{ - return QET::Palette::isDark(QApplication::palette()); -} - -void PaletteGraphicsView::paintingInverted(bool inverted) -{ - Q_UNUSED(inverted) -} - -/** - @brief PaletteGraphicsView::listenToScene - Connect a receiver to the scene's changed() signal, dropping the one - on the previous scene. Any receiver does; this one has nothing to do. - The connection dies with the view. -*/ -void PaletteGraphicsView::listenToScene(QGraphicsScene *scene) -{ - disconnect(m_scene_connection); - m_scene_connection = QMetaObject::Connection(); - if (scene) - m_scene_connection = connect(scene, &QGraphicsScene::changed, - this, [](const QList &) {}); -} - -/** - @brief PaletteGraphicsView::eventFilter - Repaint the whole viewport when the application palette changes. Qt - sends that change to the application object and then repaints only - the widgets whose own palette changed with it, which under a style - sheet is not the case (see invertsLightness()): the scene would then - repaint only what it updates itself, and the viewport around the - sheet would keep the colors of the previous palette. The filter sits - on the application object, the one receiver Qt always notifies. -*/ -bool PaletteGraphicsView::eventFilter(QObject *watched, QEvent *event) -{ - if (watched == qApp && event->type() == QEvent::ApplicationPaletteChange) - viewport()->update(); - return QGraphicsView::eventFilter(watched, event); -} - -/** - @brief PaletteGraphicsView::paintEvent - Paints as QGraphicsView on a light palette, inverted on a dark one. -*/ -void PaletteGraphicsView::paintEvent(QPaintEvent *event) -{ - if (invertsLightness()) - paintInverted(event->rect()); - else - QGraphicsView::paintEvent(event); -} - -/** - @brief PaletteGraphicsView::paintInverted - Render \a area of the viewport into an off-screen image, invert the - lightness of that image between the palette's Base and Text colors and - blit it to the viewport. Inverting the finished rendering turns the - white sheet dark and the black ink light in one pass, and keeps the - hue of colored strokes. - @param area the part of the viewport to repaint, in viewport coordinates -*/ -void PaletteGraphicsView::paintInverted(const QRect &area) -{ - const QRect rect = area.intersected(viewport()->rect()); - if (rect.isEmpty()) - return; - - const qreal ratio = viewport()->devicePixelRatioF(); - QImage buffer(qCeil(rect.width() * ratio), qCeil(rect.height() * ratio), - QImage::Format_RGB32); - buffer.setDevicePixelRatio(ratio); - // render() paints only what the scene draws; what it leaves blank is - // the white sheet, which the inversion turns into the Base color. - buffer.fill(Qt::white); - - QPainter buffer_painter(&buffer); - buffer_painter.setRenderHints(renderHints()); - paintingInverted(true); - render(&buffer_painter, QRectF(QPointF(0, 0), QSizeF(rect.size())), - rect, Qt::IgnoreAspectRatio); - paintingInverted(false); - buffer_painter.end(); - - // The application palette, for the reason given in invertsLightness(). - const QPalette application_palette = QApplication::palette(); - QET::Palette::invertLightness(buffer, application_palette.color(QPalette::Base), - application_palette.color(QPalette::Text)); - - QPainter painter(viewport()); - painter.drawImage(rect.topLeft(), buffer); - drawRubberBand(painter); -} - -/** - @brief PaletteGraphicsView::drawRubberBand - Draw the selection rubber band the way QGraphicsView::paintEvent does. - Rendering the view into an off-screen image skips it, so it is drawn - here instead, after the inversion, in the palette colors. - @param painter a painter on the viewport -*/ -void PaletteGraphicsView::drawRubberBand(QPainter &painter) -{ - const QRect band = rubberBandRect(); - if (band.isNull()) - return; - - QStyleOptionRubberBand option; - option.initFrom(viewport()); - option.rect = band; - option.shape = QRubberBand::Rectangle; - - QStyleHintReturnMask mask; - if (viewport()->style()->styleHint(QStyle::SH_RubberBand_Mask, &option, - viewport(), &mask)) - painter.setClipRegion(mask.region, Qt::IntersectClip); - viewport()->style()->drawControl(QStyle::CE_RubberBand, &option, - &painter, viewport()); -} diff --git a/sources/palettegraphicsview.h b/sources/palettegraphicsview.h deleted file mode 100644 index 9d361da92..000000000 --- a/sources/palettegraphicsview.h +++ /dev/null @@ -1,73 +0,0 @@ -/* - Copyright 2006-2026 The QElectroTech Team - This file is part of QElectroTech. - - QElectroTech is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 2 of the License, or - (at your option) any later version. - - QElectroTech is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with QElectroTech. If not, see . -*/ -#ifndef PALETTE_GRAPHICS_VIEW_H -#define PALETTE_GRAPHICS_VIEW_H - -#include - -class QPainter; - -/** - A QGraphicsView that shows its scene with inverted lightness while its - palette is dark: white becomes the palette's Base color, black its Text - color, and colored strokes keep their hue. The scene itself is left as - drawn, so printing and exporting it still give black on white. On a - light palette the view paints exactly as QGraphicsView does. - - The view paints through QGraphicsView::render() into an image and blits - the inverted image. 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 QGraphicsScene::update() on, every scene update - and every item update would wait for an unrelated repaint. So the view - listens to changed() on every scene it is given, which makes the scene - clear the flag before it emits. Set the scene through this class, not - through a QGraphicsView pointer. -*/ -class PaletteGraphicsView : public QGraphicsView -{ - Q_OBJECT - - public: - explicit PaletteGraphicsView(QWidget *parent = nullptr); - explicit PaletteGraphicsView(QGraphicsScene *scene, QWidget *parent = nullptr); - - void setScene(QGraphicsScene *scene); - bool invertsLightness() const; - - protected: - bool eventFilter(QObject *watched, QEvent *event) override; - void paintEvent(QPaintEvent *event) override; - /** - Called with true right before the scene is rendered for an - inverted display and with false right after, so a scene can - adapt what it draws (a softer grid, for instance). Does - nothing by default. - */ - virtual void paintingInverted(bool inverted); - - private: - void listenToScene(QGraphicsScene *scene); - void paintInverted(const QRect &area); - void drawRubberBand(QPainter &painter); - - QMetaObject::Connection m_scene_connection; -}; - -#endif diff --git a/sources/qetpalette.cpp b/sources/qetpalette.cpp index 9cf56c7cd..63c5f98bb 100644 --- a/sources/qetpalette.cpp +++ b/sources/qetpalette.cpp @@ -18,9 +18,7 @@ #include "qetpalette.h" #include -#include #include - #include namespace { @@ -77,57 +75,6 @@ bool QET::Palette::isDark(const QPalette &palette) return palette.color(QPalette::Active, QPalette::Window).lightness() < 128; } -void QET::Palette::invertLightness(QImage &image, const QColor &sheet, - const QColor &ink) -{ - if (image.format() != QImage::Format_RGB32) - image.convertTo(QImage::Format_RGB32); - - // One table per channel maps the inverted value (0 = was white, - // 255 = was black) onto the sheet..ink span. - uchar red_of[256], green_of[256], blue_of[256]; - for (int v = 0; v < 256; ++v) { - red_of[v] = uchar(sheet.red() + (ink.red() - sheet.red()) * v / 255); - green_of[v] = uchar(sheet.green() + (ink.green() - sheet.green()) * v / 255); - blue_of[v] = uchar(sheet.blue() + (ink.blue() - sheet.blue()) * v / 255); - } - - /* Inverting the lightness of an HSL color while keeping its hue and - saturation leaves the distance between the highest and the lowest - channel unchanged, so it comes down to one offset per pixel: - c + 255 - max - min. The offset turns the highest channel into - 255 - min and the lowest into 255 - max, so no channel can leave - the 0..255 range and no clamping is needed. The loop runs on every - repaint of a folio, hence the plain integer arithmetic. */ - for (int y = 0; y < image.height(); ++y) { - quint32 *line = reinterpret_cast(image.scanLine(y)); - for (int x = 0, width = image.width(); x < width; ++x) { - const quint32 pixel = line[x]; - const int red = (pixel >> 16) & 0xff; - const int green = (pixel >> 8) & 0xff; - const int blue = pixel & 0xff; - int highest = red > green ? red : green; - int lowest = red < green ? red : green; - if (blue > highest) highest = blue; - if (blue < lowest) lowest = blue; - const int offset = 255 - highest - lowest; - line[x] = 0xff000000u - | (quint32(red_of[red + offset]) << 16) - | (quint32(green_of[green + offset]) << 8) - | quint32(blue_of[blue + offset]); - } - } -} - -QColor QET::Palette::gridDotColor(const QColor &sheet, bool inverted) -{ - if (sheet == QColor(Qt::black)) - return Qt::white; - if (inverted) - return QColor(sheet.red() * 2 / 3, sheet.green() * 2 / 3, sheet.blue() * 2 / 3); - return Qt::black; -} - double QET::Palette::contrastRatio(const QColor &a, const QColor &b) { double lighter = relativeLuminance(a); diff --git a/sources/qetpalette.h b/sources/qetpalette.h index 536b660d2..0df5bb3a8 100644 --- a/sources/qetpalette.h +++ b/sources/qetpalette.h @@ -22,7 +22,6 @@ #include #include -class QImage; class QStyle; /** @@ -55,28 +54,6 @@ namespace QET { */ bool isDark(const QPalette &palette); - /** - Invert the lightness of every pixel of \a image, keeping its hue - and saturation, then stretch the result between two colors: pure - white becomes \a sheet, pure black becomes \a ink, and a red - line stays red, only lighter. Made for a rendering of a white - sheet that has to read on a dark palette, with sheet = Base and - ink = Text. The image must be opaque; an image in another format - is converted to RGB32 first. - */ - void invertLightness(QImage &image, const QColor &sheet = Qt::black, - const QColor &ink = Qt::white); - - /** - The color of the grid dots on a sheet of color \a sheet: black, - or white on a black sheet. With \a inverted the sheet is about to - be shown with its lightness inverted (PaletteGraphicsView), where - black dots would come out as bright as the ink; the dots are then - a third of the way from the sheet color to black, which shows as - a soft gray. - */ - QColor gridDotColor(const QColor &sheet, bool inverted); - /** WCAG 2 contrast ratio between two opaque colors, from 1 (equal) to 21 (black on white). Normal text needs at least 4.5, large diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 76eae00f7..603cc52e9 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -95,8 +95,6 @@ add_executable( tst_qetpalette tst_qetpalette.cpp inkcontrast.h - ${QET_DIR}/sources/palettegraphicsview.cpp - ${QET_DIR}/sources/palettegraphicsview.h ${QET_DIR}/sources/qetpalette.cpp ${QET_DIR}/sources/qetpalette.h ${QET_DIR}/sources/ElementsCollection/elementpreviewdelegate.cpp diff --git a/tests/qttest/tst_qetpalette.cpp b/tests/qttest/tst_qetpalette.cpp index 4ced9d5a1..4aea6d76d 100644 --- a/tests/qttest/tst_qetpalette.cpp +++ b/tests/qttest/tst_qetpalette.cpp @@ -19,9 +19,6 @@ #include #include #include -#include -#include -#include #include #include #include @@ -32,12 +29,10 @@ #include #include #include -#include #include #include "inkcontrast.h" #include "ElementsCollection/elementpreviewdelegate.h" -#include "palettegraphicsview.h" #include "qetpalette.h" using QET::Palette::contrastRatio; @@ -75,15 +70,6 @@ class tst_qetpalette : public QObject void invertedLightnessKeepsHueAndAlpha(); void elementPreviewReadsOnBothPalettes(); void previewDelegateAdaptsLineArtOnly(); - void invertLightnessMapsSheetAndInk(); - void invertedViewReadsOnDarkSheet(); - void invertLightnessSpeed(); - void gridDotColorSoftensInvertedDots(); - void paletteViewFollowsThePalette(); - void paletteViewKeepsSceneUpdatesFlowing(); - void paletteViewDrawsTheRubberBand(); - void paletteViewFollowsTheApplicationUnderAStyleSheet(); - void paletteViewFillsWhatTheSceneLeavesBlank(); private: static void addPaletteRows(); @@ -451,370 +437,6 @@ void tst_qetpalette::previewDelegateAdaptsLineArtOnly() QVERIFY2(blue, "the colored icon lost its color"); } -/** - The lightness inversion sends white to the sheet color and black to - the ink color, lands a mid gray between the two, and keeps the hue of - a colored line. Without sheet and ink it is a plain inversion. -*/ -void tst_qetpalette::invertLightnessMapsSheetAndInk() -{ - const QColor sheet(30, 30, 30); - const QColor ink(220, 220, 220); - QImage image(4, 1, QImage::Format_ARGB32); - image.setPixelColor(0, 0, Qt::white); - image.setPixelColor(1, 0, Qt::black); - image.setPixelColor(2, 0, QColor(128, 128, 128)); - image.setPixelColor(3, 0, QColor(200, 0, 0)); - QImage plain = image; - - QET::Palette::invertLightness(image, sheet, ink); - QCOMPARE(image.format(), QImage::Format_RGB32); - QCOMPARE(image.pixelColor(0, 0), sheet); - QCOMPARE(image.pixelColor(1, 0), ink); - const int middle = (sheet.red() + ink.red()) / 2; - QVERIFY(qAbs(image.pixelColor(2, 0).red() - middle) <= 2); - const QColor red = image.pixelColor(3, 0); - QCOMPARE(red.hslHue(), 0); - QVERIFY2(red.hslSaturationF() > 0.5, qPrintable(red.name())); - QVERIFY2(red.lightness() > QColor(200, 0, 0).lightness(), qPrintable(red.name())); - - QET::Palette::invertLightness(plain); - QCOMPARE(plain.pixelColor(0, 0), QColor(Qt::black)); - QCOMPARE(plain.pixelColor(1, 0), QColor(Qt::white)); - QCOMPARE(plain.pixelColor(3, 0), QColor(255, 55, 55)); -} - -/** - A view rendered the way DiagramView does it on a dark palette: a part - of the viewport goes into an image, which is inverted between the - palette's Base and Text. The white sheet comes out as Base, black - lines read at text contrast, and a blue box is still blue. -*/ -void tst_qetpalette::invertedViewReadsOnDarkSheet() -{ - QGraphicsScene scene(0, 0, 200, 120); - scene.setBackgroundBrush(Qt::white); - scene.addLine(10, 60, 190, 60, QPen(Qt::black, 2)); - scene.addRect(20, 20, 40, 20, QPen(Qt::NoPen), QBrush(QColor(30, 96, 176))); - - QGraphicsView view(&scene); - view.setFrameShape(QFrame::NoFrame); - view.setAlignment(Qt::AlignLeft | Qt::AlignTop); - view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - view.resize(200, 120); - view.show(); - QVERIFY(QTest::qWaitForWindowExposed(&view)); - - // A part of the viewport that is not at its origin, as a partial - // repaint after a scroll would be. - const QRect area(10, 10, 100, 60); - QImage buffer(area.size(), QImage::Format_RGB32); - QPainter painter(&buffer); - view.render(&painter, QRectF(QPointF(0, 0), QSizeF(area.size())), area); - painter.end(); - - const QPalette dark = QET::Palette::fusionDark(); - const QColor base = dark.color(QPalette::Active, QPalette::Base); - const QColor text = dark.color(QPalette::Active, QPalette::Text); - QET::Palette::invertLightness(buffer, base, text); - - QHash histogram; - for (int y = 0; y < buffer.height(); ++y) - for (int x = 0; x < buffer.width(); ++x) - ++histogram[buffer.pixel(x, y)]; - QRgb dominant = 0; - for (auto it = histogram.cbegin(); it != histogram.cend(); ++it) - if (it.value() > histogram.value(dominant)) dominant = it.key(); - QCOMPARE(QColor(dominant), base); - - const double contrast = inkContrast(buffer, buffer.rect()); - QVERIFY2(contrast >= QET::Palette::contrastRatio(base, text) - 0.5, - qPrintable(QString("ink reads %1:1 on the dark sheet").arg(contrast))); - - // The box at scene (20..60, 20..40) sits at (10..50, 10..30) in the buffer. - const QColor box = buffer.pixelColor(30, 20); - QVERIFY2(box.hslSaturationF() > 0.3 && box.blue() > box.red() + 60, - qPrintable(QString("the blue box became %1").arg(box.name()))); -} - -namespace { - /** - A PaletteGraphicsView that counts its paints and records the - paintingInverted() calls it receives. - */ - class ProbeView : public PaletteGraphicsView - { - public: - int paints = 0; - QList paint_rects; - QList inverted_calls; - using PaletteGraphicsView::PaletteGraphicsView; - /// True when a paint since index \a from covered the whole viewport. - bool fullyRepaintedSince(int from) const - { - for (int i = from; i < paint_rects.size(); ++i) - if (paint_rects.at(i).contains(viewport()->rect())) - return true; - return false; - } - protected: - void paintEvent(QPaintEvent *event) override - { - ++paints; - paint_rects << event->rect(); - PaletteGraphicsView::paintEvent(event); - } - void paintingInverted(bool inverted) override - { - inverted_calls << inverted; - } - }; - - /** - A small folio: a white sheet with a black line and a red box. - Returns the box, which is selectable. - */ - QGraphicsRectItem *fillSheet(QGraphicsScene &scene) - { - scene.setSceneRect(0, 0, 200, 120); - scene.setBackgroundBrush(Qt::white); - scene.addLine(10, 60, 190, 60, QPen(Qt::black, 2)); - QGraphicsRectItem *box = scene.addRect(20, 20, 40, 20, QPen(Qt::NoPen), QBrush(QColor(200, 0, 0))); - box->setFlag(QGraphicsItem::ItemIsSelectable); - return box; - } - - /// The view sized to its scene, without frame or scroll bars. - void showAsSheet(QGraphicsView &view) - { - view.setFrameShape(QFrame::NoFrame); - view.setAlignment(Qt::AlignLeft | Qt::AlignTop); - view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - view.resize(200, 120); - view.show(); - } - - /// The most frequent color of an image: the sheet. - QColor sheetColor(const QImage &image) - { - QHash histogram; - for (int y = 0; y < image.height(); ++y) - for (int x = 0; x < image.width(); ++x) - ++histogram[image.pixel(x, y)]; - QRgb best = 0; - for (auto it = histogram.cbegin(); it != histogram.cend(); ++it) - if (it.value() > histogram.value(best)) best = it.key(); - return QColor(best); - } -} - -/** - Grid dots are black, white on a black sheet, and a third of the way - from the sheet color to black when the sheet is about to be shown - inverted, so that they do not come out as bright as the ink. -*/ -void tst_qetpalette::gridDotColorSoftensInvertedDots() -{ - QCOMPARE(QET::Palette::gridDotColor(Qt::white, false), QColor(Qt::black)); - QCOMPARE(QET::Palette::gridDotColor(Qt::white, true), QColor(170, 170, 170)); - QCOMPARE(QET::Palette::gridDotColor(Qt::darkGray, true), QColor(85, 85, 85)); - QCOMPARE(QET::Palette::gridDotColor(Qt::black, false), QColor(Qt::white)); - QCOMPARE(QET::Palette::gridDotColor(Qt::black, true), QColor(Qt::white)); -} - -/** - On a light palette the view shows the sheet as drawn and never tells - the scene it inverts. On a dark palette, set while the view is - showing, the sheet comes out as Base, the black line at text contrast, - the red box still red, and the scene hears paintingInverted(true) - before and (false) after. Back on a light palette the sheet is white - again. -*/ -void tst_qetpalette::paletteViewFollowsThePalette() -{ - QApplication::setStyle(QStyleFactory::create("Fusion")); - QApplication::setPalette(QET::Palette::fusionLight()); - - QGraphicsScene scene; - fillSheet(scene); - ProbeView view(&scene); - showAsSheet(view); - QVERIFY(QTest::qWaitForWindowExposed(&view)); - - const QImage light = view.viewport()->grab().toImage(); - QCOMPARE(sheetColor(light), QColor(Qt::white)); - QVERIFY(view.inverted_calls.isEmpty()); - - QApplication::setPalette(QET::Palette::fusionDark()); - QTRY_VERIFY(view.invertsLightness()); - const QImage dark = view.viewport()->grab().toImage(); - const QColor base = QET::Palette::fusionDark().color(QPalette::Active, QPalette::Base); - const QColor text = QET::Palette::fusionDark().color(QPalette::Active, QPalette::Text); - QCOMPARE(sheetColor(dark), base); - const double contrast = inkContrast(dark, dark.rect()); - QVERIFY2(contrast >= QET::Palette::contrastRatio(base, text) - 0.5, - qPrintable(QString("ink reads %1:1 on the dark sheet").arg(contrast))); - const QColor box = dark.pixelColor(40, 30); - QVERIFY2(box.hslHue() == 0 && box.hslSaturationF() > 0.3 && box.red() > box.blue() + 60, - qPrintable(QString("the red box became %1").arg(box.name()))); - QVERIFY(!view.inverted_calls.isEmpty()); - QCOMPARE(view.inverted_calls.first(), true); - QCOMPARE(view.inverted_calls.last(), false); - QCOMPARE(view.inverted_calls.count(true), view.inverted_calls.count(false)); - - QApplication::setPalette(QET::Palette::fusionLight()); - QTRY_VERIFY(!view.invertsLightness()); - QCOMPARE(sheetColor(view.viewport()->grab().toImage()), QColor(Qt::white)); -} - -/** - 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. On - a dark palette the view paints through render() instead, so it listens - to the scene's changed() signal, which makes the scene clear the flag - before it emits. Three whole-scene updates and a selection must each - repaint the view, with the scene set after construction as - DiagramView does it. -*/ -void tst_qetpalette::paletteViewKeepsSceneUpdatesFlowing() -{ - QApplication::setStyle(QStyleFactory::create("Fusion")); - QApplication::setPalette(QET::Palette::fusionDark()); - - QGraphicsScene scene; - QGraphicsRectItem *box = fillSheet(scene); - ProbeView view; - view.setScene(&scene); - showAsSheet(view); - QVERIFY(QTest::qWaitForWindowExposed(&view)); - QTRY_VERIFY(view.paints >= 1); - QVERIFY(view.invertsLightness()); - - 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; - box->setSelected(true); - QTRY_VERIFY2(view.paints > before, "the selection change was dropped"); -} - -/** - render() skips Qt's selection rubber band, so the view draws it after - the inversion: while a drag on the sheet is in progress, the dragged - area no longer shows the bare sheet. -*/ -void tst_qetpalette::paletteViewDrawsTheRubberBand() -{ - QApplication::setStyle(QStyleFactory::create("Fusion")); - QApplication::setPalette(QET::Palette::fusionDark()); - - QGraphicsScene scene; - fillSheet(scene); - ProbeView view(&scene); - view.setDragMode(QGraphicsView::RubberBandDrag); - showAsSheet(view); - QVERIFY(QTest::qWaitForWindowExposed(&view)); - const QColor base = QET::Palette::fusionDark().color(QPalette::Active, QPalette::Base); - QCOMPARE(view.viewport()->grab().toImage().pixelColor(170, 100), base); - - QTest::mousePress(view.viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(120, 80)); - QTest::mouseMove(view.viewport(), QPoint(190, 115)); - QTRY_VERIFY(!view.rubberBandRect().isNull()); - const QColor inside = view.viewport()->grab().toImage().pixelColor(170, 100); - QVERIFY2(inside != base, qPrintable(QString("no rubber band drawn, pixel is %1").arg(inside.name()))); - QTest::mouseRelease(view.viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(190, 115)); -} - -/** - A style sheet on an ancestor pins the palette of every widget under it - to the application palette in force when the sheet was applied - (QStyleSheetStyle keeps its own copy; the folio tab widget carries - such a sheet). The view's own palette() is therefore stale after a - live switch, and the view must follow the application palette - instead: inside a tab widget with a style sheet, switching the - application to dark and back still changes the sheet. Qt also skips - the repaint of a widget whose palette did not change, so the view - has to repaint its whole viewport by itself on each switch, without - anyone asking for a rendering: otherwise the area around the sheet - keeps the old colors. -*/ -void tst_qetpalette::paletteViewFollowsTheApplicationUnderAStyleSheet() -{ - QApplication::setStyle(QStyleFactory::create("Fusion")); - QApplication::setPalette(QET::Palette::fusionLight()); - - QGraphicsScene scene; - fillSheet(scene); - QTabWidget tabs; - tabs.setStyleSheet("QTabBar::scroller {width: 0px;}"); // as sources/projectview.cpp - auto *view = new ProbeView(&scene); - view->setFrameShape(QFrame::NoFrame); - view->setAlignment(Qt::AlignLeft | Qt::AlignTop); - tabs.addTab(view, "folio"); - tabs.resize(320, 240); - tabs.show(); - QVERIFY(QTest::qWaitForWindowExposed(&tabs)); - QCOMPARE(sheetColor(view->viewport()->grab().toImage()), QColor(Qt::white)); - - int since = view->paint_rects.size(); - QApplication::setPalette(QET::Palette::fusionDark()); - QTRY_VERIFY(view->invertsLightness()); - QTRY_VERIFY2(view->fullyRepaintedSince(since), "no full repaint after the switch to dark"); - const QColor base = QET::Palette::fusionDark().color(QPalette::Active, QPalette::Base); - QCOMPARE(sheetColor(view->viewport()->grab().toImage()), base); - - since = view->paint_rects.size(); - QApplication::setPalette(QET::Palette::fusionLight()); - QTRY_VERIFY(!view->invertsLightness()); - QTRY_VERIFY2(view->fullyRepaintedSince(since), "no full repaint after the switch to light"); - QCOMPARE(sheetColor(view->viewport()->grab().toImage()), QColor(Qt::white)); -} - -/** - render() paints only what the scene draws. A scene without a - background brush leaves the rest of the buffer untouched, so the view - fills it first: on the dark palette the blank area comes out as Base, - not as whatever the memory held. -*/ -void tst_qetpalette::paletteViewFillsWhatTheSceneLeavesBlank() -{ - QApplication::setStyle(QStyleFactory::create("Fusion")); - QApplication::setPalette(QET::Palette::fusionDark()); - - QGraphicsScene scene(0, 0, 200, 120); - scene.addLine(10, 60, 190, 60, QPen(Qt::black, 2)); - ProbeView view(&scene); - showAsSheet(view); - QVERIFY(QTest::qWaitForWindowExposed(&view)); - - const QImage image = view.viewport()->grab().toImage(); - const QColor base = QET::Palette::fusionDark().color(QPalette::Active, QPalette::Base); - QCOMPARE(sheetColor(image), base); - QCOMPARE(image.pixelColor(100, 20), base); - QCOMPARE(image.pixelColor(100, 100), base); -} - -/** - The inversion runs on every repaint of the folio, so a 4K viewport - has to cost a few milliseconds. Reported, not asserted: the bound - depends on the build box. -*/ -void tst_qetpalette::invertLightnessSpeed() -{ - QImage image(3840, 2000, QImage::Format_RGB32); - image.fill(Qt::white); - QBENCHMARK { - QET::Palette::invertLightness(image, QColor(30, 30, 30), QColor(220, 220, 220)); - } -} - int main(int argc, char **argv) { // Widgets are painted for real, on Qt's offscreen platform so the test