mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-07 20:34:14 +02:00
Merge pull request #1349 from saschbe/fix/image-dark-colours
Preserve imported image colours by default in dark mode
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#ifndef DARK_IMAGE_RENDERING_H
|
||||
#define DARK_IMAGE_RENDERING_H
|
||||
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
#include <functional>
|
||||
|
||||
namespace DarkImageRendering {
|
||||
// View-local raster composition boundary. Qt still paints the scene in
|
||||
// its native order, with its own transforms, clipping and opacity.
|
||||
// A different painter (printing, export, another view or an item cache)
|
||||
// cannot consume this display-only context.
|
||||
struct Context {
|
||||
QPainter *painter;
|
||||
bool invertScene;
|
||||
std::function<void(bool, const QRect &)> flush;
|
||||
};
|
||||
inline thread_local Context *current = nullptr;
|
||||
class Scope {
|
||||
Context *previous;
|
||||
public:
|
||||
explicit Scope(Context *context) : previous(current) { current = context; }
|
||||
~Scope() { current = previous; }
|
||||
Scope(const Scope &) = delete;
|
||||
Scope &operator=(const Scope &) = delete;
|
||||
};
|
||||
inline void paintPixmap(QPainter *painter, const QPixmap &pixmap, bool adapt)
|
||||
{
|
||||
Context *context = current;
|
||||
const bool boundary = context && context->painter == painter && adapt != context->invertScene;
|
||||
if (boundary) context->flush(context->invertScene, QRect());
|
||||
painter->drawPixmap(pixmap.rect(), pixmap);
|
||||
if (boundary) {
|
||||
// Only the raster was drawn since the preceding flush. Avoid a
|
||||
// whole-viewport copy for a small logo; include filtering/AA edges.
|
||||
const QRect area = painter->worldTransform().mapRect(QRectF(pixmap.rect())).toAlignedRect().adjusted(-2,-2,2,2);
|
||||
context->flush(adapt, area);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "palettegraphicsview.h"
|
||||
|
||||
#include "qetpalette.h"
|
||||
#include "darkimagerendering.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QEvent>
|
||||
@@ -99,12 +100,13 @@ bool PaletteGraphicsView::eventFilter(QObject *watched, QEvent *event)
|
||||
*/
|
||||
void PaletteGraphicsView::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
if (invertsLightness() && !customBackgroundColor())
|
||||
if (invertsLightness())
|
||||
{
|
||||
paintInverted(event);
|
||||
return;
|
||||
}
|
||||
m_buffer = QImage();
|
||||
m_composite = QImage();
|
||||
QGraphicsView::paintEvent(event);
|
||||
}
|
||||
|
||||
@@ -130,7 +132,7 @@ void PaletteGraphicsView::paintInverted(QPaintEvent *event)
|
||||
const QSize size(qCeil(viewport()->width() * ratio), qCeil(viewport()->height() * ratio));
|
||||
if (m_buffer.size() != size || m_buffer.devicePixelRatio() != ratio)
|
||||
{
|
||||
m_buffer = QImage(size, QImage::Format_RGB32);
|
||||
m_buffer = QImage(size, QImage::Format_ARGB32_Premultiplied);
|
||||
m_buffer.setDevicePixelRatio(ratio);
|
||||
}
|
||||
|
||||
@@ -141,9 +143,13 @@ void PaletteGraphicsView::paintInverted(QPaintEvent *event)
|
||||
m_buffer_painter.setClipRect(exposed);
|
||||
m_buffer_painter.setRenderHints(renderHints());
|
||||
m_buffer_painter.setWorldTransform(viewportTransform());
|
||||
m_exposed = exposed;
|
||||
m_layered = false;
|
||||
DarkImageRendering::Context context{&m_buffer_painter, !customBackgroundColor(), [this](bool invert, const QRect &bounds) { flushLayer(invert, bounds); }};
|
||||
DarkImageRendering::Scope rasterScope(&context);
|
||||
|
||||
m_inverting = true;
|
||||
paintingInverted(true);
|
||||
paintingInverted(!customBackgroundColor());
|
||||
const OptimizationFlags flags = optimizationFlags();
|
||||
setOptimizationFlag(QGraphicsView::IndirectPainting, true);
|
||||
QGraphicsView::paintEvent(event);
|
||||
@@ -151,10 +157,44 @@ void PaletteGraphicsView::paintInverted(QPaintEvent *event)
|
||||
paintingInverted(false);
|
||||
m_inverting = false;
|
||||
|
||||
if (m_layered) flushLayer(!customBackgroundColor());
|
||||
m_buffer_painter.end();
|
||||
blitInverted(exposed);
|
||||
}
|
||||
|
||||
void PaletteGraphicsView::flushLayer(bool invert, const QRect &bounds)
|
||||
{
|
||||
const QRect area = bounds.isNull() ? m_exposed : bounds.intersected(m_exposed);
|
||||
if (area.isEmpty()) return;
|
||||
const qreal ratio = m_buffer.devicePixelRatio();
|
||||
const QRect pixels = QRectF(area.topLeft() * ratio, area.size() * ratio).toAlignedRect();
|
||||
QImage layer = m_buffer.copy(pixels);
|
||||
layer.setDevicePixelRatio(ratio);
|
||||
if (invert) {
|
||||
const QPalette palette = QApplication::palette();
|
||||
QET::Palette::invertLightnessLayer(layer, palette.color(QPalette::Base), palette.color(QPalette::Text));
|
||||
}
|
||||
if (!m_layered) {
|
||||
if (m_composite.size() != m_buffer.size() || m_composite.devicePixelRatio() != ratio) {
|
||||
m_composite = QImage(m_buffer.size(), QImage::Format_ARGB32_Premultiplied);
|
||||
m_composite.setDevicePixelRatio(ratio);
|
||||
}
|
||||
m_composite.fill(Qt::transparent);
|
||||
m_layered = true;
|
||||
}
|
||||
QPainter composite(&m_composite);
|
||||
composite.drawImage(QPointF(pixels.topLeft()) / ratio, layer);
|
||||
// Keep Qt's active painter and its saved state stack intact. Replacing
|
||||
// or restarting it here would lose ancestor clipping and opacity.
|
||||
m_buffer_painter.save();
|
||||
m_buffer_painter.resetTransform();
|
||||
m_buffer_painter.setClipping(false);
|
||||
m_buffer_painter.setOpacity(1.0);
|
||||
m_buffer_painter.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
m_buffer_painter.fillRect(QRectF(QPointF(pixels.topLeft()) / ratio, QSizeF(pixels.size()) / ratio), Qt::transparent);
|
||||
m_buffer_painter.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief PaletteGraphicsView::drawBackground
|
||||
Into the off-screen image while painting inverted, else as
|
||||
@@ -199,15 +239,16 @@ void PaletteGraphicsView::drawForeground(QPainter *painter, const QRectF &rect)
|
||||
void PaletteGraphicsView::blitInverted(const QRect &area)
|
||||
{
|
||||
const qreal ratio = m_buffer.devicePixelRatio();
|
||||
QImage part = m_buffer.copy(QRectF(area.topLeft() * ratio, area.size() * ratio).toAlignedRect());
|
||||
const QRect pixels = QRectF(area.topLeft() * ratio, area.size() * ratio).toAlignedRect();
|
||||
QImage part = (m_layered ? m_composite : m_buffer).copy(pixels);
|
||||
part.setDevicePixelRatio(ratio);
|
||||
// The application palette, for the reason given in invertsLightness().
|
||||
const QPalette application_palette = QApplication::palette();
|
||||
QET::Palette::invertLightness(part, application_palette.color(QPalette::Base),
|
||||
if (!m_layered && !customBackgroundColor()) QET::Palette::invertLightness(part, application_palette.color(QPalette::Base),
|
||||
application_palette.color(QPalette::Text));
|
||||
|
||||
QPainter painter(viewport());
|
||||
painter.drawImage(area.topLeft(), part);
|
||||
painter.drawImage(QPointF(pixels.topLeft()) / ratio, part);
|
||||
drawRubberBand(painter);
|
||||
}
|
||||
|
||||
|
||||
@@ -90,11 +90,15 @@ class PaletteGraphicsView : public QGraphicsView
|
||||
void paintInverted(QPaintEvent *event);
|
||||
void blitInverted(const QRect &area);
|
||||
void drawRubberBand(QPainter &painter);
|
||||
void flushLayer(bool invert, const QRect &bounds = QRect());
|
||||
|
||||
/// The off-screen image the hooks paint into while m_inverting:
|
||||
/// the viewport's size, in its coordinates. Kept between paints,
|
||||
/// dropped when the view paints on a light palette again.
|
||||
QImage m_buffer;
|
||||
QImage m_composite;
|
||||
QRect m_exposed;
|
||||
bool m_layered = false;
|
||||
QPainter m_buffer_painter;
|
||||
/// True while paintEvent() paints for an inverted display.
|
||||
bool m_inverting = false;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "diagramimageitem.h"
|
||||
#include "../darkimagerendering.h"
|
||||
#include "../shownkinds.h"
|
||||
|
||||
#include "../PropertiesEditor/propertieseditordialog.h"
|
||||
@@ -97,7 +98,7 @@ DiagramImageItem::~DiagramImageItem()
|
||||
@param widget the QWidget where we draw the pixmap
|
||||
*/
|
||||
void DiagramImageItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) {
|
||||
painter -> drawPixmap(pixmap_.rect(),pixmap_);
|
||||
DarkImageRendering::paintPixmap(painter, pixmap_, m_adapt_to_dark_theme);
|
||||
|
||||
Q_UNUSED(option); Q_UNUSED(widget);
|
||||
|
||||
@@ -155,6 +156,14 @@ void DiagramImageItem::setLabel(const QString &label)
|
||||
emit labelChanged();
|
||||
}
|
||||
|
||||
void DiagramImageItem::setAdaptToDarkTheme(bool adapt)
|
||||
{
|
||||
if (m_adapt_to_dark_theme == adapt) return;
|
||||
m_adapt_to_dark_theme = adapt;
|
||||
update();
|
||||
emit adaptToDarkThemeChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramImageItem::editProperty
|
||||
Open the appropriate dialog to edit this image
|
||||
@@ -1479,6 +1488,7 @@ bool DiagramImageItem::fromXml(const QDomElement &e)
|
||||
if (e.tagName() != "image") {
|
||||
return (false);
|
||||
}
|
||||
setAdaptToDarkTheme(e.attribute("adapt_to_dark_theme", "false") == "true");
|
||||
|
||||
const QUuid uuid(e.attribute(QStringLiteral("uuid")));
|
||||
if (!uuid.isNull() && uuid != m_uuid) setUuid(uuid);
|
||||
@@ -1623,6 +1633,7 @@ QDomElement DiagramImageItem::toXml(QDomDocument &document) const
|
||||
{
|
||||
QDomElement result = document.createElement("image");
|
||||
result.setAttribute("uuid", m_uuid.toString());
|
||||
if (m_adapt_to_dark_theme) result.setAttribute("adapt_to_dark_theme", "true");
|
||||
//write some attribute
|
||||
result.setAttribute("x", QString::number(pos().x()));
|
||||
result.setAttribute("y", QString::number(pos().y()));
|
||||
|
||||
@@ -53,6 +53,7 @@ class DiagramImageItem : public QetGraphicsItem {
|
||||
// colours -- as one value, so that every edit of it (crop, colour
|
||||
// key, mirror, replace) is undone together with the displayed pixmap.
|
||||
Q_PROPERTY(QVariant imageSource READ imageSourceVariant WRITE setImageSourceVariant)
|
||||
Q_PROPERTY(bool adaptToDarkTheme READ adaptToDarkTheme WRITE setAdaptToDarkTheme NOTIFY adaptToDarkThemeChanged)
|
||||
// A second, deliberately non-compensating property on the SAME
|
||||
// underlying value -- setPivot() (above) intentionally adjusts
|
||||
// pos() to keep the image visually in place, which is exactly
|
||||
@@ -149,12 +150,15 @@ class DiagramImageItem : public QetGraphicsItem {
|
||||
void setPivotRaw(const QPointF &pivot);
|
||||
QString label() const { return m_label; }
|
||||
void setLabel(const QString &label);
|
||||
bool adaptToDarkTheme() const { return m_adapt_to_dark_theme; }
|
||||
void setAdaptToDarkTheme(bool adapt);
|
||||
|
||||
signals:
|
||||
void pixmapChanged();
|
||||
void transformChanged();
|
||||
void uuidChanged();
|
||||
void labelChanged();
|
||||
void adaptToDarkThemeChanged();
|
||||
|
||||
protected:
|
||||
void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override;
|
||||
@@ -258,6 +262,7 @@ class DiagramImageItem : public QetGraphicsItem {
|
||||
// Empty by default, and then neither saved nor painted, so a picture
|
||||
// without one costs exactly what it did before.
|
||||
QString m_label;
|
||||
bool m_adapt_to_dark_theme = false;
|
||||
QFont m_label_font;
|
||||
QSizeF m_label_size; // in scene units, measured once in setLabel()
|
||||
QPointF m_label_scale{1.0, 1.0}; // scale the label rect was last computed for -- see updateLabelScale()
|
||||
|
||||
+10
-3
@@ -83,8 +83,14 @@ bool QET::Palette::isDark(const QPalette &palette)
|
||||
void QET::Palette::invertLightness(QImage &image, const QColor &sheet,
|
||||
const QColor &ink)
|
||||
{
|
||||
if (image.format() != QImage::Format_RGB32)
|
||||
image.convertTo(QImage::Format_RGB32);
|
||||
if (image.format() != QImage::Format_RGB32) image.convertTo(QImage::Format_RGB32);
|
||||
invertLightnessLayer(image, sheet, ink);
|
||||
}
|
||||
|
||||
void QET::Palette::invertLightnessLayer(QImage &image, const QColor &sheet, const QColor &ink)
|
||||
{
|
||||
if (image.format() != QImage::Format_RGB32 && image.format() != QImage::Format_ARGB32)
|
||||
image.convertTo(QImage::Format_ARGB32);
|
||||
|
||||
// One table per channel maps the inverted value (0 = was white,
|
||||
// 255 = was black) onto the sheet..ink span.
|
||||
@@ -106,6 +112,7 @@ void QET::Palette::invertLightness(QImage &image, const QColor &sheet,
|
||||
quint32 *line = reinterpret_cast<quint32 *>(image.scanLine(y));
|
||||
for (int x = 0, width = image.width(); x < width; ++x) {
|
||||
const quint32 pixel = line[x];
|
||||
if ((pixel & 0xff000000u) == 0) continue;
|
||||
const int red = (pixel >> 16) & 0xff;
|
||||
const int green = (pixel >> 8) & 0xff;
|
||||
const int blue = pixel & 0xff;
|
||||
@@ -114,7 +121,7 @@ void QET::Palette::invertLightness(QImage &image, const QColor &sheet,
|
||||
if (blue > highest) highest = blue;
|
||||
if (blue < lowest) lowest = blue;
|
||||
const int offset = 255 - highest - lowest;
|
||||
line[x] = 0xff000000u
|
||||
line[x] = (pixel & 0xff000000u)
|
||||
| (quint32(red_of[red + offset]) << 16)
|
||||
| (quint32(green_of[green + offset]) << 8)
|
||||
| quint32(blue_of[blue + offset]);
|
||||
|
||||
@@ -66,6 +66,9 @@ namespace QET {
|
||||
*/
|
||||
void invertLightness(QImage &image, const QColor &sheet = Qt::black,
|
||||
const QColor &ink = Qt::white);
|
||||
/// The same colour mapping for a transparent composition layer.
|
||||
/// Retains alpha; premultiplied input is converted to straight ARGB32.
|
||||
void invertLightnessLayer(QImage &image, const QColor &sheet, const QColor &ink);
|
||||
|
||||
/**
|
||||
The color of the grid dots on a sheet of color \a sheet: black,
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
#include "../qetgraphicsitem/diagramimageitem.h"
|
||||
#include "../qeticons.h"
|
||||
#include "../ui_imagepropertieswidget.h"
|
||||
#include <QSignalBlocker>
|
||||
#include <QDialog>
|
||||
#include <QEvent>
|
||||
|
||||
/**
|
||||
@brief ImagePropertiesWidget::ImagePropertiesWidget
|
||||
@@ -53,6 +56,19 @@ ImagePropertiesWidget::~ImagePropertiesWidget()
|
||||
delete ui;
|
||||
}
|
||||
|
||||
bool ImagePropertiesWidget::event(QEvent *event)
|
||||
{
|
||||
// The generic properties dialog resets on its Cancel button, but its
|
||||
// title-bar close and Escape reach reject() directly. Roll back the
|
||||
// same preview in all rejection paths, without changing other editors.
|
||||
if (event->type() == QEvent::ParentChange) {
|
||||
if (auto *dialog = qobject_cast<QDialog *>(parentWidget())) {
|
||||
connect(dialog, &QDialog::rejected, this, &ImagePropertiesWidget::reset, Qt::UniqueConnection);
|
||||
}
|
||||
}
|
||||
return PropertiesEditorWidget::event(event);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImagePropertiesWidget::setImageItem
|
||||
Set the image to edit properties
|
||||
@@ -67,12 +83,15 @@ void ImagePropertiesWidget::setImageItem(DiagramImageItem *image)
|
||||
{
|
||||
disconnect(m_image, &DiagramImageItem::transformChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
disconnect(m_image, &DiagramImageItem::labelChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
disconnect(m_image, &DiagramImageItem::adaptToDarkThemeChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
}
|
||||
|
||||
m_image = image;
|
||||
connect(m_image, &DiagramImageItem::transformChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
connect(m_image, &DiagramImageItem::labelChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
connect(m_image, &DiagramImageItem::adaptToDarkThemeChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
m_movable = image->isMovable();
|
||||
m_adapt_to_dark_theme = image->adaptToDarkTheme();
|
||||
m_scaleX = m_image->scaleFactorX();
|
||||
m_scaleY = m_image->scaleFactorY();
|
||||
m_rotation = m_image->rotationAngle();
|
||||
@@ -107,6 +126,7 @@ void ImagePropertiesWidget::apply()
|
||||
m_skewX = m_image->skewX();
|
||||
m_skewY = m_image->skewY();
|
||||
m_label = m_image->label();
|
||||
m_adapt_to_dark_theme = m_image->adaptToDarkTheme();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,6 +144,9 @@ void ImagePropertiesWidget::reset()
|
||||
m_image->setSkewY(m_skewY);
|
||||
m_image->setLabel(m_label);
|
||||
m_image->setMovable(m_movable);
|
||||
m_preview_dark_theme = true;
|
||||
m_image->setAdaptToDarkTheme(m_adapt_to_dark_theme);
|
||||
m_preview_dark_theme = false;
|
||||
updateUi();
|
||||
}
|
||||
|
||||
@@ -146,6 +169,7 @@ bool ImagePropertiesWidget::setLiveEdit(bool live_edit)
|
||||
connect (ui->m_skew_x_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
connect (ui->m_skew_y_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
connect (ui->m_label_le, &QLineEdit::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
connect (ui->m_adapt_to_dark_theme_cb, &QCheckBox::clicked, this, &ImagePropertiesWidget::apply);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -155,6 +179,7 @@ bool ImagePropertiesWidget::setLiveEdit(bool live_edit)
|
||||
disconnect (ui->m_skew_x_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
disconnect (ui->m_skew_y_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
disconnect (ui->m_label_le, &QLineEdit::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
disconnect (ui->m_adapt_to_dark_theme_cb, &QCheckBox::clicked, this, &ImagePropertiesWidget::apply);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -179,7 +204,7 @@ QUndoCommand* ImagePropertiesWidget::associatedUndo() const
|
||||
const qreal newSkewX = ui->m_skew_x_sb->value();
|
||||
const qreal newSkewY = ui->m_skew_y_sb->value();
|
||||
|
||||
QPropertyUndoCommand *undo = nullptr;
|
||||
QUndoCommand *undo = nullptr;
|
||||
auto chain = [&](const char *property, qreal oldValue, qreal newValue, const QString &text)
|
||||
{
|
||||
if (qFuzzyCompare(oldValue, newValue))
|
||||
@@ -188,9 +213,10 @@ QUndoCommand* ImagePropertiesWidget::associatedUndo() const
|
||||
new QPropertyUndoCommand(m_image, property, oldValue, newValue, undo);
|
||||
else
|
||||
{
|
||||
undo = new QPropertyUndoCommand(m_image, property, oldValue, newValue);
|
||||
undo->enableAnimation();
|
||||
undo->setText(text);
|
||||
auto *change = new QPropertyUndoCommand(m_image, property, oldValue, newValue);
|
||||
change->enableAnimation();
|
||||
change->setText(text);
|
||||
undo = change;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -213,6 +239,14 @@ QUndoCommand* ImagePropertiesWidget::associatedUndo() const
|
||||
}
|
||||
}
|
||||
|
||||
const bool adapt = ui->m_adapt_to_dark_theme_cb->isChecked();
|
||||
if (adapt != m_adapt_to_dark_theme) {
|
||||
if (undo) new QPropertyUndoCommand(m_image, "adaptToDarkTheme", m_adapt_to_dark_theme, adapt, undo);
|
||||
else {
|
||||
undo = new QUndoCommand(tr("Modifier l'adaptation d'une image au thème sombre"));
|
||||
new QPropertyUndoCommand(m_image, "adaptToDarkTheme", m_adapt_to_dark_theme, adapt, undo);
|
||||
}
|
||||
}
|
||||
return undo;
|
||||
}
|
||||
|
||||
@@ -233,6 +267,9 @@ void ImagePropertiesWidget::updateUi()
|
||||
// signal from a resize handle used directly on the canvas while
|
||||
// this dialog is open).
|
||||
if (!m_image || m_updating_ratio) return;
|
||||
if (m_live_edit && !m_preview_dark_theme) m_adapt_to_dark_theme = m_image->adaptToDarkTheme();
|
||||
const QSignalBlocker darkThemeBlocker(ui->m_adapt_to_dark_theme_cb);
|
||||
ui->m_adapt_to_dark_theme_cb->setChecked(m_image->adaptToDarkTheme());
|
||||
|
||||
m_updating_ratio = true;
|
||||
ui->m_width_sb->setValue(m_image->scaleFactorX() * 100.0);
|
||||
@@ -395,3 +432,11 @@ void ImagePropertiesWidget::on_m_lock_pos_cb_clicked()
|
||||
{
|
||||
m_image->setMovable(!ui->m_lock_pos_cb->isChecked());
|
||||
}
|
||||
|
||||
void ImagePropertiesWidget::on_m_adapt_to_dark_theme_cb_toggled(bool checked)
|
||||
{
|
||||
if (!m_image) return;
|
||||
m_preview_dark_theme = true;
|
||||
m_image->setAdaptToDarkTheme(checked);
|
||||
m_preview_dark_theme = false;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ class ImagePropertiesWidget : public PropertiesEditorWidget
|
||||
void reset() override;
|
||||
bool setLiveEdit(bool live_edit) override;
|
||||
QUndoCommand* associatedUndo() const override;
|
||||
protected:
|
||||
bool event(QEvent *event) override;
|
||||
|
||||
private:
|
||||
void updateUi() override;
|
||||
@@ -56,11 +58,14 @@ class ImagePropertiesWidget : public PropertiesEditorWidget
|
||||
void on_m_lock_ratio_tb_toggled(bool checked);
|
||||
void on_m_restore_ratio_pb_clicked();
|
||||
void on_m_lock_pos_cb_clicked();
|
||||
void on_m_adapt_to_dark_theme_cb_toggled(bool checked);
|
||||
|
||||
private:
|
||||
Ui::ImagePropertiesWidget *ui;
|
||||
DiagramImageItem *m_image;
|
||||
bool m_movable;
|
||||
bool m_adapt_to_dark_theme = false;
|
||||
bool m_preview_dark_theme = false;
|
||||
// All tracked independently -- the image may already be
|
||||
// non-uniformly scaled, rotated, and/or skewed via the resize/
|
||||
// rotate/skew handles before this dialog is even opened, and
|
||||
|
||||
@@ -173,7 +173,17 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<item row="8" column="0" colspan="3">
|
||||
<widget class="QCheckBox" name="m_adapt_to_dark_theme_cb">
|
||||
<property name="text">
|
||||
<string>Adapter l’image au thème sombre</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Adapte les couleurs à l’affichage sombre uniquement. L’image originale, les exports et les impressions restent inchangés.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
|
||||
@@ -66,6 +66,22 @@ include(../../cmake/fetch_pugixml.cmake)
|
||||
|
||||
enable_testing()
|
||||
|
||||
add_executable(tst_darkimagelayers tst_darkimagelayers.cpp
|
||||
${QET_DIR}/sources/palettegraphicsview.cpp ${QET_DIR}/sources/palettegraphicsview.h
|
||||
${QET_DIR}/sources/qetpalette.cpp)
|
||||
add_test(NAME tst_darkimagelayers COMMAND tst_darkimagelayers)
|
||||
target_include_directories(tst_darkimagelayers PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_darkimagelayers PRIVATE Qt::Test Qt::Widgets)
|
||||
find_package(Python3 QUIET COMPONENTS Interpreter)
|
||||
if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja")
|
||||
add_test(NAME tst_darkimageintegration
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_image_dark_probe.py
|
||||
--build ${CMAKE_BINARY_DIR} --binary $<TARGET_FILE:qelectrotech>
|
||||
--source ${CMAKE_CURRENT_SOURCE_DIR}/image_dark_integration_probe.cpp
|
||||
--fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/unlinked_contact_label.qet)
|
||||
set_tests_properties(tst_darkimageintegration PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE)
|
||||
endif()
|
||||
|
||||
add_executable(${PROJECT_NAME} tst_mytest.cpp)
|
||||
add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME})
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Linked by run_image_dark_probe.py against the application's own objects.
|
||||
#include <QApplication>
|
||||
#include <QBuffer>
|
||||
#include <QCheckBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDomDocument>
|
||||
#include <QFile>
|
||||
#include <QFontDatabase>
|
||||
#include <QPrinter>
|
||||
#include <QPushButton>
|
||||
#include <QSettings>
|
||||
#include <QSvgGenerator>
|
||||
#include <QTemporaryDir>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
#include "../../sources/diagram.h"
|
||||
#include "../../sources/qetproject.h"
|
||||
#include "../../sources/qetresult.h"
|
||||
#include "../../sources/qetmessagebox.h"
|
||||
#include "../../sources/qetpalette.h"
|
||||
#include "../../sources/palettegraphicsview.h"
|
||||
#include "../../sources/qetgraphicsitem/diagramimageitem.h"
|
||||
#include "../../sources/ui/imagepropertieswidget.h"
|
||||
#include "../../sources/PropertiesEditor/propertieseditordialog.h"
|
||||
|
||||
static void check(bool value, const char *message) { if (!value) throw std::runtime_error(message); }
|
||||
static QByteArray png(const QPixmap &pixmap) {
|
||||
QByteArray bytes; QBuffer buffer(&bytes); buffer.open(QIODevice::WriteOnly); pixmap.save(&buffer,"PNG"); return bytes;
|
||||
}
|
||||
static QByteArray read(const QString &path) { QFile file(path); check(file.open(QIODevice::ReadOnly),"read export"); return file.readAll(); }
|
||||
static DiagramImageItem *findImage(Diagram *diagram) {
|
||||
for (auto *item : diagram->items()) if (auto *image = dynamic_cast<DiagramImageItem *>(item)) return image;
|
||||
return nullptr;
|
||||
}
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
QApplication app(argc,argv);
|
||||
std::freopen(argv[2],"w",stdout); std::freopen(argv[2],"a",stderr);
|
||||
QTemporaryDir settings;
|
||||
QSettings::setDefaultFormat(QSettings::IniFormat);
|
||||
QSettings::setPath(QSettings::IniFormat,QSettings::UserScope,settings.path());
|
||||
QCoreApplication::setOrganizationName("QETImageDarkRegression");
|
||||
QETProject::setBackupEnabled(false); QET::QetMessageBox::setNonInteractive(true);
|
||||
QFontDatabase::addApplicationFont(":/fonts/LiberationSans-Regular.ttf");
|
||||
try {
|
||||
const QString output=QString::fromLocal8Bit(argv[3]);
|
||||
QETProject project(QString::fromLocal8Bit(argv[1])); check(project.state()==QETProject::Ok,"load fixture");
|
||||
auto *diagram=project.diagrams().first();
|
||||
QImage original(40,30,QImage::Format_ARGB32); original.fill(QColor(197,81,33));
|
||||
for(int y=0;y<30;++y) for(int x=10;x<20;++x) original.setPixelColor(x,y,QColor(197,81,33,128));
|
||||
for(int y=10;y<16;++y) for(int x=24;x<30;++x) original.setPixelColor(x,y,Qt::transparent);
|
||||
QPixmap source=QPixmap::fromImage(original);
|
||||
auto *image=new DiagramImageItem(source); diagram->addItem(image);
|
||||
image->setPos(55,55); image->setLabel("Image test"); image->setRotationAngle(17); image->setScaleFactorX(1.3); image->setSkewX(8);
|
||||
check(!image->adaptToDarkTheme(),"new image defaults to original colours");
|
||||
QDomDocument xml; auto legacy=image->toXml(xml);
|
||||
check(!legacy.hasAttribute("adapt_to_dark_theme"),"default does not add an attribute to old files");
|
||||
const QRect crop(4,3,24,20);
|
||||
legacy.firstChild().setNodeValue(QString::fromLatin1(png(source.copy(crop)).toBase64()));
|
||||
auto base=xml.createElement("image_base"); base.appendChild(xml.createTextNode(QString::fromLatin1(png(source).toBase64()))); legacy.appendChild(base);
|
||||
auto cropElement=xml.createElement("crop"); cropElement.setAttribute("x",4); cropElement.setAttribute("y",3); cropElement.setAttribute("w",24); cropElement.setAttribute("h",20); legacy.appendChild(cropElement);
|
||||
image->setAdaptToDarkTheme(true); check(image->fromXml(legacy),"load cropped legacy image");
|
||||
check(!image->adaptToDarkTheme(),"missing attribute resets an existing image to original colours");
|
||||
check(image->pixmap().size()==crop.size(),"crop survives");
|
||||
const QByteArray pristine=png(image->pixmap()); const auto transform=image->transform(); const auto bounds=image->boundingRect();
|
||||
ImagePropertiesWidget widget(image); auto *box=widget.findChild<QCheckBox *>("m_adapt_to_dark_theme_cb");
|
||||
check(box && !box->isChecked(),"unchecked properties default");
|
||||
check(box->text()==QString::fromUtf8("Adapter l’image au thème sombre"),"checkbox wording");
|
||||
check(box->toolTip()==QString::fromUtf8("Adapte les couleurs à l’affichage sombre uniquement. L’image originale, les exports et les impressions restent inchangés."),"tooltip wording");
|
||||
box->setChecked(true); check(image->adaptToDarkTheme(),"preview"); widget.reset(); check(!image->adaptToDarkTheme(),"reset cancels preview");
|
||||
diagram->undoStack().clear(); box->setChecked(true); widget.apply(); check(image->adaptToDarkTheme(),"apply");
|
||||
diagram->undoStack().undo(); check(!image->adaptToDarkTheme()&&!box->isChecked(),"undo updates checkbox");
|
||||
diagram->undoStack().redo(); check(image->adaptToDarkTheme()&&box->isChecked(),"redo updates checkbox");
|
||||
widget.setLiveEdit(true); box->click(); check(!image->adaptToDarkTheme(),"live edit");
|
||||
check(diagram->undoStack().count()==2,"deliberate toggles do not merge");
|
||||
diagram->undoStack().undo(); check(image->adaptToDarkTheme(),"live undo");
|
||||
diagram->undoStack().redo(); check(!image->adaptToDarkTheme(),"live redo");
|
||||
box->click(); check(image->adaptToDarkTheme()&&diagram->undoStack().count()==3,"edit after undo has current baseline");
|
||||
for (auto role : {QDialogButtonBox::Cancel,QDialogButtonBox::Reset,QDialogButtonBox::Apply}) {
|
||||
image->setAdaptToDarkTheme(false);
|
||||
auto *editor=new ImagePropertiesWidget(image); PropertiesEditorDialog dialog(editor);
|
||||
editor->findChild<QCheckBox *>("m_adapt_to_dark_theme_cb")->setChecked(true);
|
||||
dialog.findChild<QDialogButtonBox *>()->button(role)->click();
|
||||
check(image->adaptToDarkTheme()==(role==QDialogButtonBox::Apply),"actual dialog button semantics");
|
||||
}
|
||||
check(png(image->pixmap())==pristine && image->transform()==transform && image->boundingRect()==bounds && image->label()=="Image test","choice cannot modify pixels, transform, crop or caption");
|
||||
image->setAdaptToDarkTheme(false);
|
||||
{
|
||||
auto *editor=new ImagePropertiesWidget(image); PropertiesEditorDialog dialog(editor);
|
||||
editor->findChild<QCheckBox *>("m_adapt_to_dark_theme_cb")->setChecked(true);
|
||||
dialog.reject(); check(!image->adaptToDarkTheme(),"Escape/title-bar rejection cancels preview");
|
||||
}
|
||||
PaletteGraphicsView view(diagram); view.setSceneRect(0,0,180,180); view.setFrameShape(QFrame::NoFrame);
|
||||
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
view.setAlignment(Qt::AlignLeft|Qt::AlignTop); view.resize(180,180); view.setAttribute(Qt::WA_DontShowOnScreen); view.show();
|
||||
for (bool dark : {false,true}) for(bool adapt : {false,true}) {
|
||||
const auto palette=dark?QET::Palette::fusionDark():QET::Palette::fusionLight(); QApplication::setPalette(palette);
|
||||
image->setAdaptToDarkTheme(adapt); QApplication::processEvents(); const auto shown=view.viewport()->grab().toImage();
|
||||
shown.save(output+QString("/screen-%1-%2.png").arg(dark).arg(adapt));
|
||||
const auto point=view.mapFromScene(image->mapToScene(QPointF(2,2))); const auto ratio=shown.devicePixelRatio();
|
||||
QImage expected(1,1,QImage::Format_RGB32); expected.fill(QColor(197,81,33));
|
||||
if(dark&&adapt) QET::Palette::invertLightness(expected,palette.color(QPalette::Base),palette.color(QPalette::Text));
|
||||
check(shown.pixelColor(qRound(point.x()*ratio),qRound(point.y()*ratio))==expected.pixelColor(0,0),"actual rotated/cropped image display colour");
|
||||
project.setFilePath(output+QString("/project-%1-%2.qet").arg(dark).arg(adapt)); check(project.write().isOk(),"project save");
|
||||
QETProject reopened(project.filePath()); check(reopened.state()==QETProject::Ok,"project reopen");
|
||||
auto *loaded=findImage(reopened.diagrams().first()); check(loaded && loaded->adaptToDarkTheme()==adapt,"per-image persisted choice");
|
||||
check(png(loaded->pixmap())==pristine && loaded->transform()==transform && loaded->label()=="Image test","reopen keeps original image/crop/transform/caption");
|
||||
}
|
||||
QApplication::setPalette(QET::Palette::fusionDark()); QImage exports[2];
|
||||
for(int i=0;i<2;++i) {
|
||||
image->setAdaptToDarkTheme(i); exports[i]=QImage(180,180,QImage::Format_RGB32); exports[i].fill(Qt::white);
|
||||
{QPainter painter(&exports[i]); diagram->render(&painter,QRectF(0,0,180,180),QRectF(0,0,180,180));}
|
||||
exports[i].save(output+QString("/export-%1.png").arg(i));
|
||||
QSvgGenerator svg; svg.setFileName(output+QString("/export-%1.svg").arg(i)); svg.setSize(QSize(180,180)); svg.setViewBox(QRect(0,0,180,180));
|
||||
{QPainter painter(&svg); diagram->render(&painter,QRectF(0,0,180,180),QRectF(0,0,180,180));}
|
||||
QPrinter printer; printer.setOutputFormat(QPrinter::PdfFormat); printer.setOutputFileName(output+QString("/print-%1.pdf").arg(i)); printer.setResolution(72);
|
||||
{QPainter painter(&printer); check(painter.isActive(),"PDF print painter"); diagram->render(&painter,QRectF(0,0,180,180),QRectF(0,0,180,180));}
|
||||
}
|
||||
check(exports[0]==exports[1],"raster export unchanged"); check(read(output+"/export-0.svg")==read(output+"/export-1.svg"),"SVG export unchanged");
|
||||
std::puts("PASS: image defaults, XML crop/alpha/transform/label, properties preview/apply/reset/cancel, undo/redo and live edit, four display modes, project save/reopen, raster/SVG export, PDF print generation");
|
||||
} catch(const std::exception &error) {std::fprintf(stderr,"FAIL: %s\n",error.what());return 1;}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Link the real image/widget integration probe using an existing Ninja app build.
|
||||
|
||||
Reuses app objects without duplicating the app or introducing a core-library
|
||||
refactor. No application source or existing executable is replaced. The
|
||||
generated manifest, probe binary, reports and export fixtures stay in build/.
|
||||
"""
|
||||
import argparse
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--build', required=True)
|
||||
parser.add_argument('--binary', required=True)
|
||||
parser.add_argument('--source', required=True)
|
||||
parser.add_argument('--fixture', required=True)
|
||||
args = parser.parse_args()
|
||||
build = pathlib.Path(args.build).resolve()
|
||||
binary = pathlib.Path(args.binary).resolve()
|
||||
source = pathlib.Path(args.source).resolve()
|
||||
fixture = pathlib.Path(args.fixture).resolve()
|
||||
manifest = (build / 'build.ninja').read_text(encoding='utf-8')
|
||||
match = re.search(r'^build (\S*?/sources/main\.cpp\.(?:obj|o)):', manifest, re.MULTILINE)
|
||||
if not match:
|
||||
sys.exit('Cannot find the application main object in the Ninja build.')
|
||||
main_object = match.group(1)
|
||||
probe_object = main_object.replace('main.cpp.', 'image_dark_integration_probe.cpp.')
|
||||
probe_binary = binary.with_name('image_dark_integration_probe' + binary.suffix)
|
||||
def escape(path):
|
||||
return pathlib.Path(path).as_posix().replace('$', '$$').replace(':', '$:').replace(' ', '$ ')
|
||||
main_source = source.parents[2] / 'sources' / 'main.cpp'
|
||||
manifest = manifest.replace(main_object, probe_object)
|
||||
manifest = manifest.replace(escape(main_source), escape(source))
|
||||
if binary.suffix:
|
||||
manifest = manifest.replace(binary.name, probe_binary.name)
|
||||
else:
|
||||
old_target = escape(binary.name if binary.parent == build else binary)
|
||||
new_target = escape(probe_binary.name if probe_binary.parent == build else probe_binary)
|
||||
manifest = manifest.replace('build ' + old_target + ':', 'build ' + new_target + ':')
|
||||
old_file = binary.name if binary.parent == build else binary.as_posix()
|
||||
new_file = probe_binary.name if probe_binary.parent == build else probe_binary.as_posix()
|
||||
manifest = re.sub(r'^ TARGET_FILE = ' + re.escape(old_file) + r'$',
|
||||
' TARGET_FILE = ' + new_file, manifest, flags=re.MULTILINE)
|
||||
probe_manifest = build / 'image-dark-probe.ninja'
|
||||
probe_manifest.write_text(manifest, encoding='utf-8')
|
||||
ninja = shutil.which('ninja')
|
||||
if not ninja:
|
||||
sys.exit('Ninja is required for the integration probe.')
|
||||
target = probe_binary.name if probe_binary.parent == build else str(probe_binary)
|
||||
subprocess.run([ninja, '-C', str(build), '-f', probe_manifest.name, target], check=True)
|
||||
output = build / 'image-dark-integration'
|
||||
output.mkdir(exist_ok=True)
|
||||
report = output / 'results.txt'
|
||||
result = subprocess.run([str(probe_binary), str(fixture), str(report), str(output)], timeout=90)
|
||||
if report.exists():
|
||||
print(report.read_text(encoding='utf-8', errors='replace'))
|
||||
if result.returncode == 0:
|
||||
def print_streams(path):
|
||||
streams = re.findall(rb'\nstream\r?\n(.*?)\nendstream', path.read_bytes(), re.DOTALL)
|
||||
decoded = []
|
||||
for stream in streams:
|
||||
try:
|
||||
data = zlib.decompress(stream)
|
||||
except zlib.error:
|
||||
data = stream
|
||||
# Qt assigns every PDF a fresh XMP DocumentID (and dates).
|
||||
# Metadata is not printing content and must not be compared
|
||||
# as if it were a graphics/image/font stream.
|
||||
if b'<x:xmpmeta' not in data:
|
||||
decoded.append(data)
|
||||
return decoded
|
||||
first = print_streams(output / 'print-0.pdf')
|
||||
second = print_streams(output / 'print-1.pdf')
|
||||
if not first or first != second:
|
||||
sys.exit('PDF printing changed the decoded graphics/image/font streams.')
|
||||
print('PASS: PDF print graphics/image/font streams identical for both choices.')
|
||||
try:
|
||||
import pymupdf
|
||||
except ImportError:
|
||||
print('PDF raster comparison not run: optional PyMuPDF is unavailable.')
|
||||
else:
|
||||
documents = [pymupdf.open(output / f'print-{i}.pdf') for i in range(2)]
|
||||
if len(documents[0]) != len(documents[1]):
|
||||
sys.exit('PDF page counts differ.')
|
||||
for page in range(len(documents[0])):
|
||||
pixels = [doc[page].get_pixmap(dpi=144, alpha=False) for doc in documents]
|
||||
if (pixels[0].width, pixels[0].height, pixels[0].samples) != (pixels[1].width, pixels[1].height, pixels[1].samples):
|
||||
sys.exit('Rasterized PDF print pages differ.')
|
||||
for i, image in enumerate(pixels):
|
||||
image.save(output / f'print-{i}-page-{page}.png')
|
||||
print('PASS: PDF print pages rendered at 144 dpi are pixel-identical.')
|
||||
sys.exit(result.returncode)
|
||||
@@ -0,0 +1,129 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
#include <QGraphicsRectItem>
|
||||
#include <QGraphicsScene>
|
||||
#include "darkimagerendering.h"
|
||||
#include "palettegraphicsview.h"
|
||||
#include "qetpalette.h"
|
||||
|
||||
class Raster : public QGraphicsItem {
|
||||
public:
|
||||
QPixmap pixmap;
|
||||
bool adapt = false;
|
||||
explicit Raster(QGraphicsItem *parent = nullptr) : QGraphicsItem(parent) {
|
||||
QImage image(40, 30, QImage::Format_ARGB32);
|
||||
image.fill(QColor(197, 81, 33));
|
||||
for (int y = 0; y < 30; ++y)
|
||||
for (int x = 10; x < 20; ++x) image.setPixelColor(x, y, QColor(197,81,33,128));
|
||||
for (int y = 0; y < 30; ++y)
|
||||
for (int x = 30; x < 40; ++x) image.setPixelColor(x, y, Qt::transparent);
|
||||
pixmap = QPixmap::fromImage(image);
|
||||
}
|
||||
QRectF boundingRect() const override { return QRectF(0,0,40,40); }
|
||||
void paint(QPainter *p, const QStyleOptionGraphicsItem *, QWidget *) override {
|
||||
DarkImageRendering::paintPixmap(p, pixmap, adapt);
|
||||
p->fillRect(QRectF(0,34,8,4), Qt::black); // caption/decorations remain adapted
|
||||
}
|
||||
};
|
||||
|
||||
class tst_darkimagelayers : public QObject {
|
||||
Q_OBJECT
|
||||
static QColor sample(const QImage &image, const PaletteGraphicsView &view, QPointF scene) {
|
||||
const QPoint point = view.mapFromScene(scene);
|
||||
const qreal dpr = image.devicePixelRatio();
|
||||
return image.pixelColor(qRound(point.x()*dpr), qRound(point.y()*dpr));
|
||||
}
|
||||
static void setup(PaletteGraphicsView &view) {
|
||||
view.setFrameShape(QFrame::NoFrame);
|
||||
view.setAlignment(Qt::AlignLeft | Qt::AlignTop);
|
||||
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
view.resize(100,80); view.show();
|
||||
}
|
||||
private slots:
|
||||
void cleanup() {
|
||||
QApplication::setPalette(QET::Palette::fusionLight());
|
||||
PaletteGraphicsView::setCustomBackgroundColor(false);
|
||||
}
|
||||
void modes_data() {
|
||||
QTest::addColumn<bool>("dark"); QTest::addColumn<bool>("adapt"); QTest::addColumn<bool>("custom");
|
||||
QTest::newRow("light-original") << false << false << false;
|
||||
QTest::newRow("light-adapt") << false << true << false;
|
||||
QTest::newRow("dark-original") << true << false << false;
|
||||
QTest::newRow("dark-adapt") << true << true << false;
|
||||
QTest::newRow("light-custom-original") << false << false << true;
|
||||
QTest::newRow("light-custom-adapt") << false << true << true;
|
||||
QTest::newRow("dark-custom-original") << true << false << true;
|
||||
QTest::newRow("dark-custom-adapt") << true << true << true;
|
||||
}
|
||||
void modes() {
|
||||
QFETCH(bool,dark); QFETCH(bool,adapt); QFETCH(bool,custom);
|
||||
PaletteGraphicsView::setCustomBackgroundColor(custom);
|
||||
const QPalette palette = dark ? QET::Palette::fusionDark() : QET::Palette::fusionLight();
|
||||
QApplication::setPalette(palette);
|
||||
const QColor background = custom ? QColor(80,90,100) : QColor(Qt::white);
|
||||
QGraphicsScene scene(0,0,100,80); scene.setBackgroundBrush(background);
|
||||
auto *raster = new Raster; raster->adapt = adapt; raster->setPos(10,10); scene.addItem(raster);
|
||||
scene.addRect(32,10,5,20,QPen(Qt::NoPen),QBrush(Qt::black))->setZValue(1);
|
||||
PaletteGraphicsView view(&scene); setup(view);
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&view));
|
||||
const QImage shown = view.viewport()->grab().toImage();
|
||||
QImage expected(100,80,QImage::Format_RGB32);
|
||||
expected.fill(custom ? background : (dark && !adapt ? palette.color(QPalette::Base) : QColor(Qt::white)));
|
||||
QPainter p(&expected);
|
||||
if (dark && custom && adapt) {
|
||||
QImage adapted = raster->pixmap.toImage();
|
||||
QET::Palette::invertLightnessLayer(adapted,palette.color(QPalette::Base),palette.color(QPalette::Text));
|
||||
p.drawImage(QPoint(10,10),adapted);
|
||||
} else p.drawPixmap(QPoint(10,10),raster->pixmap);
|
||||
p.fillRect(QRect(32,10,5,20), dark && !adapt && !custom ? palette.color(QPalette::Text) : QColor(Qt::black));
|
||||
p.end();
|
||||
if (dark && adapt && !custom) QET::Palette::invertLightness(expected,palette.color(QPalette::Base),palette.color(QPalette::Text));
|
||||
for (const QPoint point : {QPoint(15,15),QPoint(25,15),QPoint(45,15),QPoint(34,15)})
|
||||
QCOMPARE(sample(shown,view,point), expected.pixelColor(point));
|
||||
QCOMPARE(sample(shown,view,QPoint(13,45)), dark && !custom ? palette.color(QPalette::Text) : QColor(Qt::black));
|
||||
}
|
||||
void parentClippingRotationOpacityAndOverlap() {
|
||||
const auto palette = QET::Palette::fusionDark(); QApplication::setPalette(palette);
|
||||
QGraphicsScene scene(0,0,100,80); scene.setBackgroundBrush(Qt::white);
|
||||
auto *parent = scene.addRect(0,0,25,40,QPen(Qt::NoPen),QBrush(Qt::NoBrush));
|
||||
parent->setFlag(QGraphicsItem::ItemClipsChildrenToShape); parent->setPos(35,15); parent->setRotation(12);
|
||||
auto *first = new Raster(parent); first->setOpacity(0.5);
|
||||
auto *second = new Raster; second->setPos(36,30); scene.addItem(second); second->setZValue(2);
|
||||
QImage blue(40,30,QImage::Format_ARGB32); blue.fill(QColor(20,80,210,128)); second->pixmap=QPixmap::fromImage(blue);
|
||||
PaletteGraphicsView view(&scene); setup(view); QVERIFY(QTest::qWaitForWindowExposed(&view));
|
||||
const auto shown = view.viewport()->grab().toImage();
|
||||
// Original pixels blend once over the dark sheet, then the upper
|
||||
// original image blends once over those pixels. No inverse transform.
|
||||
QImage reference(1,1,QImage::Format_RGB32); reference.fill(palette.color(QPalette::Base));
|
||||
QPainter p(&reference); p.setOpacity(0.5); p.fillRect(reference.rect(),QColor(197,81,33)); p.end();
|
||||
QCOMPARE(sample(shown,view,first->mapToScene(QPointF(5,5))),reference.pixelColor(0,0));
|
||||
QPainter upper(&reference); upper.fillRect(reference.rect(),QColor(20,80,210,128)); upper.end();
|
||||
QCOMPARE(sample(shown,view,first->mapToScene(QPointF(5,20))),reference.pixelColor(0,0));
|
||||
// Pixel in the child's transparent stripe / beyond the parent's clip.
|
||||
QCOMPARE(sample(shown,view,first->mapToScene(QPointF(28,5))),palette.color(QPalette::Base));
|
||||
first->setPos(3,0); scene.update(); QApplication::processEvents();
|
||||
QVERIFY(!view.viewport()->grab().isNull());
|
||||
}
|
||||
void alphaMapping() {
|
||||
QImage image(2,1,QImage::Format_ARGB32_Premultiplied);
|
||||
image.setPixelColor(0,0,QColor(0,0,0,128)); image.setPixelColor(1,0,Qt::transparent);
|
||||
QET::Palette::invertLightnessLayer(image,QColor(30,30,30),QColor(220,220,220));
|
||||
QCOMPARE(image.pixelColor(0,0),QColor(220,220,220,128));
|
||||
QCOMPARE(image.pixelColor(1,0).alpha(),0);
|
||||
}
|
||||
void exportUnaffected() {
|
||||
QApplication::setPalette(QET::Palette::fusionDark());
|
||||
QGraphicsScene scene(0,0,100,80); scene.setBackgroundBrush(Qt::white);
|
||||
auto *raster=new Raster; raster->setPos(10,10); scene.addItem(raster);
|
||||
QImage results[2];
|
||||
for(int i=0;i<2;++i) {
|
||||
raster->adapt=i; results[i]=QImage(100,80,QImage::Format_RGB32); results[i].fill(Qt::white);
|
||||
QPainter p(&results[i]); scene.render(&p,QRectF(0,0,100,80),scene.sceneRect());
|
||||
}
|
||||
QCOMPARE(results[0],results[1]);
|
||||
QCOMPARE(results[0].pixelColor(15,15),QColor(197,81,33));
|
||||
}
|
||||
};
|
||||
QTEST_MAIN(tst_darkimagelayers)
|
||||
#include "tst_darkimagelayers.moc"
|
||||
Reference in New Issue
Block a user