16#include <QPixmapCache>
17#include <QRegularExpression>
18#include <QSvgRenderer>
33QByteArray orientSvgTextNodes(
const QByteArray &svgBytes,
const qreal angle,
const bool flipX,
const bool flipY)
35 static const QRegularExpression textTag(QStringLiteral(
"<text\\b[^>]*>"));
36 static const QRegularExpression idAttr(QStringLiteral(
"\\bid\\s*=\\s*\"([^\"]*)\""));
37 static const QRegularExpression transformAttr(QStringLiteral(
"\\btransform\\s*="));
39 QString svg = QString::fromUtf8(svgBytes);
47 auto matches = textTag.globalMatch(svg);
48 while (matches.hasNext()) {
49 const auto match = matches.next();
50 out += svg.mid(last, match.capturedStart() - last);
51 QString tag = match.captured(0);
52 if (!idAttr.match(tag).hasMatch()) {
54 tag.insert(5, QStringLiteral(
" id=\"wpflip-text-%1\"").arg(generated++));
57 last = match.capturedEnd();
63 QSvgRenderer probe(svg.toUtf8());
64 if (!probe.isValid()) {
68 const qreal sx = flipX ? -1.0 : 1.0;
69 const qreal sy = flipY ? -1.0 : 1.0;
77 auto matches = textTag.globalMatch(svg);
78 while (matches.hasNext()) {
79 const auto match = matches.next();
80 out += svg.mid(last, match.capturedStart() - last);
81 QString tag = match.captured(0);
83 const auto idMatch = idAttr.match(tag);
84 if (idMatch.hasMatch() && !transformAttr.match(tag).hasMatch()) {
85 const QRectF bounds = probe.boundsOnElement(idMatch.captured(1));
86 if (!bounds.isEmpty()) {
87 const QPointF c = bounds.center();
90 ops += QStringLiteral(
"rotate(%1) ").arg(-angle);
93 ops += QStringLiteral(
"scale(%1,%2) ").arg(sx).arg(sy);
95 const QString transform =
96 QStringLiteral(
" transform=\"translate(%1,%2) %3translate(%4,%5)\"")
97 .arg(c.x()).arg(c.y()).arg(ops).arg(-c.x()).arg(-c.y());
98 tag.insert(5, transform);
103 last = match.capturedEnd();
105 out += svg.mid(last);
115QPixmap orientedSvgPixmap(
const QString &resolvedPath,
const qreal angle,
const bool flipX,
const bool flipY)
120 const qreal canonAngle = std::fmod(std::fmod(angle, 360.0) + 360.0, 360.0);
124 const QString key = QLatin1String(
"wp_oriented_svg:") + resolvedPath + QLatin1Char(
'|')
125 + QString::number(canonAngle) + QLatin1Char(
'|')
126 + (flipX ? QLatin1Char(
'1') : QLatin1Char(
'0'))
127 + (flipY ? QLatin1Char(
'1') : QLatin1Char(
'0'));
130 if (QPixmapCache::find(key, &cached)) {
134 QFile file(resolvedPath);
135 if (!file.open(QIODevice::ReadOnly)) {
138 const QByteArray raw = file.readAll();
141 if (!raw.contains(
"<text")) {
144 const QByteArray modified = orientSvgTextNodes(raw, canonAngle, flipX, flipY);
146 QSvgRenderer renderer(modified);
147 if (!renderer.isValid()) {
151 QPixmap pixmap(renderer.defaultSize());
152 pixmap.fill(Qt::transparent);
153 QPainter painter(&pixmap);
154 painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform | QPainter::TextAntialiasing);
155 renderer.render(&painter);
158 QPixmapCache::insert(key, pixmap);
172std::shared_ptr<QSvgRenderer> sharedSvgRenderer(
const QString &resolvedPath,
const qreal angle,
const bool flipX,
const bool flipY)
174 static QHash<QString, std::shared_ptr<QSvgRenderer>> cache;
176 const qreal canonAngle = std::fmod(std::fmod(angle, 360.0) + 360.0, 360.0);
179 const QDateTime mtime = QFileInfo(resolvedPath).lastModified();
180 const QString key = resolvedPath + QLatin1Char(
'|')
181 + QString::number(mtime.toMSecsSinceEpoch()) + QLatin1Char(
'|')
182 + QString::number(canonAngle) + QLatin1Char(
'|')
183 + (flipX ? QLatin1Char(
'1') : QLatin1Char(
'0'))
184 + (flipY ? QLatin1Char(
'1') : QLatin1Char(
'0'));
186 if (
const auto it = cache.constFind(key); it != cache.constEnd()) {
190 QFile file(resolvedPath);
191 if (!file.open(QIODevice::ReadOnly)) {
192 cache.insert(key, nullptr);
195 QByteArray svg = file.readAll();
197 const bool oriented = angle != 0.0 || flipX || flipY;
198 if (oriented && svg.contains(
"<text")) {
199 svg = orientSvgTextNodes(svg, canonAngle, flipX, flipY);
202 auto renderer = std::make_shared<QSvgRenderer>(svg);
203 if (!renderer->isValid()) {
206 cache.insert(key, renderer);
221 const QString &pixmapPath)
224 m_pixmapPath = pixmapPath;
230 if (m_defaultAppearances.isEmpty() && !m_pixmapPath.isEmpty()) {
231 m_defaultAppearances = QStringList({m_pixmapPath});
242 if (m_hasCustomRenderPixmap) {
243 return m_owner->boundingRect().center();
245 return QRectF(m_pixmap.rect()).center();
250 setPixmap(m_usingDefaultAppearance ? m_defaultAppearances.at(index) : m_alternativeAppearances.at(index));
256 if (pixmapPath.isEmpty() || (pixmapPath == m_currentPixmapPath)) {
262 const QSize previousSize = m_pixmap.size();
268 m_owner->prepareGeometryChange();
275 const QString path = pixmapPath;
281 if (!m_basePixmap.load(path)) {
282 const QFileInfo info(path);
283 const QString reason = !info.exists()
284 ? tr(
"File does not exist")
286 ? tr(
"File is not readable")
287 : tr(
"Unknown reason");
290 m_basePixmap.load(m_defaultAppearances.constFirst());
291 m_pixmap = m_basePixmap;
295 m_currentPixmapPath = pixmapPath;
296 qCDebug(zero) <<
"Problem loading pixmapPath: " << path;
297 throw PANDACEPTION(
"Couldn't load pixmap: %1 (%2)", path, reason);
300 m_resolvedPixmapPath = path;
302 m_hasCustomRenderPixmap =
false;
310 if (m_pixmap.size() != previousSize) {
311 m_owner->invalidateRenderCache();
319 m_currentPixmapPath = pixmapPath;
324 if (index < 0 || index >= m_alternativeAppearances.size()) {
332 pixmap.load(m_alternativeAppearances.at(index));
334 return pixmap.isNull() ?
pixmap :
pixmap.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
340 m_owner->prepareGeometryChange();
341 const QSize previousSize = m_pixmap.size();
343 m_hasCustomRenderPixmap =
true;
346 if (m_pixmap.size() != previousSize) {
347 m_owner->invalidateRenderCache();
359 if (m_hasCustomRenderPixmap) {
367 const bool oriented = m_owner->rotatesGraphic()
368 && (m_owner->isFlippedX() || m_owner->isFlippedY() || (m_owner->rotation() != 0.0));
373 m_owner->prepareGeometryChange();
374 if (oriented && m_resolvedPixmapPath.endsWith(QLatin1String(
".svg"), Qt::CaseInsensitive)) {
375 const QPixmap variant = orientedSvgPixmap(m_resolvedPixmapPath, m_owner->rotation(), m_owner->isFlippedX(), m_owner->isFlippedY());
376 m_pixmap = variant.isNull() ? m_basePixmap : variant;
378 m_pixmap = m_basePixmap;
380 rebuildSvgRenderer();
384void ElementAppearance::rebuildSvgRenderer()
388 if (!m_resolvedPixmapPath.endsWith(QLatin1String(
".svg"), Qt::CaseInsensitive)) {
389 m_svgRenderer.reset();
398 const bool oriented = m_owner->rotatesGraphic()
399 && (m_owner->isFlippedX() || m_owner->isFlippedY() || (m_owner->rotation() != 0.0));
401 m_svgRenderer = sharedSvgRenderer(m_resolvedPixmapPath,
402 oriented ? m_owner->rotation() : 0.0,
403 oriented && m_owner->isFlippedX(),
404 oriented && m_owner->isFlippedY());
409 if (defaultAppearance) {
410 m_alternativeAppearances = m_defaultAppearances;
412 m_alternativeAppearances[0] = fileName;
415 m_usingDefaultAppearance = defaultAppearance;
421 if (index < 0 || index >= m_alternativeAppearances.size()) {
425 if (fileName.isEmpty()) {
426 m_alternativeAppearances[index] = m_defaultAppearances.at(index);
428 m_alternativeAppearances[index] = fileName;
431 m_usingDefaultAppearance = (m_alternativeAppearances == m_defaultAppearances);
437 if (index < 0 || index >= m_alternativeAppearances.size()) {
440 m_alternativeAppearances[index] = path;
445 m_usingDefaultAppearance = std::equal(
446 m_defaultAppearances.begin(), m_defaultAppearances.end(),
447 m_alternativeAppearances.begin(), m_alternativeAppearances.end()
454 for (
int i = 0; i < m_alternativeAppearances.size() && i < m_defaultAppearances.size(); ++i) {
455 const QString &appearance = m_alternativeAppearances.at(i);
456 if (appearance != m_defaultAppearances.at(i) && !appearance.startsWith(
":/")) {
457 result.append(appearance);
467 painter->setBrush(m_selectionBrush);
469 painter->setPen(QPen(m_selectionPen, 0.5, Qt::SolidLine));
471 painter->drawRoundedRect(boundingRect, 5, 5);
478 if (m_svgRenderer && m_svgRenderer->isValid()) {
480 painter->setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing |
481 QPainter::SmoothPixmapTransform,
true);
483 m_svgRenderer->render(painter, QRectF(QPointF(0, 0), QSizeF(m_svgRenderer->defaultSize())));
488 painter->drawPixmap(QPoint(0, 0), m_pixmap);
Common logging utilities, the Pandaception error type, and helper macros.
#define PANDACEPTION(msg,...)
#define qCDebug(category)
Owns a GraphicElement's pixmap / SVG appearance, appearance list, and selection paint.
Abstract base class for all graphical circuit elements.
Theme management types and singleton ThemeManager.
QStringList externalFiles() const
Returns the external (non-resource) appearance file paths the element depends on.
ElementAppearance(GraphicElement *owner)
Constructs the appearance bound to its owning owner element.
void recomputeUsingDefault()
Recomputes the using-default flag by comparing the alternative and default lists.
void setRenderPixmap(const QPixmap &pixmap)
QPointF pixmapCenter() const
Returns the centre point of the displayed pixmap in local coordinates.
~ElementAppearance()
Out-of-line so the unique_ptr to the forward-declared QSvgRenderer can be destroyed.
const QStringList & alternativeAppearances() const
Returns the active (possibly user-customised) appearance paths.
void applyOrientation()
Re-derives the displayed pixmap (and SVG renderer) from the owner's current orientation.
void render(QPainter *painter, const QRectF &boundingRect, const bool selected) const
Draws the selection highlight (when selected) and the SVG/pixmap body onto painter.
void setAppearance(const bool defaultAppearance, const QString &fileName)
Switches the appearance: restores defaults or applies fileName at slot 0.
const QStringList & defaultAppearances() const
Returns the built-in default appearance paths.
void setAlternativeAppearanceAt(const int index, const QString &path)
Sets one alternative-appearance slot without reloading the pixmap (serializer / subclass use).
void setPixmap(const int index)
Loads and applies the appearance at position index in the active list.
void seedFromMetadata(const QStringList &defaultAppearances, const QStringList &alternativeAppearances, const QString &pixmapPath)
QPixmap previewPixmapAt(const int index, const QSize &size) const
void applyTheme(const ThemeAttributes &theme)
Applies the selection-highlight colors from theme.
QPixmap pixmap() const
Returns the pixmap currently displayed.
void setAppearanceAt(const int index, const QString &fileName)
Sets a custom appearance at index (empty fileName restores that slot's default).
Abstract base class for all graphical circuit elements in wiRedPanda.
Contains all color attributes for a theme.