TechDraw: refactor click handles for editable fields

Fixes #19387
This commit is contained in:
Benjamin Bræstrup Sayoc
2025-03-16 01:35:04 +01:00
committed by Chris Hennes
parent 7ac21c5eff
commit 393ab112e5
2 changed files with 162 additions and 134 deletions

View File

@@ -24,6 +24,7 @@
#ifndef _PreComp_
# include <QDomDocument>
# include <QFile>
# include <QFontMetrics>
# include <QGraphicsColorizeEffect>
# include <QGraphicsEffect>
# include <QGraphicsSvgItem>
@@ -47,17 +48,85 @@
#include "Rez.h"
#include "TemplateTextField.h"
#include "ZVALUE.h"
#include "DrawGuiUtil.h"
namespace {
QFont getFont(QDomElement& elem)
{
if(elem.hasAttribute(QStringLiteral("font-family"))) {
return elem.attribute(QStringLiteral("font-family"));
}
QDomElement parent = elem.parentNode().toElement();
// Check if has parent
if(!parent.isNull()) {
// Traverse up and check if parent node has attribute
return getFont(parent);
}
// No attribute and no parent nodes left? Defaulting:
return QFont(QStringLiteral("sans"));
}
std::vector<QDomElement> getFCElements(QDomDocument& doc) {
QDomNodeList textElements = doc.elementsByTagName(QStringLiteral("text"));
std::vector<QDomElement> filteredTextElements;
filteredTextElements.reserve(textElements.size());
for(int i = 0; i < textElements.size(); i++) {
QDomElement textElement = textElements.at(i).toElement();
if(textElement.hasAttribute(QStringLiteral(FREECAD_ATTR_EDITABLE))) {
filteredTextElements.push_back(textElement);
}
}
return filteredTextElements;
}
void applyWorkaround(QByteArray& svgCode)
{
QDomDocument doc;
doc.setContent(svgCode);
// Example <text font-size="12px"><tspan font-size"12px">sadasd</tspan></text>
// QSvgRenderer::boundsOnElement calculates the bounds of the text element using text width of the `tspan`
// but faultly the text height of the `text` element that might have another font size. The width
// of a `tspan will also always be one character too wide.
// Workaround: apply the font-size of the `tspan` to its parent `text` and remove `tspan` completely
std::vector<QDomElement> textElements = getFCElements(doc);
for(QDomElement& textElement : textElements) {
QDomElement tspan = textElement.firstChildElement(QStringLiteral("tspan"));
if(tspan.isNull()) {
continue;
}
if(tspan.hasAttribute(QStringLiteral("font-size"))) {
QString fontSize = tspan.attribute(QStringLiteral("font-size"));
textElement.setAttribute(QStringLiteral("font-size"), fontSize);
}
if(tspan.hasAttribute(QStringLiteral("x"))) {
QString x = tspan.attribute(QStringLiteral("x"));
textElement.setAttribute(QStringLiteral("x"), x);
}
if(tspan.hasAttribute(QStringLiteral("y"))) {
QString y = tspan.attribute(QStringLiteral("y"));
textElement.setAttribute(QStringLiteral("y"), y);
}
// Delete tspan, but keep tspan content
textElement.replaceChild(tspan.firstChild(), tspan);
}
svgCode = doc.toByteArray();
}
} // anonymous namespace
using namespace TechDrawGui;
using namespace TechDraw;
QGISVGTemplate::QGISVGTemplate(QGSPage* scene) : QGITemplate(scene), firstTime(true)
QGISVGTemplate::QGISVGTemplate(QGSPage* scene) : QGITemplate(scene),
m_svgItem(new QGraphicsSvgItem(this)),
m_svgRender(new QSvgRenderer())
{
m_svgItem = new QGraphicsSvgItem(this);
m_svgRender = new QSvgRenderer();
m_svgItem->setSharedRenderer(m_svgRender);
m_svgItem->setFlags(QGraphicsItem::ItemClipsToShape);
@@ -73,23 +142,20 @@ QGISVGTemplate::~QGISVGTemplate() { delete m_svgRender; }
void QGISVGTemplate::openFile(const QFile& file) { Q_UNUSED(file); }
void QGISVGTemplate::load(const QByteArray& svgCode)
void QGISVGTemplate::load(QByteArray svgCode)
{
applyWorkaround(svgCode);
m_svgRender->load(svgCode);
QSize size = m_svgRender->defaultSize();
m_svgItem->setSharedRenderer(m_svgRender);
if (firstTime) {
createClickHandles();
firstTime = false;
}
createClickHandles();
//convert from pixels or mm or inches in svg file to mm page size
TechDraw::DrawSVGTemplate* tmplte = getSVGTemplate();
double xaspect, yaspect;
xaspect = tmplte->getWidth() / static_cast<double>(size.width());
yaspect = tmplte->getHeight() / static_cast<double>(size.height());
double xaspect = tmplte->getWidth() / static_cast<double>(size.width());
double yaspect = tmplte->getHeight() / static_cast<double>(size.height());
QTransform qtrans;
qtrans.translate(0.0, Rez::guiX(-tmplte->getHeight()));
@@ -97,8 +163,8 @@ void QGISVGTemplate::load(const QByteArray& svgCode)
m_svgItem->setTransform(qtrans);
if (Preferences::lightOnDark()) {
QColor color = PreferencesGui::getAccessibleQColor(QColor(Qt::black));
QGraphicsColorizeEffect* colorizeEffect = new QGraphicsColorizeEffect();
auto color = PreferencesGui::getAccessibleQColor(QColor(Qt::black));
auto* colorizeEffect = new QGraphicsColorizeEffect();
colorizeEffect->setColor(color);
m_svgItem->setGraphicsEffect(colorizeEffect);
}
@@ -115,9 +181,8 @@ TechDraw::DrawSVGTemplate* QGISVGTemplate::getSVGTemplate()
if (pageTemplate && pageTemplate->isDerivedFrom<TechDraw::DrawSVGTemplate>()) {
return static_cast<TechDraw::DrawSVGTemplate*>(pageTemplate);
}
else {
return nullptr;
}
return nullptr;
}
void QGISVGTemplate::draw()
@@ -132,12 +197,30 @@ void QGISVGTemplate::draw()
void QGISVGTemplate::updateView(bool update)
{
Q_UNUSED(update);
if (update) {
clearClickHandles();
createClickHandles();
}
draw();
}
void QGISVGTemplate::clearClickHandles()
{
prepareGeometryChange();
constexpr int TemplateTextFieldType{QGraphicsItem::UserType + 160};
auto templateChildren = childItems();
for (auto& child : templateChildren) {
if (child->type() == TemplateTextFieldType) {
child->hide();
scene()->removeItem(child);
delete child;
}
}
}
void QGISVGTemplate::createClickHandles()
{
prepareGeometryChange();
TechDraw::DrawSVGTemplate* svgTemplate = getSVGTemplate();
if (svgTemplate->isRestoring()) {
//the embedded file is not available yet, so just return
@@ -167,128 +250,75 @@ void QGISVGTemplate::createClickHandles()
//TODO: Find location of special fields (first/third angle) and make graphics items for them
QColor editClickBoxColor = PreferencesGui::templateClickBoxColor();
auto textMap = svgTemplate->EditableTexts.getValues();
TechDraw::XMLQuery query(templateDocument);
// XPath query to select all <text> nodes with "freecad:editable" attribute
// XPath query to select all <tspan> nodes whose <text> parent
// has "freecad:editable" attribute
query.processItems(QStringLiteral("declare default element namespace \"" SVG_NS_URI "\"; "
"declare namespace freecad=\"" FREECAD_SVG_NS_URI "\"; "
"//text[@" FREECAD_ATTR_EDITABLE "]/tspan"),
[&](QDomElement& tspan) -> bool {
QString fontSizeString = tspan.attribute(QStringLiteral("font-size"));
QDomElement textElement = tspan.parentNode().toElement();
QString textAnchorString = textElement.attribute(QStringLiteral("text-anchor"));
QString name = textElement.attribute(QString::fromUtf8(FREECAD_ATTR_EDITABLE));
double x = Rez::guiX(
textElement.attribute(QStringLiteral("x"), QStringLiteral("0.0")).toDouble());
double y = Rez::guiX(
textElement.attribute(QStringLiteral("y"), QStringLiteral("0.0")).toDouble());
if (name.isEmpty()) {
Base::Console().Warning(
"QGISVGTemplate::createClickHandles - no name for editable text at %f, %f\n", x, y);
return true;
std::vector<QDomElement> textElements = getFCElements(templateDocument);
for(QDomElement& textElement : textElements) {
// Get elements bounding box of text
QString id = textElement.attribute(QStringLiteral("id"));
QRectF textRect = m_svgRender->boundsOnElement(id);
// Get tight bounding box of text
QDomElement tspan = textElement.firstChildElement();
QFont font = getFont(tspan);
QFontMetricsF fm(font);
double factor = textRect.height() / fm.height(); // Correcting font metrics and SVG text due to different font sizes
QRectF tightTextRect = textRect.adjusted(0.0, 0.0, 0.0, -fm.descent() * factor);
tightTextRect.setTop(tightTextRect.bottom() - fm.capHeight() * factor);
// Ensure min size; if no text content, tightTextRect will have no size
// and factor will also be incorrect
// Default font size guess. Getting attribute seems complicated, as it can have different units
// and both be in style attribute and native attribute
font.setPointSizeF(1.5);
fm = QFontMetricsF(font);
if (tightTextRect.height() < fm.capHeight()) {
tightTextRect.setTop(tightTextRect.bottom() - fm.capHeight());
}
std::string editableNameString = textMap[name.toStdString()];
// default box size
double textHeight{0};
QString style = textElement.attribute(QStringLiteral("style"));
if (!style.isEmpty()) {
// get text attributes from style element
textHeight = getFontSizeFromStyle(style);
double charWidth = fm.horizontalAdvance(QLatin1Char(' '));
if(tightTextRect.width() < charWidth) {
tightTextRect.setWidth(charWidth);
}
if (textHeight == 0) {
textHeight = getFontSizeFromElement(fontSizeString);
}
if (textHeight == 0.0) {
textHeight = Preferences::labelFontSizeMM() * 3.78; // 3.78 = px/mm
}
QGraphicsTextItem textItemForLength;
QFont fontForLength(Preferences::labelFontQString());
fontForLength.setPixelSize(textHeight);
textItemForLength.setFont(fontForLength);
textItemForLength.setPlainText(QString::fromStdString(editableNameString));
auto brect = textItemForLength.boundingRect();
auto newLength = brect.width();
double charWidth = newLength / editableNameString.length();
if (textAnchorString == QStringLiteral("middle")) {
x = x - editableNameString.length() * charWidth / 2;
}
double textLength = editableNameString.length() * charWidth;
textLength = std::max(charWidth, textLength);
// Transform tight bounding box of text
QPolygonF tightTextPoly(tightTextRect); // Polygon because rect cannot be rotated
QTransform SVGTransform = m_svgRender->transformForElement(id);
tightTextPoly = SVGTransform.map(tightTextPoly); // SVGTransform always before templateTransform
QTransform templateTransform;
templateTransform.translate(0.0, Rez::guiX(-getSVGTemplate()->getHeight()));
templateTransform.scale(Rez::getRezFactor(), Rez::getRezFactor());
tightTextPoly = templateTransform.map(tightTextPoly);
QString name = textElement.attribute(QStringLiteral(FREECAD_ATTR_EDITABLE));
auto item(new TemplateTextField(this, svgTemplate, name.toStdString()));
auto autoValue = svgTemplate->getAutofillByEditableName(name);
item->setAutofill(autoValue);
double pad = 1.0;
double top = Rez::guiX(-svgTemplate->getHeight()) + y - textHeight - pad;
double bottom = top + textHeight + 2.0 * pad;
double left = x - pad;
item->setRectangle(QRectF(left, top,
newLength + 2.0 * pad, textHeight + 2.0 * pad));
item->setLine(QPointF( left, bottom),
QPointF(left + newLength + 2.0 * pad, bottom));
item->setLineColor(editClickBoxColor);
QMarginsF padding(
0.0,
0.15 * tightTextRect.height(),
0.0,
0.2 * tightTextRect.height()
);
QRectF clickrect = tightTextRect.marginsAdded(padding);
QPolygonF clickpoly = SVGTransform.map(clickrect);
clickpoly = templateTransform.map(clickpoly);
item->setRectangle(clickpoly.boundingRect()); // TODO: templateTextField doesn't support polygon yet
QPointF bottomLeft = clickpoly.at(3);
QPointF bottomRight = clickpoly.at(2);
item->setLine(bottomLeft, bottomRight);
item->setLineColor(PreferencesGui::templateClickBoxColor());
item->setZValue(ZVALUE::SVGTEMPLATE + 1);
addToGroup(item);
textFields.push_back(item);
return true;
});
}
}
//! find the font-size hidden in a style element
double QGISVGTemplate::getFontSizeFromStyle(QString style)
{
if (style.isEmpty()) {
return 0.0;
}
// get text attributes from style element
QRegularExpression rxFontSize(QStringLiteral("font-size:([0-9]*.?[0-9]*)px;"));
QRegularExpressionMatch match;
int pos{0};
pos = style.indexOf(rxFontSize, 0, &match);
if (pos != -1) {
return Rez::guiX(match.captured(1).toDouble());
}
return 0.0;
}
//! find the font-size hidden in a style element
double QGISVGTemplate::getFontSizeFromElement(QString element)
{
if (element.isEmpty()) {
return 0.0;
}
// font-size="3.95px"
QRegularExpression rxFontSize(QStringLiteral("([0-9]*.?[0-9]*)px"));
QRegularExpressionMatch match;
int pos{0};
pos = element.indexOf(rxFontSize, 0, &match);
if (pos != -1) {
return Rez::guiX(match.captured(1).toDouble());
}
return 0.0;
}
#include <Mod/TechDraw/Gui/moc_QGISVGTemplate.cpp>

View File

@@ -25,25 +25,24 @@
#include <Mod/TechDraw/TechDrawGlobal.h>
class QGraphicsScene;
class QGraphicsSvgItem;
class QSvgRenderer;
class QFile;
class QString;
#include "QGITemplate.h"
namespace TechDraw
{
class DrawSVGTemplate;
}
#include "QGITemplate.h"
namespace TechDrawGui
{
class QGSPage;
class TechDrawGuiExport QGISVGTemplate: public QGITemplate
class TechDrawGuiExport QGISVGTemplate : public TechDrawGui::QGITemplate
{
Q_OBJECT
@@ -64,13 +63,12 @@ public:
protected:
void openFile(const QFile& file);
void load(const QByteArray& svgCode);
void createClickHandles(void);
static double getFontSizeFromStyle(QString style);
static double getFontSizeFromElement(QString element);
void load(QByteArray svgCode);
protected:
bool firstTime;
void createClickHandles();
void clearClickHandles();
private:
QGraphicsSvgItem* m_svgItem;
QSvgRenderer* m_svgRender;
};// class QGISVGTemplate