Merge master into feat/1335-flip-elements

Conflicts with the DXF blocks export (#1350) and the picture factory
leak fix. The mirror now lives in drawSymbol(); a mirrored symbol is
drawn in full rather than as an INSERT of its block, since a negative
INSERT scale would mirror its texts too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-07 21:11:00 +13:00
84 changed files with 5539 additions and 425 deletions
+14
View File
@@ -301,10 +301,16 @@ set(QET_SRC_FILES
${QET_DIR}/sources/shortcutmanager.h
${QET_DIR}/sources/shortcutbarsettings.cpp
${QET_DIR}/sources/shortcutbarsettings.h
${QET_DIR}/sources/diagramtoolbarsettings.cpp
${QET_DIR}/sources/diagramtoolbarsettings.h
${QET_DIR}/sources/toolbarsettings.cpp
${QET_DIR}/sources/toolbarsettings.h
${QET_DIR}/sources/diagramcontexttoolbar.cpp
${QET_DIR}/sources/diagramcontexttoolbar.h
${QET_DIR}/sources/diagramgestureoverlay.cpp
${QET_DIR}/sources/diagramgestureoverlay.h
${QET_DIR}/sources/gesturesettings.cpp
${QET_DIR}/sources/gesturesettings.h
${QET_DIR}/sources/commandsearchpopup.cpp
${QET_DIR}/sources/commandsearchpopup.h
${QET_DIR}/sources/titleblockcell.cpp
@@ -856,6 +862,14 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ui/configpage/shortcutsconfigpage.h
${QET_DIR}/sources/ui/configpage/shortcutbarconfigpage.cpp
${QET_DIR}/sources/ui/configpage/shortcutbarconfigpage.h
${QET_DIR}/sources/ui/customizedialog.cpp
${QET_DIR}/sources/ui/customizedialog.h
${QET_DIR}/sources/ui/configpage/toolbarcommandsconfigpage.cpp
${QET_DIR}/sources/ui/configpage/toolbarcommandsconfigpage.h
${QET_DIR}/sources/ui/configpage/toolbarsconfigpage.cpp
${QET_DIR}/sources/ui/configpage/toolbarsconfigpage.h
${QET_DIR}/sources/ui/configpage/gesturesconfigpage.cpp
${QET_DIR}/sources/ui/configpage/gesturesconfigpage.h
${QET_DIR}/sources/undocommand/addelementtextcommand.cpp
${QET_DIR}/sources/undocommand/addelementtextcommand.h
+17 -4
View File
@@ -708,13 +708,26 @@ void BorderTitleBlock::drawDxf(
// render the titleblock, using the TitleBlockTemplate object
if (display_titleblock_) {
Createdxf::layer = DxfExport::Layer::TitleBlock;
//qp -> translate(titleblock_rect_.topLeft());
QRectF rect = titleBlockRect();
//Laid out horizontally, then turned like the QPainter in
//draw(): a quarter turn counter-clockwise around its top
//left, which for a title block at the right is the bottom
//right of the diagram (issue #1339).
QRectF rect = titleBlockRectForQPainter();
QTransform dxf_transform;
if (m_edge != Qt::BottomEdge)
{
const QPointF pivot(rect.left() * Createdxf::xScale,
Createdxf::sheetHeight
- rect.top() * Createdxf::yScale);
dxf_transform.translate(pivot.x(), pivot.y());
dxf_transform.rotate(90);
dxf_transform.translate(-pivot.x(), -pivot.y());
}
m_titleblock_template_renderer -> renderDxf(rect,
rect.width(),
file_path,
color);
//qp -> translate(-titleblock_rect_.topLeft());
color,
dxf_transform);
}
// Transform back to QET scale
+9 -2
View File
@@ -331,7 +331,8 @@ int exportImages(QETProject &project, const QString &format,
/// One DXF file per diagram, written by the same code as the export dialog
/// (DxfExport) with the dialog's default options -- the export settings of
/// the preferences -- so both give the same file.
int exportDxf(QETProject &project, const QString &out_dir, bool showTerminals)
int exportDxf(QETProject &project, const QString &out_dir, bool showTerminals,
bool dxfBlocks, bool dxfAttributes)
{
const QList<Diagram *> diagrams = project.diagrams();
if (diagrams.isEmpty()) {
@@ -344,6 +345,10 @@ int exportDxf(QETProject &project, const QString &out_dir, bool showTerminals)
properties.format = QStringLiteral("DXF");
if (showTerminals)
properties.draw_terminals = true;
if (dxfBlocks || dxfAttributes)
properties.dxf_blocks = true;
if (dxfAttributes)
properties.dxf_attributes = true;
int index = 0;
bool has_images = false;
@@ -982,6 +987,8 @@ int run(const QStringList &args)
// collected below.
QStringList filtered = args;
const bool showTerminals = filtered.removeAll("--show-terminals") > 0;
const bool dxfBlocks = filtered.removeAll("--dxf-blocks") > 0;
const bool dxfAttributes = filtered.removeAll("--dxf-attributes") > 0;
// --no-slaves and --no-junctions leave rows out of --export-bom.
const bool includeSlaves = filtered.removeAll("--no-slaves") == 0;
const bool includeJunctions = filtered.removeAll("--no-junctions") == 0;
@@ -1038,7 +1045,7 @@ int run(const QStringList &args)
if (format == "pdf")
return exportPdf(project, output, showTerminals);
if (format == "dxf")
return exportDxf(project, output, showTerminals);
return exportDxf(project, output, showTerminals, dxfBlocks, dxfAttributes);
if (format == "cables" || format == "wires")
return exportCsv(project, format, output);
if (format == "bom")
+6 -1
View File
@@ -45,7 +45,7 @@ namespace CLIExport {
qelectrotech --export-pdf <project.qet> <output.pdf> [--show-terminals]
qelectrotech --export-png <project.qet> <output_dir> [--show-terminals]
qelectrotech --export-svg <project.qet> <output_dir> [--show-terminals]
qelectrotech --export-dxf <project.qet> <output_dir> [--show-terminals]
qelectrotech --export-dxf <project.qet> <output_dir> [--show-terminals] [--dxf-blocks] [--dxf-attributes]
qelectrotech --export-cables <project.qet> <output.csv>
qelectrotech --export-wires <project.qet> <output.csv>
qelectrotech --export-bom <project.qet> <output.csv> [--no-slaves] [--no-junctions]
@@ -66,6 +66,11 @@ namespace CLIExport {
does; off by default, matching the GUI export dialog's
default. For DXF, draws the terminal markers. Has no effect on
the list export modes.
--dxf-blocks: write each symbol as a DXF block, placed with one
INSERT per use, so a CAD program selects it as one object.
--dxf-attributes: as --dxf-blocks, and a symbol's texts (its label,
function...) are attributes of its INSERT. LibreCAD does not
show attributes.
cables: wiring list (one row per conductor) as CSV.
wires: list of distinct wire numbers as CSV.
bom: bill of materials (one row per element) as CSV.
+279 -130
View File
@@ -22,6 +22,9 @@
#include <QString>
#include "dxfexport.h"
#include <QtMath>
#include <cmath>
const double Createdxf::sheetWidth = 4000;
const double Createdxf::sheetHeight = 2700;
@@ -65,7 +68,16 @@ Createdxf::~Createdxf()
/* Header section of every DXF file.
@param layers : the layers the entities will be written on, declared in
the LAYER table beside layer "0", which every DXF has. */
void Createdxf::dxfBegin (const QString& fileName, const QStringList &layers)
/**
@brief Createdxf::dxfBegin
Write the header, the tables with @a layers, and the BLOCKS section,
then open the ENTITIES section. @a writeBlocks, if given, is called
inside the BLOCKS section to write the block definitions
(dxfBlockBegin(), the entities, dxfBlockEnd()): a block has to be
defined before any entity, so before anything is drawn.
*/
void Createdxf::dxfBegin (const QString& fileName, const QStringList &layers,
const std::function<void()> &writeBlocks)
{
layer = QStringLiteral("0");
@@ -273,17 +285,203 @@ void Createdxf::dxfBegin (const QString& fileName, const QStringList &layers)
To_Dxf << "SECTION" << "\r\n";
To_Dxf << 2 << "\r\n";
To_Dxf << "BLOCKS" << "\r\n";
file.close();
}
if (writeBlocks)
writeBlocks();
QFile entities(fileName);
if (entities.open(QFile::Append)) {
QTextStream To_Dxf(&entities);
To_Dxf << 0 << "\r\n";
To_Dxf << "ENDSEC" << "\r\n";
To_Dxf << 0 << "\r\n";
To_Dxf << "SECTION" << "\r\n";
To_Dxf << 2 << "\r\n";
To_Dxf << "ENTITIES" << "\r\n";
file.close();
entities.close();
}
}
}
/**
@brief Createdxf::dxfBlockBegin
Start the definition of block @a name, with its base point at @a x,
@a y. Only inside dxfBegin()'s writeBlocks; every entity written until
dxfBlockEnd() belongs to the block.
@param hasAttributes : the block has attribute definitions
*/
void Createdxf::dxfBlockBegin(const QString &fileName,
const QString &name,
double x,
double y,
bool hasAttributes)
{
QFile file(fileName);
if (!file.open(QFile::Append))
return;
QTextStream To_Dxf(&file);
To_Dxf << 0 << "\r\n";
To_Dxf << "BLOCK" << "\r\n";
To_Dxf << 8 << "\r\n";
To_Dxf << "0" << "\r\n";
To_Dxf << 2 << "\r\n";
To_Dxf << name << "\r\n";
To_Dxf << 70 << "\r\n";
To_Dxf << (hasAttributes ? 2 : 0) << "\r\n";
To_Dxf << 10 << "\r\n";
To_Dxf << x << "\r\n";
To_Dxf << 20 << "\r\n";
To_Dxf << y << "\r\n";
To_Dxf << 30 << "\r\n";
To_Dxf << 0.0 << "\r\n";
file.close();
}
/**
@brief Createdxf::dxfBlockEnd
End the block started by dxfBlockBegin().
*/
void Createdxf::dxfBlockEnd(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QFile::Append))
return;
QTextStream To_Dxf(&file);
To_Dxf << 0 << "\r\n";
To_Dxf << "ENDBLK" << "\r\n";
To_Dxf << 8 << "\r\n";
To_Dxf << "0" << "\r\n";
file.close();
}
/**
@brief Createdxf::drawInsert
Place block @a name at @a x, @a y, turned counter-clockwise by
@a rotation degrees, on the current layer, followed by its
@a attributes.
*/
void Createdxf::drawInsert(const QString &fileName,
const QString &name,
double x,
double y,
double rotation,
const QList<Attribute> &attributes)
{
QFile file(fileName);
if (!file.open(QFile::Append))
return;
QTextStream To_Dxf(&file);
To_Dxf << 0 << "\r\n";
To_Dxf << "INSERT" << "\r\n";
To_Dxf << 8 << "\r\n";
To_Dxf << layer << "\r\n";
if (!attributes.isEmpty()) {
To_Dxf << 66 << "\r\n";
To_Dxf << 1 << "\r\n";
}
To_Dxf << 2 << "\r\n";
To_Dxf << name << "\r\n";
To_Dxf << 10 << "\r\n";
To_Dxf << x << "\r\n";
To_Dxf << 20 << "\r\n";
To_Dxf << y << "\r\n";
To_Dxf << 30 << "\r\n";
To_Dxf << 0.0 << "\r\n";
To_Dxf << 50 << "\r\n";
To_Dxf << rotation << "\r\n";
for (const Attribute &attribute : attributes) {
To_Dxf << 0 << "\r\n";
To_Dxf << "ATTRIB" << "\r\n";
To_Dxf << 8 << "\r\n";
To_Dxf << (attribute.layer.isEmpty() ? layer : attribute.layer) << "\r\n";
To_Dxf << 62 << "\r\n";
To_Dxf << entityColour(attribute.colour) << "\r\n";
To_Dxf << 10 << "\r\n";
To_Dxf << attribute.x << "\r\n";
To_Dxf << 20 << "\r\n";
To_Dxf << attribute.y << "\r\n";
To_Dxf << 30 << "\r\n";
To_Dxf << 0.0 << "\r\n";
To_Dxf << 40 << "\r\n";
To_Dxf << attribute.height << "\r\n";
To_Dxf << 1 << "\r\n";
To_Dxf << singleLine(attribute.text) << "\r\n";
To_Dxf << 2 << "\r\n";
To_Dxf << attribute.tag << "\r\n";
To_Dxf << 70 << "\r\n";
To_Dxf << (attribute.invisible ? 1 : 0) << "\r\n";
To_Dxf << 50 << "\r\n";
To_Dxf << attribute.rotation << "\r\n";
To_Dxf << 41 << "\r\n";
To_Dxf << attribute.xScaleW << "\r\n";
}
if (!attributes.isEmpty()) {
To_Dxf << 0 << "\r\n";
To_Dxf << "SEQEND" << "\r\n";
To_Dxf << 8 << "\r\n";
To_Dxf << layer << "\r\n";
}
file.close();
}
/**
@brief Createdxf::drawAttdef
Define an attribute of the block being written (between
dxfBlockBegin() and dxfBlockEnd()): its tag, where it sits and how it
looks; @a attribute's text is the default value.
*/
void Createdxf::drawAttdef(const QString &fileName, const Attribute &attribute)
{
QFile file(fileName);
if (!file.open(QFile::Append))
return;
QTextStream To_Dxf(&file);
To_Dxf << 0 << "\r\n";
To_Dxf << "ATTDEF" << "\r\n";
To_Dxf << 8 << "\r\n";
To_Dxf << (attribute.layer.isEmpty() ? layer : attribute.layer) << "\r\n";
To_Dxf << 62 << "\r\n";
To_Dxf << entityColour(attribute.colour) << "\r\n";
To_Dxf << 10 << "\r\n";
To_Dxf << attribute.x << "\r\n";
To_Dxf << 20 << "\r\n";
To_Dxf << attribute.y << "\r\n";
To_Dxf << 30 << "\r\n";
To_Dxf << 0.0 << "\r\n";
To_Dxf << 40 << "\r\n";
To_Dxf << attribute.height << "\r\n";
To_Dxf << 1 << "\r\n";
To_Dxf << singleLine(attribute.text) << "\r\n";
To_Dxf << 3 << "\r\n";
To_Dxf << attribute.tag << "\r\n";
To_Dxf << 2 << "\r\n";
To_Dxf << attribute.tag << "\r\n";
To_Dxf << 70 << "\r\n";
To_Dxf << (attribute.invisible ? 1 : 0) << "\r\n";
To_Dxf << 50 << "\r\n";
To_Dxf << attribute.rotation << "\r\n";
To_Dxf << 41 << "\r\n";
To_Dxf << attribute.xScaleW << "\r\n";
file.close();
}
/**
@brief Createdxf::blockName
@return @a name as a block or attribute name an R10 reader accepts:
upper case, only A-Z, 0-9, _, - and $, at most 31 characters
*/
QString Createdxf::blockName(const QString &name)
{
QString result;
for (const QChar c : name.toUpper()) {
const ushort u = c.unicode();
const bool ok = (u >= 'A' && u <= 'Z') || (u >= '0' && u <= '9')
|| u == '_' || u == '-' || u == '$';
result += ok ? c : QChar('_');
}
return result.left(31);
}
/**
@brief Createdxf::dxfEnd
End Section of every DXF File
@@ -504,6 +702,24 @@ int Createdxf::dxfColor(QPen pen) {
return Createdxf::dxfColor(pen.color());
}
/**
@brief Createdxf::drawArcEllipse
Draw an arc of an ellipse, or a whole ellipse, in DXF units.
@param x, y : top left of the ellipse's bounding rect (y is the top, the
DXF Y axis going up)
@param w, h : width and height of that rect
@param startAngle, spanAngle : in degrees, as QPainter::drawArc() takes
them: counter-clockwise from 3 o'clock
@param hotspot_x, hotspot_y, rotation_angle : the whole shape is turned
clockwise by rotation_angle degrees around the hotspot, as a rotated
symbol is
A circle, or an arc of one, is written as one exact CIRCLE or ARC. R10,
the version this file declares, has no ELLIPSE entity, so a true
ellipse is written as a polyline through points of the curve, one every
5 degrees. It used to be four ARCs fitted through a point between the
curve and its chord, which drew every circle as a flattened diamond
(issue #1339).
*/
void Createdxf::drawArcEllipse(
const QString &file_path,
qreal x,
@@ -517,138 +733,46 @@ void Createdxf::drawArcEllipse(
qreal rotation_angle,
const int &colorcode)
{
// vector of parts of arc (stored as a pair of startAngle and spanAngle) for each quadrant.
QVector< QPair<qreal,qreal> > arc_parts_vector;
if (qFuzzyIsNull(spanAngle) || w <= 0 || h <= 0)
return;
if (spanAngle > 0) {
qreal start = startAngle;
qreal span;
int i;
for ( i = startAngle; i < startAngle+spanAngle; i++ ) {
int absolute_theta = (i > 0) ? i : -i;
if (absolute_theta == 0 || absolute_theta == 90 ||
absolute_theta == 180 || absolute_theta == 270 ||
absolute_theta == 360) {
span = i - start;
QPair<qreal, qreal> newPart(start,span);
arc_parts_vector.push_back(newPart);
start = i;
}
}
if (start != i) {
span = i - start;
QPair<qreal, qreal> newPart(start,span);
arc_parts_vector.push_back(newPart);
}
} else {
qreal start = startAngle;
qreal span;
int i;
for ( i = startAngle; i > startAngle+spanAngle; i-- ) {
int absolute_theta = (i > 0) ? i : -i;
if (absolute_theta == 0 || absolute_theta == 90 ||
absolute_theta == 180 || absolute_theta == 270 ||
absolute_theta == 360) {
span = i - start;
QPair<qreal, qreal> newPart(start,span);
arc_parts_vector.push_back(newPart);
start = i;
}
}
if (start != i) {
span = i - start;
QPair<qreal, qreal> newPart(start,span);
arc_parts_vector.push_back(newPart);
const qreal a = w/2;
const qreal b = h/2;
const QPointF center = DxfExport::rotation_transformed(
x + a, y - b, hotspot_x, hotspot_y, rotation_angle);
const bool full_turn = qAbs(spanAngle) >= 360;
if (qAbs(a - b) <= 1e-6 * qMax(a, b))
{
if (full_turn) {
drawCircle(file_path, a, center.x(), center.y(), colorcode);
return;
}
//A DXF ARC always runs counter-clockwise from 50 to 51.
qreal start = startAngle - rotation_angle;
if (spanAngle < 0)
start += spanAngle;
start = std::fmod(start, 360.0);
if (start < 0)
start += 360;
drawArc(file_path, center.x(), center.y(), a,
start, start + qAbs(spanAngle), colorcode);
return;
}
for (int i = 0; i < arc_parts_vector.size(); i++) {
QPair<qreal,qreal> arc = arc_parts_vector[i];
if (arc.second == 0)
continue;
qreal arc_startAngle = arc.first * 3.142/180;
qreal arc_spanAngle = arc.second * 3.142/180;
qreal a = w/2;
qreal b = h/2;
qreal x1 = x + w/2 + a*cos(arc_startAngle);
qreal y1 = y - h/2 + b*sin(arc_startAngle);
qreal x2 = x + w/2 + a*cos(arc_startAngle + arc_spanAngle);
qreal y2 = y - h/2 + b*sin(arc_startAngle + arc_spanAngle);
qreal mid_ellipse_x = x + w/2 + a*cos(arc_startAngle + arc_spanAngle/2);
qreal mid_ellipse_y = y - h/2 + b*sin(arc_startAngle + arc_spanAngle/2);
qreal mid_line_x = (x1+x2)/2;
qreal mid_line_y = (y1+y2)/2;
qreal x3 = (mid_ellipse_x + mid_line_x)/2;
qreal y3 = (mid_ellipse_y + mid_line_y)/2;
// find circumcenter of points (x1,y1), (x3,y3) and (x2,y2)
qreal a1 = 2*x2 - 2*x1;
qreal b1 = 2*y2 - 2*y1;
qreal c1 = x1*x1 + y1*y1 - x2*x2 - y2*y2;
qreal a2 = 2*x3 - 2*x1;
qreal b2 = 2*y3 - 2*y1;
qreal c2 = x1*x1 + y1*y1 - x3*x3 - y3*y3;
qreal center_x = (b1*c2 - b2*c1) / (a1*b2 - a2*b1);
qreal center_y = (a1*c2 - a2*c1) / (b1*a2 - b2*a1);
qreal radius = sqrt( (x1-center_x)*(x1-center_x) + (y1-center_y)*(y1-center_y) );
if ( x1 > center_x && y1 > center_y )
arc_startAngle = asin( (y1 - center_y) / radius );
else if ( x1 > center_x && y1 < center_y )
arc_startAngle = 3.142*2 - asin( (center_y - y1) / radius );
else if ( x1 < center_x && y1 < center_y )
arc_startAngle = 3.142 + asin( (center_y - y1) / radius );
else
arc_startAngle = 3.142 - asin( (y1 - center_y) / radius );
qreal arc_endAngle;
if ( x2 > center_x && y2 > center_y )
arc_endAngle = asin( (y2 - center_y) / radius );
else if ( x2 > center_x && y2 < center_y )
arc_endAngle = 3.142*2 - asin( (center_y - y2) / radius );
else if ( x2 < center_x && y2 < center_y )
arc_endAngle = 3.142 + asin( (center_y - y2) / radius );
else
arc_endAngle = 3.142 - asin( (y2 - center_y) / radius );
if (arc_endAngle < arc_startAngle) {
qreal temp = arc_startAngle;
arc_startAngle = arc_endAngle;
arc_endAngle = temp;
}
QPointF transformed_point = DxfExport::rotation_transformed(
center_x,
center_y,
hotspot_x,
hotspot_y,
rotation_angle);
center_x = transformed_point.x();
center_y = transformed_point.y();
arc_endAngle *= 180/3.142;
arc_startAngle *= 180/3.142;
arc_endAngle -= rotation_angle;
arc_startAngle -= rotation_angle;
drawArc(
file_path,
center_x,
center_y,
radius,
arc_startAngle,
arc_endAngle,
colorcode);
const qreal span = full_turn ? 360 : spanAngle;
const int steps = qMax(2, qCeil(qAbs(span) / 5));
QPolygonF poly;
poly.reserve(steps + 1);
for (int i = 0 ; i <= steps ; ++i)
{
const qreal theta = qDegreesToRadians(startAngle + span * i / steps);
poly << DxfExport::rotation_transformed(
x + a + a*std::cos(theta),
y - b + b*std::sin(theta),
hotspot_x, hotspot_y, rotation_angle);
}
drawPolyline(file_path, poly, colorcode, true);
}
@@ -809,6 +933,31 @@ void Createdxf::drawTextAligned(
double xAlign,
double xScaleW,
int colour)
{
drawTextAligned(fileName, text, x, y, height, rotation, oblique,
hAlign, vAlign, xAlign, y, xScaleW, colour);
}
/**
@brief Createdxf::drawTextAligned
As above, with the second alignment point given in full (@a xAlign,
@a yAlign) instead of on the same horizontal line as the insertion
point, which is what a rotated aligned text needs.
*/
void Createdxf::drawTextAligned(
const QString& fileName,
const QString& text,
double x,
double y,
double height,
double rotation,
double oblique,
int hAlign,
int vAlign,
double xAlign,
double yAlign,
double xScaleW,
int colour)
{
if (!fileName.isEmpty()) {
QFile file(fileName);
@@ -869,7 +1018,7 @@ void Createdxf::drawTextAligned(
To_Dxf << 11 << "\r\n"; // XYZ
To_Dxf << xAlign << "\r\n"; // X in UCS (User Coordinate System)coordinates
To_Dxf << 21 << "\r\n";
To_Dxf << y << "\r\n"; // Y in UCS (User Coordinate System)coordinates
To_Dxf << yAlign << "\r\n"; // Y in UCS (User Coordinate System)coordinates
To_Dxf << 31 << "\r\n";
To_Dxf << 0.0 << "\r\n"; // Z in UCS (User Coordinate System)coordinates
}
+47 -1
View File
@@ -22,6 +22,8 @@
#include <QtCore>
#include <QtWidgets>
#include <functional>
/**
@brief The Createdxf class
This class exports the project to DXF Format
@@ -31,8 +33,38 @@ class Createdxf
public:
Createdxf();
~Createdxf();
static void dxfBegin (const QString&, const QStringList &layers = QStringList());
static void dxfBegin (const QString&, const QStringList &layers = QStringList(),
const std::function<void()> &writeBlocks = {});
static void dxfEnd(const QString&);
static void dxfBlockBegin(const QString &fileName,
const QString &name,
double x,
double y,
bool hasAttributes = false);
static void dxfBlockEnd(const QString &fileName);
/// One attribute of a placed block, in DXF units
struct Attribute
{
QString tag;
QString text;
double x = 0;
double y = 0;
double height = 0;
double rotation = 0;
double xScaleW = 1;
int colour = 0;
bool invisible = false;
QString layer; ///< empty: the INSERT's layer
};
static void drawInsert(const QString &fileName,
const QString &name,
double x,
double y,
double rotation,
const QList<Attribute> &attributes = {});
static void drawAttdef(const QString &fileName,
const Attribute &attribute);
static QString blockName(const QString &name);
// you can add more functions to create more drawings.
static void drawCircle(
const QString&,
@@ -141,6 +173,20 @@ class Createdxf
double xAlign,
double xScale,
int colour);
static void drawTextAligned(
const QString& fileName,
const QString& text,
double x,
double y,
double height,
double rotation,
double oblique,
int hAlign,
int vAlign,
double xAlign,
double yAlign,
double xScale,
int colour);
static void drawPolyline(
const QString &filepath,
+42
View File
@@ -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
+14 -9
View File
@@ -58,14 +58,16 @@ bool DiagramGestureOverlay::isEnabled()
/**
@brief DiagramGestureOverlay::showAt
Show the ring centred on @a center with @a actions, at most eight,
clockwise from the top.
Show the ring centred on @a center with @a sectors directions (4 or 8)
and @a actions, one per direction clockwise from the top.
*/
void DiagramGestureOverlay::showAt(const QPoint &center,
const QList<QAction *> &actions)
const QList<QAction *> &actions,
int sectors)
{
m_center = center;
m_actions = actions.mid(0, sectors);
m_sectors = sectors == 4 ? 4 : 8;
m_actions = actions.mid(0, m_sectors);
m_active = -1;
move(center - QPoint(overlay_width / 2, outer_radius + 1));
show();
@@ -88,7 +90,7 @@ int DiagramGestureOverlay::sectorAt(const QPoint &viewport_pos) const
if (angle < 0) {
angle += 360;
}
return int(qRound(angle / (360.0 / sectors))) % sectors;
return int(qRound(angle / (360.0 / m_sectors))) % m_sectors;
}
/**
@@ -102,7 +104,7 @@ QAction *DiagramGestureOverlay::actionAt(const QPoint &viewport_pos) const
return nullptr;
}
QAction *action = m_actions.at(sector);
return action->isEnabled() ? action : nullptr;
return action && action->isEnabled() ? action : nullptr;
}
void DiagramGestureOverlay::setPointer(const QPoint &viewport_pos)
@@ -120,7 +122,7 @@ void DiagramGestureOverlay::paintEvent(QPaintEvent *event)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
const QPointF c(overlay_width / 2, outer_radius + 1);
const qreal step = 360.0 / sectors;
const qreal step = 360.0 / m_sectors;
QColor base = palette().color(QPalette::Window);
base.setAlpha(225);
@@ -130,7 +132,7 @@ void DiagramGestureOverlay::paintEvent(QPaintEvent *event)
//Highlight the sector the mouse points at, if it holds a command
if (m_active >= 0 && m_active < m_actions.count()
&& m_actions.at(m_active)->isEnabled())
&& m_actions.at(m_active) && m_actions.at(m_active)->isEnabled())
{
QPainterPath wedge;
wedge.moveTo(c);
@@ -151,6 +153,9 @@ void DiagramGestureOverlay::paintEvent(QPaintEvent *event)
for (int i = 0 ; i < m_actions.count() ; ++i)
{
QAction *action = m_actions.at(i);
if (!action) {
continue;
}
const qreal a = qDegreesToRadians(i * step);
const QPointF p(c.x() + icon_radius * qSin(a), c.y() - icon_radius * qCos(a));
const QRect r(int(p.x()) - icon_size / 2, int(p.y()) - icon_size / 2,
@@ -167,7 +172,7 @@ void DiagramGestureOverlay::paintEvent(QPaintEvent *event)
}
//Name of the highlighted command, under the ring
if (m_active >= 0 && m_active < m_actions.count())
if (m_active >= 0 && m_active < m_actions.count() && m_actions.at(m_active))
{
const QString name = m_actions.at(m_active)->text().remove(QLatin1Char('&'));
const QRect label(0, 2 * outer_radius + 4, width(), 20);
+8 -5
View File
@@ -29,8 +29,9 @@ class QAction;
mouse's direction highlighted. Releasing runs it, releasing near the
centre cancels -- SolidWorks' mouse gestures.
Commands are placed clockwise from the top. The ring only draws; the
view tracks the mouse and asks sectorAt() on release.
Commands are placed clockwise from the top, one per direction (4 or 8).
A null entry is an empty direction. The ring only draws; the view
tracks the mouse and asks sectorAt() on release.
*/
class DiagramGestureOverlay : public QWidget
{
@@ -39,14 +40,15 @@ class DiagramGestureOverlay : public QWidget
public:
explicit DiagramGestureOverlay(QWidget *viewport);
void showAt(const QPoint &center, const QList<QAction *> &actions);
void showAt(const QPoint &center, const QList<QAction *> &actions,
int sectors = 8);
void setPointer(const QPoint &viewport_pos);
int sectorAt(const QPoint &viewport_pos) const;
QAction *actionAt(const QPoint &viewport_pos) const;
static bool isEnabled();
int sectors() const { return m_sectors; }
static const int sectors = 8;
static bool isEnabled();
protected:
void paintEvent(QPaintEvent *event) override;
@@ -55,6 +57,7 @@ class DiagramGestureOverlay : public QWidget
QPoint m_center;
QList<QAction *> m_actions;
int m_active = -1;
int m_sectors = 8;
};
#endif // DIAGRAMGESTUREOVERLAY_H
+352
View File
@@ -0,0 +1,352 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#include "diagramtoolbarsettings.h"
#include "shortcutmanager.h"
#include <QApplication>
#include <QCoreApplication>
#include <QSettings>
#include <QToolBar>
namespace {
const QString CONTENTS = QStringLiteral("diagrameditor/toolbars/");
const QString CUSTOM_NAMES = QStringLiteral("diagrameditor/custom_toolbars/names");
const QString CUSTOM_TITLES = QStringLiteral("diagrameditor/custom_toolbars/titles");
//Set on a window by markWindow()
const char *const MARKED = "qetDiagramToolbars";
}
/**
@return every toolbar, built-in first, then the user's own in the order
they were added
*/
QList<DiagramToolbarSettings::Toolbar> DiagramToolbarSettings::toolbars()
{
QList<Toolbar> list;
for (const QString &name : builtInNames()) {
list << Toolbar{name, builtInTitle(name), false};
}
return list << customToolbars();
}
/**
@return the object names of the toolbars the diagram editor always has.
The scripts toolbar is not one: it is filled from the scripts folder.
*/
QStringList DiagramToolbarSettings::builtInNames()
{
return {QStringLiteral("toolbar"),
QStringLiteral("display"),
QStringLiteral("diagram"),
QStringLiteral("adding"),
QStringLiteral("diagram_depth_toolbar")};
}
QString DiagramToolbarSettings::builtInTitle(const QString &name)
{
if (name == QLatin1String("toolbar"))
return QCoreApplication::translate("QETDiagramEditor", "Outils");
if (name == QLatin1String("display"))
return QCoreApplication::translate("QETDiagramEditor", "Affichage");
if (name == QLatin1String("diagram"))
return QCoreApplication::translate("QETDiagramEditor", "Schéma");
if (name == QLatin1String("adding"))
return QCoreApplication::translate("QETDiagramEditor", "Ajouter");
if (name == QLatin1String("diagram_depth_toolbar"))
return QCoreApplication::translate("QETDiagramEditor", "Profondeur", "toolbar title");
return QString();
}
/**
@return the toolbars the user added, in the order they were added
*/
QList<DiagramToolbarSettings::Toolbar> DiagramToolbarSettings::customToolbars()
{
QSettings settings;
const QStringList names = settings.value(CUSTOM_NAMES).toStringList();
const QStringList titles = settings.value(CUSTOM_TITLES).toStringList();
QList<Toolbar> list;
for (int i = 0 ; i < names.size() ; ++i) {
list << Toolbar{names.at(i), titles.value(i, names.at(i)), true};
}
return list;
}
/**
@brief DiagramToolbarSettings::setCustomToolbars
Store the user's own toolbars. The contents of one that is no longer
in @a toolbars are removed with it.
*/
void DiagramToolbarSettings::setCustomToolbars(const QList<Toolbar> &toolbars)
{
QStringList names, titles;
for (const Toolbar &toolbar : toolbars) {
names << toolbar.name;
titles << toolbar.title;
}
for (const Toolbar &old : customToolbars()) {
if (!names.contains(old.name)) {
removeIds(old.name);
}
}
QSettings settings;
if (names.isEmpty()) {
settings.remove(CUSTOM_NAMES);
settings.remove(CUSTOM_TITLES);
} else {
settings.setValue(CUSTOM_NAMES, names);
settings.setValue(CUSTOM_TITLES, titles);
}
}
/**
@return an object name for a new toolbar that none of @a existing has
and none had before (stored positions stay with the toolbar they
belonged to)
*/
QString DiagramToolbarSettings::newCustomName(const QList<Toolbar> &existing)
{
QStringList taken;
for (const Toolbar &toolbar : existing) taken << toolbar.name;
for (const Toolbar &toolbar : customToolbars()) taken << toolbar.name;
for (int i = 1 ; ; ++i) {
const QString name = QStringLiteral("custom_%1").arg(i);
if (!taken.contains(name)) return name;
}
}
/**
@return the ids toolbar @a name holds: the user's list if they saved
one, the defaults otherwise. A saved empty list stays empty.
*/
QStringList DiagramToolbarSettings::ids(const QString &name)
{
QSettings settings;
const QString key = CONTENTS + name;
if (!settings.contains(key)) {
return defaultIds(name);
}
return settings.value(key).toStringList();
}
/**
@return what toolbar @a name holds for a new user: the toolbars as
they were built before they could be changed. A user's own toolbar
starts empty.
*/
QStringList DiagramToolbarSettings::defaultIds(const QString &name)
{
const QString sep = separatorId();
if (name == QLatin1String("toolbar")) {
return {QStringLiteral("diagrameditor.new_file"),
QStringLiteral("diagrameditor.open_file"),
QStringLiteral("diagrameditor.save_file"),
QStringLiteral("diagrameditor.save_file_as"),
QStringLiteral("diagrameditor.close_file"),
QStringLiteral("diagrameditor.print"),
QStringLiteral("diagrameditor.export_to_pdf"),
sep,
QStringLiteral("diagrameditor.undo"),
QStringLiteral("diagrameditor.redo"),
sep,
QStringLiteral("diagrameditor.cut"),
QStringLiteral("diagrameditor.copy"),
QStringLiteral("diagrameditor.paste"),
QStringLiteral("diagrameditor.duplicate"),
sep,
QStringLiteral("diagrameditor.delete_selection"),
QStringLiteral("diagrameditor.rotate_selection")};
}
if (name == QLatin1String("display")) {
return {QStringLiteral("diagrameditor.mode_selection"),
QStringLiteral("diagrameditor.mode_visualise"),
sep,
QStringLiteral("widget:handler_size"),
sep,
QStringLiteral("diagrameditor.draw_grid"),
QStringLiteral("widget:text_grid"),
QStringLiteral("diagrameditor.draw_guides"),
QStringLiteral("widget:background_color"),
sep,
QStringLiteral("diagrameditor.zoom_content"),
QStringLiteral("diagrameditor.zoom_fit"),
QStringLiteral("diagrameditor.zoom_reset")};
}
if (name == QLatin1String("diagram")) {
return {QStringLiteral("diagrameditor.edit_diagram_properties"),
QStringLiteral("diagrameditor.conductor_reset"),
QStringLiteral("diagrameditor.auto_conductor"),
QStringLiteral("diagrameditor.auto_break_conductor"),
QStringLiteral("widget:conductor_color")};
}
if (name == QLatin1String("adding")) {
//add_pdf is skipped when built without QtPdf, as before
return {QStringLiteral("diagrameditor.add_text"),
QStringLiteral("diagrameditor.add_image"),
QStringLiteral("diagrameditor.add_pdf"),
QStringLiteral("diagrameditor.add_line"),
QStringLiteral("diagrameditor.add_rectangle"),
QStringLiteral("diagrameditor.add_ellipse"),
QStringLiteral("diagrameditor.add_arc"),
QStringLiteral("diagrameditor.add_polyline"),
QStringLiteral("diagrameditor.add_path"),
QStringLiteral("diagrameditor.add_fillet"),
QStringLiteral("diagrameditor.add_terminal_strip")};
}
if (name == QLatin1String("diagram_depth_toolbar")) {
return {QStringLiteral("depth.forward"),
QStringLiteral("depth.raise"),
QStringLiteral("depth.lower"),
QStringLiteral("depth.backward")};
}
return {};
}
/**
@brief DiagramToolbarSettings::setIds
Save @a ids for toolbar @a name. Saving the defaults removes the key, so
a later change of defaults still reaches this user.
*/
void DiagramToolbarSettings::setIds(const QString &name, const QStringList &ids)
{
if (ids == defaultIds(name)) {
removeIds(name);
} else {
QSettings().setValue(CONTENTS + name, ids);
}
}
void DiagramToolbarSettings::removeIds(const QString &name)
{
QSettings().remove(CONTENTS + name);
}
QString DiagramToolbarSettings::separatorId()
{
return QStringLiteral("separator");
}
/**
@return the toolbar buttons that are widgets, not commands. Each can be
on one toolbar at a time.
*/
QStringList DiagramToolbarSettings::widgetIds()
{
return {QStringLiteral("widget:handler_size"),
QStringLiteral("widget:text_grid"),
QStringLiteral("widget:background_color"),
QStringLiteral("widget:conductor_color")};
}
bool DiagramToolbarSettings::isWidget(const QString &id)
{
return id.startsWith(QLatin1String("widget:"));
}
QString DiagramToolbarSettings::widgetTitle(const QString &id)
{
if (id == QLatin1String("widget:handler_size"))
return QCoreApplication::translate("DiagramToolbarSettings", "Taille des poignées");
if (id == QLatin1String("widget:text_grid"))
return QCoreApplication::translate("DiagramToolbarSettings", "Grille des textes");
if (id == QLatin1String("widget:background_color"))
return QCoreApplication::translate("DiagramToolbarSettings", "Couleur de fond du folio");
if (id == QLatin1String("widget:conductor_color"))
return QCoreApplication::translate("DiagramToolbarSettings", "Couleur des conducteurs");
return id;
}
/**
@return every command that can go on a toolbar: the diagram editor's,
its stored scripts', and the depth commands it shares with the element
editor
*/
QStringList DiagramToolbarSettings::availableCommandIds()
{
QStringList ids;
for (const ShortcutManager::ShortcutInfo &info :
ShortcutManager::instance().allShortcuts())
{
if (info.id.startsWith(QLatin1String("diagrameditor."))
|| info.id.startsWith(QLatin1String("depth."))) {
ids << info.id;
}
}
return ids;
}
/**
@brief DiagramToolbarSettings::fill
Empty @a toolbar and put the commands of @a ids on it, in order.
@a resolve gives the window's action for an id, a widget id included;
an id it has no action for is skipped. A separator never starts or
ends the toolbar and never follows another: those are left out, as
Qt would otherwise draw them.
*/
void DiagramToolbarSettings::fill(QToolBar *toolbar, const QStringList &ids,
const std::function<QAction *(const QString &)> &resolve)
{
//Separators belong to the toolbar that made them; commands do not
const QList<QAction *> old_actions = toolbar->actions();
toolbar->clear();
for (QAction *action : old_actions) {
if (action->isSeparator() && action->parent() == toolbar) delete action;
}
bool pending_separator = false;
for (const QString &id : ids)
{
if (id == separatorId()) {
pending_separator = !toolbar->actions().isEmpty();
continue;
}
QAction *action = resolve(id);
if (!action) {
continue;
}
if (pending_separator) {
toolbar->addSeparator();
pending_separator = false;
}
toolbar->addAction(action);
}
}
/**
@brief DiagramToolbarSettings::markWindow
Mark @a window as one whose toolbars applyToAll() rebuilds. It must
have a rebuildToolBars() slot or invokable.
*/
void DiagramToolbarSettings::markWindow(QObject *window)
{
window->setProperty(MARKED, true);
}
/**
@brief DiagramToolbarSettings::applyToAll
Rebuild the toolbars of every open window marked by markWindow(),
after the settings change.
*/
void DiagramToolbarSettings::applyToAll()
{
for (QWidget *widget : QApplication::topLevelWidgets()) {
if (widget->property(MARKED).toBool()) {
QMetaObject::invokeMethod(widget, "rebuildToolBars");
}
}
}
+79
View File
@@ -0,0 +1,79 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#ifndef DIAGRAMTOOLBARSETTINGS_H
#define DIAGRAMTOOLBARSETTINGS_H
#include <QList>
#include <QString>
#include <QStringList>
#include <functional>
class QAction;
class QToolBar;
/**
@brief What each of the diagram editor's toolbars holds.
A toolbar is a list of ids, in order: ShortcutManager command ids, the
separator id, and the ids of the few toolbar buttons that are widgets
rather than commands (the colour buttons, the text grid, the handle
size). Stored in QSettings, one key per toolbar, by the toolbar's
object name; a toolbar the user never changed uses the defaults below,
which are the toolbars as they were before they could be changed.
The user can also add toolbars of their own. They start empty.
An id no command carries any more is skipped, so a list saved by
another version still opens.
*/
class DiagramToolbarSettings
{
public:
struct Toolbar {
QString name; ///< object name, used for storage and by QMainWindow::saveState()
QString title; ///< shown to the user
bool custom = false;
};
static QList<Toolbar> toolbars();
static QStringList builtInNames();
static QString builtInTitle(const QString &name);
static QList<Toolbar> customToolbars();
static void setCustomToolbars(const QList<Toolbar> &toolbars);
static QString newCustomName(const QList<Toolbar> &existing);
static QStringList ids(const QString &name);
static QStringList defaultIds(const QString &name);
static void setIds(const QString &name, const QStringList &ids);
static void removeIds(const QString &name);
static QString separatorId();
static QStringList widgetIds();
static bool isWidget(const QString &id);
static QString widgetTitle(const QString &id);
static QStringList availableCommandIds();
static void fill(QToolBar *toolbar, const QStringList &ids,
const std::function<QAction *(const QString &)> &resolve);
static void markWindow(QObject *window);
static void applyToAll();
};
#endif // DIAGRAMTOOLBARSETTINGS_H
+25 -1
View File
@@ -42,6 +42,7 @@
#include "diagram.h"
#include "diagramcontexttoolbar.h"
#include "diagramgestureoverlay.h"
#include "gesturesettings.h"
#include "shortcutbarsettings.h"
#include "shortcutmanager.h"
#include "ElementsCollection/xmlelementcollection.h"
@@ -794,7 +795,8 @@ void DiagramView::mouseMoveEvent(QMouseEvent *e)
m_diagram->clearEventInterface();
m_swallow_native_menu = true;
}
m_gesture_overlay->showAt(m_gesture_origin, selectionCommands());
m_gesture_overlay->showAt(m_gesture_origin, gestureCommands(),
GestureSettings::directions());
}
if (m_gesture_overlay->isVisible()) {
m_gesture_overlay->setPointer(pos);
@@ -981,6 +983,28 @@ QList<QAction *> DiagramView::selectionCommands() const
return actions;
}
/**
@brief DiagramView::gestureCommands
@return the gesture ring's commands for the current selection, as this
window's actions. A list the user picked keeps its directions: a
command this window lacks leaves its direction empty (nullptr).
Otherwise the ring shows the shortcut bar's commands, in order.
*/
QList<QAction *> DiagramView::gestureCommands() const
{
QETDiagramEditor *qde = diagramEditor();
const auto context = ShortcutBarSettings::contextFor(m_diagram->selectedItems());
if (!qde || !GestureSettings::isCustom(context)) {
return selectionCommands();
}
QList<QAction *> actions;
for (const QString &id : GestureSettings::ids(context)) {
actions << (id.isEmpty() ? nullptr
: ShortcutManager::instance().action(id, qde));
}
return actions;
}
/**
@brief DiagramView::showContextToolbar
After a click that leaves something selected, show the shortcut bar's
+1
View File
@@ -154,6 +154,7 @@ class DiagramView : public PaletteGraphicsView
void placeCellRulers();
void showContextToolbar(const QPoint &viewport_pos);
QList<QAction *> selectionCommands() const;
QList<QAction *> gestureCommands() const;
/// Lowest and highest allowed value of the view transform scale (m11).
/// Prevents wheel-zoom from driving the transform to overflow, which
+424 -175
View File
@@ -35,6 +35,7 @@
#include "qetgraphicsitem/independenttextitem.h"
#include "qetgraphicsitem/qetshapeitem.h"
#include "qetgraphicsitem/terminal.h"
#include "qetinformation.h"
#include "textlines.h"
#include <QFontMetricsF>
@@ -43,6 +44,222 @@
#include <cmath>
#include <utility>
namespace {
/**
Draw the drawing of @a elmt's definition: its static texts, lines,
rectangles, circles, polygons and arcs, and its terminals when
@a draw_terminals, as if it were placed at @a elem_pos_x,
@a elem_pos_y and turned @a rotation_angle degrees, with @a elmt's
mirrors when @a mirrored. Used for every symbol drawn in full, and
once per block for a block's content.
*/
void drawSymbol(const QString &file_path, Element *elmt,
qreal elem_pos_x, qreal elem_pos_y,
double rotation_angle, bool draw_terminals,
bool mirrored = false)
{
using namespace DxfExport;
//The mirrors of an element are about its own axes, before it is
//rotated, see Element::setMirror()
const bool mirror_h = mirrored && elmt -> hasHorizontalMirror();
const bool mirror_v = mirrored && elmt -> hasVerticalMirror();
const qreal mirror_x = mirror_h ? -1 : 1;
const qreal mirror_y = mirror_v ? -1 : 1;
ElementPictureFactory::primitives primitives = ElementPictureFactory::instance()->getPrimitives(elmt->location());
Createdxf::layer = Layer::SymbolTexts;
for(QGraphicsSimpleTextItem *text : primitives.m_texts)
{
qreal fontSize = text->font().pointSizeF();
if (fontSize < 0)
fontSize = text->font().pixelSize();
QPointF text_pos = text->pos();
if (mirror_h || mirror_v)
{
//The text keeps reading normally: only its box is
//mirrored, about its own centre, as ElementPictureFactory
//draws it on the folio
QTransform box_transform;
box_transform.translate(text->pos().x(), text->pos().y());
box_transform.rotate(text->rotation());
const QFontMetricsF metrics(text->font());
const QRectF box(0, -metrics.ascent(),
text->boundingRect().width(),
text->boundingRect().height());
const QPointF centre = box_transform.mapRect(box).center();
text_pos += QPointF(centre.x() * (mirror_x - 1),
centre.y() * (mirror_y - 1));
}
qreal x = elem_pos_x + text_pos.x();
qreal y = elem_pos_y + text_pos.y();
qreal angle = text -> rotation() + rotation_angle;
qreal angler = angle * M_PI/180;
int xdir = -sin(angler);
int ydir = -cos(angler);
QPointF transformed_point = DxfExport::rotation_transformed(x, y, elem_pos_x, elem_pos_y, -rotation_angle);
x = transformed_point.x() - ydir * fontSize * 0.5;
y = transformed_point.y() - xdir * fontSize * 0.5;
QStringList lines = text->text().split('\n');
qreal offset = fontSize * 1.6;
for (QString line : lines)
{
if (line.size() > 0 && line != "_" ) {
Createdxf::drawText(file_path, line, QPointF(x, y), fontSize, 360 - angle, 0, 0.72);
}
x += offset * xdir;
y -= offset * ydir;
}
}
Createdxf::layer = Layer::Symbols;
for (QLineF line : primitives.m_lines)
{
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
QLineF l = t.map(line);
Createdxf::drawLine(file_path, l, 0);
}
for (QRectF rect : primitives.m_rectangles)
{
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
QRectF r = t.mapRect(rect);
Createdxf::drawRectangle(file_path,r,0);
}
for (QRectF circle_rect : primitives.m_circles)
{
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
QPointF c = t.map(QPointF(circle_rect.center().x(),circle_rect.center().y()));
Createdxf::drawCircle(file_path,c,circle_rect.width()/2,0);
}
for (QVector<QPointF> polygon : primitives.m_polygons)
{
if (polygon.size() == 0)
continue;
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
QPolygonF poly = t.map(polygon);
if(poly.isClosed())
Createdxf::drawPolygon(file_path,poly,0);
else
Createdxf::drawPolyline(file_path,poly,0);
}
// Draw arcs and ellipses
for (QVector<qreal> arc : primitives.m_arcs)
{
if (arc.size() == 0)
continue;
qreal x = (elem_pos_x + arc.at(0));
qreal y = (elem_pos_y + arc.at(1));
qreal w = arc.at(2);
qreal h = arc.at(3);
qreal startAngle = arc.at(4);
qreal spanAngle = arc .at(5);
//In a mirror the arc starts where it ended: an angle a
//becomes 180 - a in a horizontal one, -a in a vertical one
if (mirror_h)
{
x = elem_pos_x - arc.at(0) - w;
startAngle = 180 - startAngle - spanAngle;
}
if (mirror_v)
{
y = elem_pos_y - arc.at(1) - h;
startAngle = -startAngle - spanAngle;
}
QRectF r(x,y,w,h);
QPointF hotspot(elem_pos_x,elem_pos_y);
Createdxf::drawArcEllipse(file_path, r, startAngle, spanAngle, hotspot, rotation_angle, 0);
}
if (draw_terminals) {
Createdxf::layer = Layer::Terminals;
// Draw terminals
QList<Terminal *> list_terminals = elmt->terminals();
QColor col("red");
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
foreach(Terminal *tp, list_terminals) {
QPointF c = t.map(QPointF(tp->dock_elmt_.x(),tp->dock_elmt_.y()));
Createdxf::drawCircle(file_path,c,3.0,Createdxf::dxfColor(col));
}
}
}
/// One line of a text as the export writes it: where it starts, in
/// scene coordinates, its size, and its angle (degrees, clockwise)
struct TextLine
{
QString text;
QPointF pos;
qreal font_size;
qreal angle;
};
/// The lines of @a dti that are written, one under the other as drawn
QList<TextLine> textLines(DiagramTextItem *dti)
{
qreal fontSize = dti -> font().pointSizeF();
if (fontSize < 0)
fontSize = dti -> font().pixelSize();
qreal angle = dti -> rotation();
QGraphicsItem *parent = dti->parentItem();
while (parent) {
angle += parent->rotation();
parent = parent->parentItem();
}
qreal angler = angle * M_PI/180;
int xdir = -sin(angler);
int ydir = -cos(angler);
qreal x = (dti->scenePos().x())
+ xdir * fontSize * 1.8
- ydir * fontSize;
qreal y = dti->scenePos().y()
- ydir * fontSize * 1.8
- xdir * fontSize * 0.9;
//As drawn: a text with a width is wrapped
QStringList lines = TextLines::layoutLines(dti -> document());
qreal offset = fontSize * 1.6;
QList<TextLine> result;
foreach (QString line, lines) {
if (line.size() > 0 && line != "_" )
result << TextLine{line, QPointF(x, y), fontSize, angle};
x += offset * xdir;
y -= offset * ydir;
}
return result;
}
/// @a tag, or @a tag with _2, _3... if @a used has it, at most 31
/// characters
QString uniqueTag(const QString &tag, const QSet<QString> &used)
{
QString unique = tag;
for (int i = 2 ; used.contains(unique) ; ++i) {
const QString suffix = QStringLiteral("_%1").arg(i);
unique = tag.left(31 - suffix.size()) + suffix;
}
return unique;
}
/// Nothing would be drawn for @a elmt (its definition is missing)
bool drawsNothing(Element *elmt, bool draw_terminals)
{
const ElementPictureFactory::primitives p =
ElementPictureFactory::instance()->getPrimitives(elmt->location());
return p.m_lines.isEmpty() && p.m_rectangles.isEmpty()
&& p.m_circles.isEmpty() && p.m_polygons.isEmpty()
&& p.m_arcs.isEmpty() && p.m_texts.isEmpty()
&& (!draw_terminals || elmt->terminals().isEmpty());
}
}
/**
@brief DxfExport::folioSize
@return the size of @a diagram as exported with @a properties: its border
@@ -81,21 +298,13 @@ void DxfExport::write(Diagram *diagram, int width, int height,
width -= 2*Diagram::margin;
height -= 2*Diagram::margin;
Createdxf::xScale = Createdxf::sheetWidth / double(width);
Createdxf::yScale = Createdxf::sheetHeight / double(height);
Createdxf::dxfBegin(file_path, Layer::all());
//Each kind of content on its own layer (discussion #1071). The
//border's title block switches to Layer::TitleBlock itself, see
//BorderTitleBlock::drawDxf().
Createdxf::layer = Layer::Border;
//Add project elements (lines, rectangles, circles, texts) to dxf file
if (properties.draw_border) {
QRectF rect(Diagram::margin,Diagram::margin,width,height);
Createdxf::drawRectangle(file_path,rect,0);
}
diagram -> border_and_titleblock.drawDxf(file_path, 0);
//One scale for both axes, the largest that fits the sheet: two
//scales stretched the drawing and turned every circle into an oval
//(issue #1339).
const double scale = qMin(Createdxf::sheetWidth / double(width),
Createdxf::sheetHeight / double(height));
Createdxf::xScale = scale;
Createdxf::yScale = scale;
// Build the lists of elements.
QList<Element *> list_elements;
@@ -165,6 +374,185 @@ void DxfExport::write(Diagram *diagram, int width, int height,
}
}
//Symbols as blocks: one block per symbol definition the folio
//uses, named after its file and numbered in the order first met,
//so a repeat export is identical. A block has to be written before
//any entity, so they are all collected first.
QHash<QString, QString> block_names; // location -> block name
QList<Element *> block_models; // one placed symbol per block
if (properties.dxf_blocks) {
QSet<QString> used;
for (Element *elmt : std::as_const(list_elements)) {
const QString key = elmt -> location().toString();
if (elmt -> isMirrored() || block_names.contains(key)
|| drawsNothing(elmt, properties.draw_terminals))
continue;
QString file_name = elmt -> location().fileName();
if (file_name.endsWith(QLatin1String(".elmt")))
file_name.chop(5);
const QString base = Createdxf::blockName(
QStringLiteral("QET_") + file_name);
QString name = base;
for (int i = 2 ; used.contains(name) ; ++i) {
const QString suffix = QStringLiteral("_%1").arg(i);
name = base.left(31 - suffix.size()) + suffix;
}
used << name;
block_names.insert(key, name);
block_models << elmt;
}
}
//A mirrored symbol is drawn in full: an INSERT with a negative
//scale would mirror its texts too, which QElectroTech keeps
//readable
const auto blockOf = [&block_names](Element *elmt) {
return elmt -> isMirrored()
? QString()
: block_names.value(elmt -> location().toString());
};
//With dxf_attributes, a symbol's own texts (its label, its
//function...) become attributes of its INSERT, written where the
//text would be, so a CAD program moves them with the symbol and
//lists them. Off by default: LibreCAD does not show attributes,
//so the labels would vanish there. Tagged
//with the information they show (LABEL, FUNCTION...), or TEXT1,
//TEXT2... for a typed text; a second line gets _2. The block
//defines each tag once, where the first symbol has it.
QHash<Element *, QList<Createdxf::Attribute>> attributes;
QHash<QString, QList<Createdxf::Attribute>> attribute_definitions; // block -> ATTDEFs
QSet<DiagramTextItem *> attribute_texts;
{
QHash<Element *, QSet<QString>> used_tags;
QHash<Element *, int> typed_texts;
QHash<QString, QSet<QString>> defined_tags;
for (DiagramTextItem *dti : std::as_const(list_texts)) {
if (!properties.dxf_attributes)
break;
auto *deti = qgraphicsitem_cast<DynamicElementTextItem *>(dti);
Element *elmt = deti ? deti -> parentElement() : nullptr;
const QString block = elmt ? blockOf(elmt) : QString();
if (block.isEmpty())
continue;
const QString base =
deti -> textFrom() == DynamicElementTextItem::ElementInfo
&& !deti -> infoName().isEmpty()
? Createdxf::blockName(deti -> infoName())
: QStringLiteral("TEXT%1").arg(++typed_texts[elmt]);
//From the folio into the block: undo the INSERT
const QPointF insert(elmt -> pos().x() * Createdxf::xScale,
Createdxf::sheetHeight - elmt -> pos().y() * Createdxf::yScale);
const double turn = std::fmod(360 - elmt -> orientation() * 90, 360);
const QTransform to_block = QTransform()
.translate(0, Createdxf::sheetHeight)
.rotate(-turn)
.translate(-insert.x(), -insert.y());
const QList<TextLine> lines = textLines(dti);
for (int i = 0 ; i < lines.size() ; ++i) {
const TextLine &line = lines.at(i);
const QString tag = uniqueTag(
i == 0 ? base : base + QStringLiteral("_%1").arg(i + 1),
used_tags.value(elmt));
used_tags[elmt] << tag;
Createdxf::Attribute attribute;
attribute.tag = tag;
attribute.text = line.text;
attribute.x = line.pos.x() * Createdxf::xScale;
attribute.y = Createdxf::sheetHeight - line.pos.y() * Createdxf::yScale;
attribute.height = line.font_size * Createdxf::yScale;
attribute.rotation = 360 - line.angle;
attribute.xScaleW = 0.72;
attribute.colour = Createdxf::dxfColor(dti -> color());
attribute.layer = Layer::SymbolTexts;
attributes[elmt] << attribute;
if (!defined_tags[block].contains(tag)) {
defined_tags[block] << tag;
Createdxf::Attribute definition = attribute;
const QPointF in_block = to_block.map(QPointF(attribute.x, attribute.y));
definition.x = in_block.x();
definition.y = in_block.y();
definition.rotation = std::fmod(attribute.rotation - turn + 720, 360);
definition.text.clear();
attribute_definitions[block] << definition;
}
}
attribute_texts << dti;
}
//The rest of what is known about a symbol (manufacturer,
//reference, supplier, quantity...) as hidden attributes, so a
//CAD program can list the parts from the drawing. A field
//already written as a visible attribute (dxf_attributes) is not
//repeated; the label formula is how the label is made, not
//part data.
for (Element *elmt : std::as_const(list_elements)) {
const QString block = blockOf(elmt);
if (block.isEmpty())
continue;
const DiagramContext information = elmt -> elementInformations();
const QPointF insert(elmt -> pos().x() * Createdxf::xScale,
Createdxf::sheetHeight - elmt -> pos().y() * Createdxf::yScale);
for (const QString &key : QETInformation::elementInfoKeys()) {
if (key == QETInformation::ELMT_FORMULA)
continue;
const QString value = information.value(key).toString();
const QString tag = Createdxf::blockName(key);
if (value.isEmpty() || used_tags.value(elmt).contains(tag))
continue;
used_tags[elmt] << tag;
Createdxf::Attribute attribute;
attribute.tag = tag;
attribute.text = value;
attribute.x = insert.x();
attribute.y = insert.y();
attribute.height = 9 * Createdxf::yScale;
attribute.invisible = true;
attribute.layer = Layer::SymbolTexts;
attributes[elmt] << attribute;
if (!defined_tags[block].contains(tag)) {
defined_tags[block] << tag;
Createdxf::Attribute definition = attribute;
definition.x = 0;
definition.y = Createdxf::sheetHeight;
definition.text.clear();
attribute_definitions[block] << definition;
}
}
}
}
//A block is drawn as if its symbol sat unturned at the folio's
//origin, which is DXF (0, sheetHeight); that is its base point.
Createdxf::dxfBegin(file_path, Layer::all(), [&]() {
for (Element *model : std::as_const(block_models)) {
const QString block = block_names.value(model -> location().toString());
const QList<Createdxf::Attribute> definitions = attribute_definitions.value(block);
Createdxf::dxfBlockBegin(file_path, block, 0, Createdxf::sheetHeight,
!definitions.isEmpty());
drawSymbol(file_path, model, 0, 0, 0, properties.draw_terminals);
for (const Createdxf::Attribute &definition : definitions)
Createdxf::drawAttdef(file_path, definition);
Createdxf::dxfBlockEnd(file_path);
}
});
//Each kind of content on its own layer (discussion #1071). The
//border's title block switches to Layer::TitleBlock itself, see
//BorderTitleBlock::drawDxf().
Createdxf::layer = Layer::Border;
//Add project elements (lines, rectangles, circles, texts) to dxf file
if (properties.draw_border) {
QRectF rect(Diagram::margin,Diagram::margin,width,height);
Createdxf::drawRectangle(file_path,rect,0);
}
diagram -> border_and_titleblock.drawDxf(file_path, 0);
// Draw shapes
Createdxf::layer = Layer::Shapes;
foreach (QetShapeItem *qsi, list_shapes) qsi->toDXF(file_path, qsi->pen());
@@ -175,139 +563,25 @@ void DxfExport::write(Diagram *diagram, int width, int height,
gti->toDXF(file_path);
}
//Draw elements
//Draw elements: each one in full, or as an INSERT of its block
foreach(Element *elmt, list_elements)
{
double rotation_angle = elmt -> orientation() * 90;
//The mirrors of an element are about its own axes, before it is
//rotated, see Element::setMirror()
const qreal mirror_x = elmt -> hasHorizontalMirror() ? -1 : 1;
const qreal mirror_y = elmt -> hasVerticalMirror() ? -1 : 1;
const double rotation_angle = elmt -> orientation() * 90;
const qreal elem_pos_x = elmt -> pos().x();
const qreal elem_pos_y = elmt -> pos().y();
qreal elem_pos_x = elmt -> pos().x();
qreal elem_pos_y = elmt -> pos().y();// - (diagram -> margin / 2);
ElementPictureFactory::primitives primitives = ElementPictureFactory::instance()->getPrimitives(elmt->location());
Createdxf::layer = Layer::SymbolTexts;
for(QGraphicsSimpleTextItem *text : primitives.m_texts)
{
qreal fontSize = text->font().pointSizeF();
if (fontSize < 0)
fontSize = text->font().pixelSize();
QPointF text_pos = text->pos();
if (elmt -> isMirrored())
{
//The text keeps reading normally: only its box is
//mirrored, about its own centre, as ElementPictureFactory
//draws it on the folio
QTransform box_transform;
box_transform.translate(text->pos().x(), text->pos().y());
box_transform.rotate(text->rotation());
const QFontMetricsF metrics(text->font());
const QRectF box(0, -metrics.ascent(),
text->boundingRect().width(),
text->boundingRect().height());
const QPointF centre = box_transform.mapRect(box).center();
text_pos += QPointF(centre.x() * (mirror_x - 1),
centre.y() * (mirror_y - 1));
}
qreal x = elem_pos_x + text_pos.x();
qreal y = elem_pos_y + text_pos.y();
qreal angle = text -> rotation() + rotation_angle;
qreal angler = angle * M_PI/180;
int xdir = -sin(angler);
int ydir = -cos(angler);
QPointF transformed_point = rotation_transformed(x, y, elem_pos_x, elem_pos_y, -rotation_angle);
x = transformed_point.x() - ydir * fontSize * 0.5;
y = transformed_point.y() - xdir * fontSize * 0.5;
QStringList lines = text->text().split('\n');
qreal offset = fontSize * 1.6;
for (QString line : lines)
{
if (line.size() > 0 && line != "_" ) {
Createdxf::drawText(file_path, line, QPointF(x, y), fontSize, 360 - angle, 0, 0.72);
}
x += offset * xdir;
y -= offset * ydir;
}
}
Createdxf::layer = Layer::Symbols;
for (QLineF line : primitives.m_lines)
{
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
QLineF l = t.map(line);
Createdxf::drawLine(file_path, l, 0);
}
for (QRectF rect : primitives.m_rectangles)
{
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
QRectF r = t.mapRect(rect);
Createdxf::drawRectangle(file_path,r,0);
}
for (QRectF circle_rect : primitives.m_circles)
{
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
QPointF c = t.map(QPointF(circle_rect.center().x(),circle_rect.center().y()));
Createdxf::drawCircle(file_path,c,circle_rect.width()/2,0);
}
for (QVector<QPointF> polygon : primitives.m_polygons)
{
if (polygon.size() == 0)
continue;
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
QPolygonF poly = t.map(polygon);
if(poly.isClosed())
Createdxf::drawPolygon(file_path,poly,0);
else
Createdxf::drawPolyline(file_path,poly,0);
}
// Draw arcs and ellipses
for (QVector<qreal> arc : primitives.m_arcs)
{
if (arc.size() == 0)
continue;
qreal x = (elem_pos_x + arc.at(0));
qreal y = (elem_pos_y + arc.at(1));
qreal w = arc.at(2);
qreal h = arc.at(3);
qreal startAngle = arc.at(4);
qreal spanAngle = arc .at(5);
//In a mirror the arc starts where it ended: an angle a
//becomes 180 - a in a horizontal one, -a in a vertical one
if (elmt -> hasHorizontalMirror())
{
x = elem_pos_x - arc.at(0) - w;
startAngle = 180 - startAngle - spanAngle;
}
if (elmt -> hasVerticalMirror())
{
y = elem_pos_y - arc.at(1) - h;
startAngle = -startAngle - spanAngle;
}
QRectF r(x,y,w,h);
QPointF hotspot(elem_pos_x,elem_pos_y);
Createdxf::drawArcEllipse(file_path, r, startAngle, spanAngle, hotspot, rotation_angle, 0);
}
if (properties.draw_terminals) {
Createdxf::layer = Layer::Terminals;
// Draw terminals
QList<Terminal *> list_terminals = elmt->terminals();
QColor col("red");
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
foreach(Terminal *tp, list_terminals) {
QPointF c = t.map(QPointF(tp->dock_elmt_.x(),tp->dock_elmt_.y()));
Createdxf::drawCircle(file_path,c,3.0,Createdxf::dxfColor(col));
}
const QString block = blockOf(elmt);
if (block.isEmpty()) {
drawSymbol(file_path, elmt, elem_pos_x, elem_pos_y,
rotation_angle, properties.draw_terminals, true);
} else {
Createdxf::layer = Layer::Symbols;
//QElectroTech turns a symbol clockwise, DXF counter-clockwise
Createdxf::drawInsert(file_path, block,
elem_pos_x * Createdxf::xScale,
Createdxf::sheetHeight - elem_pos_y * Createdxf::yScale,
std::fmod(360 - rotation_angle, 360),
attributes.value(elmt));
}
}
@@ -370,39 +644,14 @@ void DxfExport::write(Diagram *diagram, int width, int height,
//Draw text items
foreach(DiagramTextItem *dti, list_texts) {
//Already written as an attribute of its symbol's INSERT
if (attribute_texts.contains(dti))
continue;
//A free text, or a symbol's own text (its label and the like)
Createdxf::layer = qgraphicsitem_cast<IndependentTextItem *>(dti)
? Layer::Texts : Layer::SymbolTexts;
qreal fontSize = dti -> font().pointSizeF();
if (fontSize < 0)
fontSize = dti -> font().pixelSize();
qreal angle = dti -> rotation();
QGraphicsItem *parent = dti->parentItem();
while (parent) {
angle += parent->rotation();
parent = parent->parentItem();
}
qreal angler = angle * M_PI/180;
int xdir = -sin(angler);
int ydir = -cos(angler);
qreal x = (dti->scenePos().x())
+ xdir * fontSize * 1.8
- ydir * fontSize;
qreal y = dti->scenePos().y()
- ydir * fontSize * 1.8
- xdir * fontSize * 0.9;
//As drawn: a text with a width is wrapped
QStringList lines = TextLines::layoutLines(dti -> document());
qreal offset = fontSize * 1.6;
foreach (QString line, lines) {
if (line.size() > 0 && line != "_" )
Createdxf::drawText(file_path, line, QPointF(x, y), fontSize, 360-angle, Createdxf::dxfColor(dti->color()), 0.72 );
x += offset * xdir;
y -= offset * ydir;
}
for (const TextLine &line : textLines(dti))
Createdxf::drawText(file_path, line.text, line.pos, line.font_size, 360-line.angle, Createdxf::dxfColor(dti->color()), 0.72 );
}
//Draw the slave cross-reference labels
+41
View File
@@ -23,6 +23,7 @@
#include "../NameList/ui/namelistwidget.h"
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../QetGraphicsItemModeler/qetgraphicshandleritem.h"
#include "../QetGraphicsItemModeler/textresizehandles.h"
#include "editorcommands.h"
#include "elementcontent.h"
#include "elementprimitivedecorator.h"
@@ -1498,9 +1499,49 @@ void ElementScene::managePrimitivesGroups()
m_decorator -> setPos(0, 0);
m_decorator -> setItems(selected_items);
}
manageTextResizeHandles(selected_items);
m_decorator_lock -> unlock();
}
/**
@brief ElementScene::manageTextResizeHandles
Show the corner handles that change the width of a text field when it
is the only selected item.
Called again when the undo stack changes, including from the push of a
resize itself: the handles of the same text are then kept, not deleted
while they are still emitting.
@param selected_items
*/
void ElementScene::manageTextResizeHandles(const QList<QGraphicsItem *> &selected_items)
{
QGraphicsTextItem *text = nullptr;
if (selected_items.size() == 1 &&
selected_items.first()->type() == PartDynamicTextField::Type)
{
text = static_cast<PartDynamicTextField *>(selected_items.first());
}
if (m_text_resize_handles && m_text_resize_handles->parentItem() == text) {
m_text_resize_handles->updateHandlesPos();
return;
}
if (m_text_resize_handles) {
m_text_resize_handles->hide();
m_text_resize_handles->deleteLater();
m_text_resize_handles.clear();
}
if (text)
{
m_text_resize_handles = new TextResizeHandles(text);
connect(m_text_resize_handles, &TextResizeHandles::resizeFinished, this,
[this, text](qreal old_width, qreal new_width, QPointF old_pos, QPointF new_pos)
{
m_undo_stack.push(new TextResizeCommand(text, old_width, new_width, old_pos, new_pos));
});
}
}
/**
@brief ElementScene::stackAction
Push the provided \a command on the undo stack.
+3
View File
@@ -33,6 +33,7 @@ class QETElementEditor;
class ESEventInterface;
class QKeyEvent;
class CustomElementGraphicPart;
class TextResizeHandles;
/**
@brief The ElementScene class
This class is the canvas allowing the visual edition of an electrical element.
@@ -90,6 +91,7 @@ class ElementScene : public QGraphicsScene
m_y_grid;
QPointer<CustomElementGraphicPart> m_single_selected_item;
QPointer<TextResizeHandles> m_text_resize_handles;
bool m_hotspot_visible = true;
bool m_background_frame_visible = false;
@@ -180,6 +182,7 @@ class ElementScene : public QGraphicsScene
void slot_editAuthorInformations();
void slot_editProperties();
void managePrimitivesGroups();
void manageTextResizeHandles(const QList<QGraphicsItem *> &selected_items);
void stackAction(ElementEditionCommand *);
signals:
+2
View File
@@ -54,6 +54,7 @@
#include "../../dxf/dxftoelmt.h"
#include "../../qet_elementscaler/qet_elementscaler.h"
#include "../UndoCommand/openelmtcommand.h"
#include "../../toolbarsettings.h"
#include <QSettings>
#include <QActionGroup>
@@ -89,6 +90,7 @@ QETElementEditor::QETElementEditor(QWidget *parent) :
readSettings(); // restoreGeometry before show()
show();
readSettingsState(); // restoreState() must be called after show() in Qt6
ToolbarSettings::applyTo(this);
}
/**
+10
View File
@@ -37,6 +37,8 @@ ExportProperties::ExportProperties() :
draw_terminal_names(false),
draw_bg_transparent(false),
draw_colored_conductors(true),
dxf_blocks(false),
dxf_attributes(false),
exported_area(QET::BorderArea)
{
}
@@ -77,6 +79,10 @@ void ExportProperties::toSettings(QSettings &settings,
draw_bg_transparent);
settings.setValue(prefix % "drawcoloredconductors",
draw_colored_conductors);
settings.setValue(prefix % "dxfblocks",
dxf_blocks);
settings.setValue(prefix % "dxfattributes",
dxf_attributes);
settings.setValue(prefix % "area",
QET::diagramAreaToString(exported_area));
}
@@ -115,6 +121,10 @@ void ExportProperties::fromSettings(QSettings &settings,
draw_colored_conductors = settings.value(
prefix % "drawcoloredconductors",
true ).toBool();
dxf_blocks = settings.value(prefix % "dxfblocks",
false).toBool();
dxf_attributes = settings.value(prefix % "dxfattributes",
false).toBool();
exported_area = QET::diagramAreaFromString(
settings.value(
+2
View File
@@ -50,6 +50,8 @@ class ExportProperties {
bool draw_terminal_names; ///< Whether to render terminal names/labels
bool draw_bg_transparent; ///< Whether to use transparency for SVG-Export
bool draw_colored_conductors; ///< Whether to render conductors colors
bool dxf_blocks; ///< DXF: each symbol as a block, placed with an INSERT
bool dxf_attributes; ///< DXF, with dxf_blocks: a symbol's texts as attributes of its INSERT
QET::DiagramArea exported_area; ///< Area of diagrams to be rendered
};
#endif
+22
View File
@@ -66,6 +66,8 @@ ExportProperties ExportPropertiesWidget::exportProperties() const
export_properties.draw_terminal_names = draw_terminal_names -> isChecked();
export_properties.draw_bg_transparent = draw_bg_transparent -> isChecked();
export_properties.draw_colored_conductors = draw_colored_conductors -> isChecked();
export_properties.dxf_blocks = dxf_blocks -> isChecked();
export_properties.dxf_attributes = dxf_attributes -> isChecked();
export_properties.exported_area = export_border -> isChecked() ? QET::BorderArea : QET::ElementsArea;
return(export_properties);
@@ -89,6 +91,9 @@ void ExportPropertiesWidget::setExportProperties(const ExportProperties &export_
draw_terminal_names -> setChecked(export_properties.draw_terminal_names);
draw_bg_transparent -> setChecked(export_properties.draw_bg_transparent);
draw_colored_conductors -> setChecked(export_properties.draw_colored_conductors);
dxf_blocks -> setChecked(export_properties.dxf_blocks);
dxf_attributes -> setChecked(export_properties.dxf_attributes);
dxf_attributes -> setEnabled(export_properties.dxf_blocks);
if (export_properties.exported_area == QET::BorderArea) {
export_border -> setChecked(true);
@@ -219,6 +224,17 @@ void ExportPropertiesWidget::build()
// use transparent background for SVG-Export
draw_bg_transparent = new QCheckBox(tr("SVG: fond transparent"), groupbox_options);
optionshlayout -> addWidget(draw_bg_transparent, 4, 0);
// each symbol as a DXF block (issue #1339)
dxf_blocks = new QCheckBox(tr("DXF : symboles en blocs"), groupbox_options);
dxf_blocks -> setToolTip(tr("Chaque symbole devient un bloc DXF, sélectionnable d'un clic dans un logiciel de CAO"));
optionshlayout -> addWidget(dxf_blocks, 4, 1);
// with blocks, a symbol's texts as attributes of its block
dxf_attributes = new QCheckBox(tr("DXF : textes des symboles en attributs"), groupbox_options);
dxf_attributes -> setToolTip(tr("Les textes d'un symbole (repère, fonction...) deviennent des attributs de son bloc. LibreCAD n'affiche pas les attributs."));
optionshlayout -> addWidget(dxf_attributes, 5, 1);
connect(dxf_blocks, &QCheckBox::toggled, dxf_attributes, &QCheckBox::setEnabled);
vboxLayout -> addWidget(groupbox_options);
@@ -234,6 +250,8 @@ void ExportPropertiesWidget::build()
setTabOrder(draw_titleblock, draw_terminals);
setTabOrder(draw_terminals, draw_colored_conductors);
setTabOrder(draw_colored_conductors, draw_bg_transparent);
setTabOrder(draw_bg_transparent, dxf_blocks);
setTabOrder(dxf_blocks, dxf_attributes);
// connexion du bouton permettant le choix du repertoire
connect(button_browse, &QPushButton::released, this, &ExportPropertiesWidget::slot_chooseADirectory);
@@ -249,6 +267,8 @@ void ExportPropertiesWidget::build()
connect(draw_terminal_names, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged);
connect(draw_bg_transparent, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged);
connect(draw_colored_conductors, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged);
connect(dxf_blocks, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged);
connect(dxf_attributes, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged);
#else
connect(draw_grid, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
connect(draw_border, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
@@ -257,5 +277,7 @@ void ExportPropertiesWidget::build()
connect(draw_terminal_names, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
connect(draw_bg_transparent, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
connect(draw_colored_conductors, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
connect(dxf_blocks, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
connect(dxf_attributes, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
#endif
}
+2
View File
@@ -65,6 +65,8 @@ class ExportPropertiesWidget : public QWidget {
QCheckBox *draw_terminal_names;
QCheckBox *draw_bg_transparent;
QCheckBox *draw_colored_conductors;
QCheckBox *dxf_blocks;
QCheckBox *dxf_attributes;
QRadioButton *export_border;
QRadioButton *export_elements;
QButtonGroup *exported_content_choices;
+7 -3
View File
@@ -152,7 +152,8 @@ void ElementPictureFactory::dropCache(const ElementsLocation &location)
m_mirrored_low_pictures_H.remove(qMakePair(uuid, mirror));
}
m_pixmap_H.remove(uuid);
m_primitives_H.remove(uuid);
//The text items belong to the cache; nothing else holds them.
qDeleteAll(m_primitives_H.take(uuid).m_texts);
}
/**
@@ -292,6 +293,9 @@ bool ElementPictureFactory::build(const ElementsLocation &location,
QPainter painter;
QPicture pic;
primitives primitives_;
//The low-zoom pass fills a second set of primitives that is only
//drawn, never kept: its text items are deleted at the end.
primitives low_primitives;
if (picture) {
painter.begin(picture);
}
@@ -365,8 +369,7 @@ bool ElementPictureFactory::build(const ElementsLocation &location,
// complex font/text operations.
} else {
parseElement(qde, painter, primitives_);
primitives fake_prim;
parseElement(qde, low_painter, fake_prim);
parseElement(qde, low_painter, low_primitives);
}
}
}
@@ -376,6 +379,7 @@ bool ElementPictureFactory::build(const ElementsLocation &location,
painter.end();
low_painter.end();
m_build_mirror = QTransform();
qDeleteAll(low_primitives.m_texts);
const auto uuid_ = cacheKey(location);
if (!mirror.isIdentity()) {
+125
View File
@@ -0,0 +1,125 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#include "gesturesettings.h"
#include <QSettings>
namespace {
const QString directions_key = QStringLiteral("diagrameditor/gestures/directions");
QString settingsKey(ShortcutBarSettings::Context context)
{
switch (context)
{
case ShortcutBarSettings::Canvas:
return QStringLiteral("diagrameditor/gestures/canvas");
case ShortcutBarSettings::Selection:
return QStringLiteral("diagrameditor/gestures/selection");
case ShortcutBarSettings::Conductor:
return QStringLiteral("diagrameditor/gestures/conductor");
}
return QString();
}
}
/**
@return how many directions the ring has: 8 (the default) or 4
*/
int GestureSettings::directions()
{
return QSettings().value(directions_key, 8).toInt() == 4 ? 4 : 8;
}
/**
@brief GestureSettings::setDirections
Save 4 or 8 directions. 8 is the default and removes the key.
*/
void GestureSettings::setDirections(int directions)
{
QSettings settings;
if (directions == 4) {
settings.setValue(directions_key, 4);
} else {
settings.remove(directions_key);
}
}
/**
@return true if the user picked the commands for @a context, false
when the ring follows the shortcut bar
*/
bool GestureSettings::isCustom(ShortcutBarSettings::Context context)
{
return QSettings().contains(settingsKey(context));
}
/**
@return the ids on the ring for @a context: the user's list if they
picked one, the defaults otherwise
*/
QStringList GestureSettings::ids(ShortcutBarSettings::Context context)
{
QSettings settings;
const QString key = settingsKey(context);
if (!settings.contains(key)) {
return defaultIds(context);
}
return settings.value(key).toStringList();
}
/**
@return the shortcut bar's commands for @a context, without its pinned
elements: what the ring has shown since it was added
*/
QStringList GestureSettings::defaultIds(ShortcutBarSettings::Context context)
{
QStringList ids;
for (const QString &id : ShortcutBarSettings::ids(context)) {
if (!ShortcutBarSettings::isElement(id)) {
ids << id;
}
}
return ids;
}
/**
@brief GestureSettings::setIds
Save @a ids for @a context; trailing empty directions are dropped.
Saving the defaults removes the key, so the ring follows the shortcut
bar again.
*/
void GestureSettings::setIds(ShortcutBarSettings::Context context, QStringList ids)
{
while (!ids.isEmpty() && ids.constLast().isEmpty()) {
ids.removeLast();
}
if (ids == defaultIds(context)) {
reset(context);
} else {
QSettings().setValue(settingsKey(context), ids);
}
}
/**
@brief GestureSettings::reset
Make the ring for @a context follow the shortcut bar again.
*/
void GestureSettings::reset(ShortcutBarSettings::Context context)
{
QSettings().remove(settingsKey(context));
}
+47
View File
@@ -0,0 +1,47 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#ifndef GESTURESETTINGS_H
#define GESTURESETTINGS_H
#include "shortcutbarsettings.h"
#include <QStringList>
/**
@brief The commands on the mouse gesture ring (right-drag on a folio).
Until the user picks them, the ring shows the shortcut bar's commands
for the selection, as it always has. A picked list is stored in
QSettings, one key per context, and is positional: entry i is the
command in direction i, clockwise from the top, and an empty entry is
an empty direction. Pinned elements are never on the ring.
*/
class GestureSettings
{
public:
static int directions();
static void setDirections(int directions);
static bool isCustom(ShortcutBarSettings::Context context);
static QStringList ids(ShortcutBarSettings::Context context);
static QStringList defaultIds(ShortcutBarSettings::Context context);
static void setIds(ShortcutBarSettings::Context context, QStringList ids);
static void reset(ShortcutBarSettings::Context context);
};
#endif // GESTURESETTINGS_H
+46
View File
@@ -42,6 +42,13 @@
#include <QStyleFactory>
#include <QtConcurrentRun>
#include <cstdio>
#include <cstdlib>
#ifdef Q_OS_WIN
#include <fcntl.h>
#include <io.h>
#endif
#ifdef Q_OS_MACOS
#include <QFileOpenEvent>
@@ -120,6 +127,43 @@ static QStringList headlessArguments(const QStringList &args)
return kept;
}
/**
The headless runs below (the exports, --info, --resave and --run) return
before the log-file handler is installed further down, so they keep
Qt's default handler. On Linux and macOS that one writes to stderr. On
Windows it writes to the debugger when the program has no console, and
a GUI program started by another program never has one: what the run
reports -- a wire that could not be reconnected, which way the project
database was filled -- never reached the caller. Write it to stderr
ourselves, in the form the default handler uses on the other systems,
so a caller reads the same lines on every system.
*/
static void headlessMessageHandler(QtMsgType type,
const QMessageLogContext &context,
const QString &msg)
{
const QByteArray line = qFormatLogMessage(type, context, msg).toUtf8();
fprintf(stderr, "%s\n", line.constData());
fflush(stderr);
if (type == QtFatalMsg) {
abort();
}
}
static void installHeadlessMessageHandler()
{
#ifdef Q_OS_WIN
// stdout and stderr are in text mode on Windows and turn "\n" into
// "\r\n": a caller would read lines ending in '\r' where the other
// systems give none. Binary mode gives both streams the same line
// ending on every system, for the JSON of --info, the messages of the
// exports and what a script logs.
_setmode(_fileno(stdout), _O_BINARY);
_setmode(_fileno(stderr), _O_BINARY);
#endif
qInstallMessageHandler(headlessMessageHandler);
}
int main(int argc, char **argv)
{
// before creating Application:
@@ -190,6 +234,7 @@ int main(int argc, char **argv)
for (int i = 0; i < argc; ++i)
raw_args << QString::fromLocal8Bit(argv[i]);
if (CLIExport::isExportRequest(raw_args)) {
installHeadlessMessageHandler();
QApplication export_app(argc, argv);
// No crash-recovery backups in one-shot CLI mode: the backup write
// runs on a background thread referencing the project and races the
@@ -207,6 +252,7 @@ int main(int argc, char **argv)
// #162). Same reasoning as the export branch above for running
// before SingleApplication and answering message boxes headlessly.
if (QetScripting::isRunRequest(raw_args)) {
installHeadlessMessageHandler();
QApplication script_app(argc, argv);
QETProject::setBackupEnabled(false);
QET::QetMessageBox::setNonInteractive(true);
+47 -6
View File
@@ -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);
}
+4
View File
@@ -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;
+38
View File
@@ -41,7 +41,12 @@
#include "ui/aboutqetdialog.h"
#include "ui/configpage/generalconfigurationpage.h"
#include "ui/configpage/shortcutsconfigpage.h"
#include "ui/configpage/gesturesconfigpage.h"
#include "ui/configpage/shortcutbarconfigpage.h"
#include "ui/configpage/toolbarsconfigpage.h"
#include "ui/configpage/toolbarcommandsconfigpage.h"
#include "ui/customizedialog.h"
#include <QTabWidget>
#include "machine_info.h"
#include "TerminalStrip/ui/terminalstripeditorwindow.h"
#include "qetversion.h"
@@ -2225,6 +2230,9 @@ void QETApp::configureQET()
cd.addPage(new PrintConfigPage());
cd.addPage(new ShortcutsConfigPage());
cd.addPage(new ShortcutBarConfigPage());
cd.addPage(new GesturesConfigPage());
cd.addPage(new ToolbarsConfigPage());
cd.addPage(new ToolbarCommandsConfigPage());
#ifdef QET_SPACEMOUSE_SUPPORT
cd.addPage(new SpaceMouseConfigPage());
#endif
@@ -2252,6 +2260,36 @@ void QETApp::configureQET()
#endif
}
/**
@brief QETApp::customizeQET
Open the Customise window: the toolbar, shortcut bar, keyboard and
gesture pages of the configuration dialog, as tabs of one window.
@param tab : the tab to show first
*/
void QETApp::customizeQET(int tab)
{
QWidget *parent_widget = qApp->activeWindow();
CustomizeDialog dialog;
//Same reason as the configuration dialog (#527)
dialog.setWindowModality(Qt::ApplicationModal);
dialog.addPage(new ToolbarsConfigPage());
dialog.addPage(new ToolbarCommandsConfigPage());
dialog.addPage(new ShortcutBarConfigPage());
dialog.addPage(new ShortcutsConfigPage());
dialog.addPage(new GesturesConfigPage());
if (auto *tabs = dialog.findChild<QTabWidget *>(QStringLiteral("customizeTabs"))) {
tabs->setCurrentIndex(tab);
}
QET::trackDialogGeometry(&dialog);
if (parent_widget) {
dialog.setParent(parent_widget, dialog.windowFlags());
}
dialog.exec();
dialog.setParent(nullptr, dialog.windowFlags());
}
/**
@brief QETApp::exportConfiguration
Save the settings of QElectroTech to a file the user chooses, to keep
+1
View File
@@ -294,6 +294,7 @@ class QETApp : public QObject
void openTitleBlockTemplate(const QString &);
void openTitleBlockTemplateFiles(const QStringList &);
void configureQET();
void customizeQET(int tab = 0);
/// Save the settings to a file (discussion #610)
void exportConfiguration();
/// Replace the settings with a saved file, then close QElectroTech
+103 -51
View File
@@ -27,9 +27,11 @@
#endif
#include <QCoreApplication>
#include <QToolButton>
#include <QWidgetAction>
#include "ElementsCollection/elementscollectionwidget.h"
#include "ElementsCollection/elementpickerpopup.h"
#include "shortcutbarsettings.h"
#include "diagramtoolbarsettings.h"
#include "qetgraphicsitem/conductor.h"
#include "itemgroups.h"
#include "commandsearchpopup.h"
@@ -86,6 +88,7 @@
#include "wiringlistexport.h"
#include "ui/wiringlistdialog.h"
#include "ui/terminalnumberingdialog.h"
#include "toolbarsettings.h"
#include <QDateTime>
#include <QDebug>
#include <QDir>
@@ -187,6 +190,7 @@ QETDiagramEditor::QETDiagramEditor(const QStringList &files, QWidget *parent) :
readSettings(); // restoreGeometry before show()
show();
readSettingsState(); // restoreState() must be called after show() in Qt6
ToolbarSettings::applyTo(this);
#ifdef QET_HAS_SCRIPTING
setUpLiveIndicator();
setUpMacroRecorder();
@@ -1324,50 +1328,27 @@ void QETDiagramEditor::setUpToolBar()
diagram_tool_bar = new QToolBar(tr("Schéma"), this);
diagram_tool_bar -> setObjectName("diagram");
main_tool_bar -> addActions(m_file_actions_group.actions());
main_tool_bar -> addAction(m_print);
main_tool_bar -> addAction(m_export_to_pdf);
main_tool_bar -> addSeparator();
main_tool_bar -> addAction(undo);
main_tool_bar -> addAction(redo);
main_tool_bar -> addSeparator();
main_tool_bar -> addAction(m_cut);
main_tool_bar -> addAction(m_copy);
main_tool_bar -> addAction(m_paste);
main_tool_bar -> addAction(m_duplicate);
main_tool_bar -> addSeparator();
main_tool_bar -> addAction(m_delete_selection);
main_tool_bar -> addAction(m_rotate_selection);
// Modes selection / visualisation et zoom
view_tool_bar -> addAction(m_mode_selection);
view_tool_bar -> addAction(m_mode_visualise);
view_tool_bar -> addSeparator();
view_tool_bar -> addWidget(new DiagramEditorHandlerSizeWidget(this));
view_tool_bar -> addSeparator();
view_tool_bar -> addAction(m_draw_grid);
view_tool_bar -> addWidget(m_text_grid_button);
view_tool_bar -> addAction(m_draw_guides);
view_tool_bar -> addWidget(m_background_color_button);
view_tool_bar -> addSeparator();
view_tool_bar -> addActions(m_zoom_action_toolBar);
diagram_tool_bar -> addAction (m_edit_diagram_properties);
diagram_tool_bar -> addAction (m_conductor_reset);
diagram_tool_bar -> addAction (m_auto_conductor);
diagram_tool_bar -> addAction (m_auto_break_conductor);
//Sits with the conductor actions it works alongside: it colours
//the selected conductors and sets the colour of the next one drawn.
m_conductor_color_button = new ConductorColorToolButton(this, this);
diagram_tool_bar -> addWidget (m_conductor_color_button);
m_add_item_tool_bar = new QToolBar(tr("Ajouter"), this);
m_add_item_tool_bar->setObjectName("adding");
m_add_item_tool_bar->addActions(m_add_item_actions_group.actions());
m_depth_tool_bar = new QToolBar(tr("Profondeur", "toolbar title"));
m_depth_tool_bar->setObjectName("diagram_depth_toolbar");
m_depth_tool_bar->addActions(m_depth_action_group->actions());
//The toolbar buttons that are widgets, not commands. Each is held by
//an action of this window, so it survives being taken off a toolbar.
auto add_widget = [this](const QString &id, QWidget *widget) {
auto *action = new QWidgetAction(this);
action->setDefaultWidget(widget);
action->setText(DiagramToolbarSettings::widgetTitle(id));
m_toolbar_widgets.insert(id, action);
};
add_widget(QStringLiteral("widget:handler_size"), new DiagramEditorHandlerSizeWidget(this));
add_widget(QStringLiteral("widget:text_grid"), m_text_grid_button);
add_widget(QStringLiteral("widget:background_color"), m_background_color_button);
//Sits with the conductor actions it works alongside: it colours
//the selected conductors and sets the colour of the next one drawn.
m_conductor_color_button = new ConductorColorToolButton(this, this);
add_widget(QStringLiteral("widget:conductor_color"), m_conductor_color_button);
addToolBar(Qt::TopToolBarArea, main_tool_bar);
addToolBar(Qt::TopToolBarArea, view_tool_bar);
@@ -1375,6 +1356,9 @@ void QETDiagramEditor::setUpToolBar()
addToolBar(Qt::TopToolBarArea, m_add_item_tool_bar);
addToolBar(Qt::TopToolBarArea, m_depth_tool_bar);
DiagramToolbarSettings::markWindow(this);
rebuildToolBars();
m_scripts_tool_bar = new QToolBar(tr("Scripts", "toolbar title"), this);
m_scripts_tool_bar->setObjectName("scripts");
addToolBar(Qt::TopToolBarArea, m_scripts_tool_bar);
@@ -1384,6 +1368,59 @@ void QETDiagramEditor::setUpToolBar()
#endif
}
/**
@brief QETDiagramEditor::rebuildToolBars
Fill every toolbar from DiagramToolbarSettings, adding the user's own
toolbars that this window does not have yet and removing those that
were deleted. Called when the window is built, when the settings
change, and when the stored scripts change (a script can be on any
toolbar).
*/
void QETDiagramEditor::rebuildToolBars()
{
const QList<DiagramToolbarSettings::Toolbar> custom = DiagramToolbarSettings::customToolbars();
QStringList custom_names;
for (const DiagramToolbarSettings::Toolbar &toolbar : custom) {
custom_names << toolbar.name;
}
for (QToolBar *toolbar : m_custom_tool_bars) {
if (!custom_names.contains(toolbar->objectName())) {
removeToolBar(toolbar);
toolbar->deleteLater();
}
}
m_custom_tool_bars.erase(std::remove_if(m_custom_tool_bars.begin(), m_custom_tool_bars.end(),
[&custom_names](QToolBar *toolbar) { return !custom_names.contains(toolbar->objectName()); }),
m_custom_tool_bars.end());
for (const DiagramToolbarSettings::Toolbar &toolbar : custom)
{
auto existing = std::find_if(m_custom_tool_bars.cbegin(), m_custom_tool_bars.cend(),
[&toolbar](QToolBar *t) { return t->objectName() == toolbar.name; });
if (existing != m_custom_tool_bars.cend()) {
(*existing)->setWindowTitle(toolbar.title);
continue;
}
auto *new_tool_bar = new QToolBar(toolbar.title, this);
new_tool_bar->setObjectName(toolbar.name);
new_tool_bar->setMovable(!ToolbarSettings::locked());
addToolBar(Qt::TopToolBarArea, new_tool_bar);
m_custom_tool_bars << new_tool_bar;
}
auto resolve = [this](const QString &id) -> QAction * {
if (DiagramToolbarSettings::isWidget(id)) {
return m_toolbar_widgets.value(id);
}
return ShortcutManager::instance().action(id, this);
};
const QList<QToolBar *> built_in {main_tool_bar, view_tool_bar, diagram_tool_bar,
m_add_item_tool_bar, m_depth_tool_bar};
for (QToolBar *toolbar : built_in + m_custom_tool_bars) {
DiagramToolbarSettings::fill(toolbar, DiagramToolbarSettings::ids(toolbar->objectName()), resolve);
}
}
/**
@brief QETDiagramEditor::setUpMenu
*/
@@ -1438,12 +1475,9 @@ void QETDiagramEditor::setUpMenu()
menu_edition -> addAction(m_paste_origin);
menu_edition -> addAction(m_duplicate);
menu_edition -> addAction(m_configure_duplicate);
menu_edition -> addSeparator();
menu_edition -> addAction(m_insert_last_element);
menu_edition -> addAction(m_show_element_picker);
menu_edition -> addAction(m_show_shortcut_bar);
menu_edition -> addAction(m_repeat_last_command);
menu_edition -> addAction(m_command_search);
menu_edition -> addSeparator();
//The same actions the "Ajouter" toolbar holds. They were toolbar-only,
//which left them unreachable for anyone working without a mouse: a
//toolbar button has no key, so text fields, images and every drawing
@@ -1453,9 +1487,24 @@ void QETDiagramEditor::setUpMenu()
m_add_item_menu -> setIcon(QET::Icons::Add);
m_add_item_menu -> addActions(m_add_item_actions_group.actions());
menu_edition -> addSeparator();
menu_edition -> addActions(m_select_actions_group.actions());
//The menu had grown to over forty entries and no longer fitted on a
//laptop screen (issue #1336). Whole families now sit one level down,
//as Ajouter and Aligner already did; every action and its shortcut
//is unchanged.
QMenu *select_menu = menu_edition -> addMenu(QET::Icons::EditSelectAll, tr("Sélection"));
select_menu -> addActions(m_select_actions_group.actions());
menu_edition -> addSeparator();
menu_edition -> addActions(m_selection_actions_group.actions());
menu_edition -> addAction(m_delete_selection);
menu_edition -> addAction(m_rotate_selection);
menu_edition -> addAction(m_rotate_group_selection);
menu_edition -> addAction(m_rotate_texts);
menu_edition -> addAction(m_find_element);
menu_edition -> addAction(m_edit_selection);
QMenu *group_menu = menu_edition -> addMenu(QET::Icons::textGroup, tr("Grouper"));
group_menu -> addAction(m_group_selection);
group_menu -> addAction(m_ungroup_selection);
group_menu -> addSeparator();
group_menu -> addAction(m_group_selected_texts);
m_align_menu = menu_edition -> addMenu(tr("Aligner"));
//Snap to grid, then the horizontal three, then the vertical three,
//in the order setUpActions() adds them
@@ -1468,18 +1517,20 @@ void QETDiagramEditor::setUpMenu()
menu_edition -> addAction(m_conductor_reset);
menu_edition -> addSeparator();
menu_edition -> addAction(m_edit_diagram_properties);
menu_edition -> addActions(m_row_column_actions_group.actions());
//Not added to a menu here: it exists so the folio's context menu can
//hold the row and column actions one level down (see
//Shared with the folio's context menu (see
//DiagramView::contextMenuActions()).
m_row_column_menu = new QMenu(tr("Lignes et colonnes"), this);
m_row_column_menu = menu_edition -> addMenu(tr("Lignes et colonnes"));
m_row_column_menu -> setIcon(QET::Icons::EditTableInsertColumnRight);
m_row_column_menu -> addActions(m_row_column_actions_group.actions());
menu_edition -> addSeparator();
menu_edition -> addActions(m_depth_action_group->actions());
QMenu *depth_menu = menu_edition -> addMenu(QET::Icons::BringForward, tr("Profondeur"));
depth_menu -> addActions(m_depth_action_group->actions());
menu_edition -> addSeparator();
menu_edition -> addAction(m_find);
menu_edition -> addAction(m_jump_to_element);
menu_edition -> addAction(m_command_search);
menu_edition -> addSeparator();
menu_edition -> addAction(m_show_shortcut_bar);
menu_edition -> addAction(m_repeat_last_command);
// menu Projet
menu_project -> addAction(m_project_edit_properties);
@@ -4047,6 +4098,7 @@ void QETDiagramEditor::rebuildScriptActions()
if (has_scripts != m_had_scripts) m_scripts_tool_bar->setVisible(has_scripts);
m_had_scripts = has_scripts;
rebuildToolBars();
updateScriptActions();
}
+5
View File
@@ -140,6 +140,7 @@ class QETDiagramEditor : public QETMainWindow
void rememberPlacedElement(const ElementsLocation &location);
void showElementPicker();
void showShortcutBar();
void rebuildToolBars();
bool repeatLastCommand();
void generateTerminalBlock();
void setWindowedMode();
@@ -330,6 +331,10 @@ class QETDiagramEditor : public QETMainWindow
*m_add_item_tool_bar = nullptr,
*m_depth_tool_bar = nullptr,
*m_scripts_tool_bar = nullptr; ///< One button per stored script
/// Toolbars the user added (DiagramToolbarSettings)
QList<QToolBar *> m_custom_tool_bars;
/// The toolbar buttons that are widgets, by DiagramToolbarSettings id
QHash<QString, QAction *> m_toolbar_widgets;
#ifdef QET_HAS_SCRIPTING
QMenu *m_scripts_menu = nullptr;
QList<QAction *> m_script_actions; ///< One per stored script, rebuilt when the folder changes
+12 -1
View File
@@ -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()
@@ -22,6 +22,8 @@
#include "../qetapp.h"
#include "../richtext/richtexteditor_p.h"
#include "../textanchor.h"
#include "../utils/qetutils.h"
#include "../QetGraphicsItemModeler/textresizehandles.h"
/**
@brief DiagramTextItem::DiagramTextItem
@@ -335,6 +337,7 @@ void DiagramTextItem::focusInEvent(QFocusEvent *event)
m_previous_html_text = toHtml();
m_previous_text = toPlainText();
refreshTextResizeHandles();
}
/**
@@ -357,6 +360,89 @@ void DiagramTextItem::focusOutEvent(QFocusEvent *event)
setFlag(QGraphicsItem::ItemIsMovable, true);
setFlag(QGraphicsTextItem::ItemIsFocusable, false);
refreshTextResizeHandles();
}
/**
@brief DiagramTextItem::itemChange
Show or hide the resize handles with the selection and the scene.
@param change
@param value
@return
*/
QVariant DiagramTextItem::itemChange(GraphicsItemChange change, const QVariant &value)
{
//Also when the text comes back to a scene: Qt keeps it selected
//across removeItem()/addItem() (undoing a delete) and sends no
//selection change, and its handles were removed with the scene.
if (change == QGraphicsItem::ItemSelectedHasChanged
|| change == QGraphicsItem::ItemSceneHasChanged)
refreshTextResizeHandles();
return QGraphicsTextItem::itemChange(change, value);
}
/**
@brief DiagramTextItem::textResizeHandlesWanted
@return true when the corner handles to change the width of this text
should be shown. A text without a "textWidth" property never has them.
*/
bool DiagramTextItem::textResizeHandlesWanted() const
{
return false;
}
/**
@brief DiagramTextItem::isEditing
@return true while the text itself is being typed in
*/
bool DiagramTextItem::isEditing() const
{
return textInteractionFlags() & Qt::TextEditable;
}
/**
@brief DiagramTextItem::refreshTextResizeHandles
Create or remove the corner handles to change the width of this text,
according to textResizeHandlesWanted().
Called from itemChange() and when the edition starts or ends, not from
paint(): moving items from paint() left fragments behind
(qelectrotech#1002).
*/
void DiagramTextItem::refreshTextResizeHandles()
{
const bool wanted = scene() && textResizeHandlesWanted();
if (wanted && !m_resize_handles)
{
m_resize_handles = new TextResizeHandles(this, QETUtils::graphicsHandlerSize(this));
connect(m_resize_handles, &TextResizeHandles::resizeFinished,
this, &DiagramTextItem::pushResizeCommand);
}
else if (!wanted && m_resize_handles) {
removeTextResizeHandles();
}
}
/**
@brief DiagramTextItem::removeTextResizeHandles
*/
void DiagramTextItem::removeTextResizeHandles()
{
delete m_resize_handles;
m_resize_handles = nullptr;
}
/**
@brief DiagramTextItem::pushResizeCommand
Make the width change done with the resize handles undoable. The change
is already applied, live during the drag.
*/
void DiagramTextItem::pushResizeCommand(qreal old_width, qreal new_width,
QPointF old_pos, QPointF new_pos)
{
if (Diagram *diagram_ = diagram())
diagram_->undoStack().push(new TextResizeCommand(this, old_width, new_width,
old_pos, new_pos));
}
/**
+12
View File
@@ -24,6 +24,7 @@
class Diagram;
class QDomElement;
class QDomDocument;
class TextResizeHandles;
/**
This class represents a selectable, movable and editable text field on a
@@ -91,11 +92,15 @@ class DiagramTextItem : public QGraphicsTextItem
bool isHtml() const;
protected:
void refreshTextResizeHandles();
TextResizeHandles *textResizeHandles() const {return m_resize_handles;}
void paint(QPainter *,
const QStyleOptionGraphicsItem *,
QWidget *) override;
void focusInEvent(QFocusEvent *) override;
void focusOutEvent(QFocusEvent *) override;
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
void mouseDoubleClickEvent (QGraphicsSceneMouseEvent *event) override;
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
@@ -110,7 +115,10 @@ class DiagramTextItem : public QGraphicsTextItem
void prepareAlignment();
void finishAlignment();
virtual bool textResizeHandlesWanted() const;
virtual bool hasUserTextWidth() const {return false;}
bool isEditing() const;
void removeTextResizeHandles();
protected:
@@ -128,7 +136,11 @@ class DiagramTextItem : public QGraphicsTextItem
QPointF m_mouse_to_origin_movement;
private:
void pushResizeCommand(qreal old_width, qreal new_width,
QPointF old_pos, QPointF new_pos);
QRectF m_alignment_rect;
TextResizeHandles *m_resize_handles = nullptr;
Qt::Alignment m_alignment = (Qt::AlignTop | Qt::AlignLeft);
};
#endif
@@ -148,6 +148,16 @@ void IndependentTextItem::setTextWidth(qreal width)
emit textWidthChanged(width);
}
/**
@brief IndependentTextItem::textResizeHandlesWanted
@return true when this text is selected and not being typed in: the
corner handles then change its width.
*/
bool IndependentTextItem::textResizeHandlesWanted() const
{
return isSelected() && !isEditing();
}
void IndependentTextItem::focusOutEvent(QFocusEvent *event)
{
DiagramTextItem::focusOutEvent(event);
@@ -63,6 +63,7 @@ class IndependentTextItem : public DiagramTextItem
protected:
void focusOutEvent(QFocusEvent *event) override;
bool textResizeHandlesWanted() const override;
bool hasUserTextWidth() const override {return m_text_width > 0;}
private:
+22
View File
@@ -93,6 +93,11 @@ void QETMainWindow::initCommonActions()
}
});
customize_action_ = new QAction(QET::Icons::ConfigureToolbars, tr("&Personnaliser..."), this);
ShortcutManager::instance().registerAction(customize_action_, "mainwindow.customize", tr("Général"), QKeySequence());
customize_action_ -> setStatusTip(tr("Barres d'outils, barre de raccourcis, clavier et gestes de la souris, dans une seule fenêtre", "status bar tip"));
connect(customize_action_, &QAction::triggered, qet_app, [qet_app]() { qet_app->customizeQET(); });
export_config_action_ = new QAction(QET::Icons::DocumentExport, tr("Enregistrer la configuration sous..."), this);
export_config_action_ -> setStatusTip(tr("Enregistre les réglages de QElectroTech dans un fichier", "status bar tip"));
connect(export_config_action_, &QAction::triggered, qet_app, &QETApp::exportConfiguration);
@@ -190,6 +195,7 @@ void QETMainWindow::initCommonMenus()
settings_menu_ = new QMenu(tr("&Configuration", "window menu"), this);
settings_menu_ -> addAction(fullscreen_action_);
settings_menu_ -> addAction(configure_action_);
settings_menu_ -> addAction(customize_action_);
settings_menu_ -> addSeparator();
settings_menu_ -> addAction(export_config_action_);
settings_menu_ -> addAction(import_config_action_);
@@ -274,6 +280,22 @@ void QETMainWindow::updateFullScreenAction()
ShortcutManager::instance().registerAction(fullscreen_action_, "mainwindow.fullscreen", tr("Général"), Qt::CTRL | Qt::SHIFT | Qt::Key_F);
}
/**
@brief QETMainWindow::createPopupMenu
The menu shown on a right-click on a toolbar or a dock title, and the
Configuration > Afficher submenu: Qt's list of toolbars and docks,
then Personnaliser..., as in most applications with toolbars.
*/
QMenu *QETMainWindow::createPopupMenu()
{
QMenu *menu = QMainWindow::createPopupMenu();
if (menu) {
menu -> addSeparator();
menu -> addAction(customize_action_);
}
return menu;
}
/**
Check whether a sub menu dedicated to docks and toolbars can be inserted on
top of the settings menu.
+2
View File
@@ -43,6 +43,7 @@ class QETMainWindow : public QMainWindow {
protected:
void activateMenuBar();
bool event(QEvent *) override;
QMenu *createPopupMenu() override;
void dragEnterEvent(QDragEnterEvent *e) override;
void dropEvent(QDropEvent *e) override;
virtual void firstActivation(QEvent *);
@@ -56,6 +57,7 @@ class QETMainWindow : public QMainWindow {
// attributes
protected:
QAction *configure_action_; ///< Launch the QElectroTech configuration dialog
QAction *customize_action_; ///< Launch the Customise window (toolbars, keys, gestures)
QAction *export_config_action_; ///< Save the settings to a file
QAction *import_config_action_; ///< Replace the settings with a saved file
QAction *fullscreen_action_; ///< Toggle full screen
+10 -3
View File
@@ -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]);
+3
View File
@@ -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,
+2
View File
@@ -27,6 +27,7 @@
#include "templatelocationsaver.h"
#include "templatelogomanager.h"
#include "templateview.h"
#include "../toolbarsettings.h"
/**
@param parent parent QWidget of this window
@@ -46,6 +47,7 @@ QETTitleBlockTemplateEditor::QETTitleBlockTemplateEditor(QWidget *parent) :
initActions();
initMenus();
initToolbars();
ToolbarSettings::applyTo(this);
readSettings();
}
+31 -14
View File
@@ -1646,12 +1646,16 @@ void TitleBlockTemplate::render(QPainter &painter,
Width of the titleblock to render
@param file_path :
@param color :
@param dxf_transform :
Applied to every DXF point after the title block is laid out, as the
QPainter is rotated for a title block at the right of the folio
*/
void TitleBlockTemplate::renderDxf(QRectF &title_block_rect,
const DiagramContext &diagram_context,
int titleblock_width,
QString &file_path,
int color) const
int color,
const QTransform &dxf_transform) const
{
QList<int> widths = columnsWidth(titleblock_width);
@@ -1663,12 +1667,13 @@ void TitleBlockTemplate::renderDxf(QRectF &title_block_rect,
*Createdxf::yScale;
double recWidth = title_block_rect.width() * Createdxf::xScale;
double recHeight = title_block_rect.height() * Createdxf::yScale;
Createdxf::drawRectangle(file_path,
xCoord,
yCoord,
recWidth,
recHeight,
color);
Createdxf::drawPolyline(file_path,
dxf_transform.map(QPolygonF(QRectF(xCoord,
yCoord,
recWidth,
recHeight))),
color,
true);
// run through each individual cell
for (int j = 0 ; j < rows_heights_.count() ; ++ j) {
@@ -1702,7 +1707,10 @@ void TitleBlockTemplate::renderDxf(QRectF &title_block_rect,
y = yCoord + recHeight - h - y*Createdxf::yScale;
w *= Createdxf::xScale;
Createdxf::drawRectangle(file_path, x, y, w, h, color);
Createdxf::drawPolyline(file_path,
dxf_transform.map(QPolygonF(QRectF(x, y, w, h))),
color,
true);
if (cells_[i][j] -> type() == TitleBlockCell::TextCell)
{
QString final_text =
@@ -1715,7 +1723,8 @@ void TitleBlockTemplate::renderDxf(QRectF &title_block_rect,
y,
w,
h,
color);
color,
dxf_transform);
}
}
}
@@ -1967,6 +1976,7 @@ void TitleBlockTemplate::renderTextCell(QPainter &painter,
@param w
@param h
@param color
@param dxf_transform : see renderDxf()
*/
void TitleBlockTemplate::renderTextCellDxf(
QString &file_path,
@@ -1976,7 +1986,8 @@ void TitleBlockTemplate::renderTextCellDxf(
qreal y,
qreal w,
qreal h,
int color) const
int color,
const QTransform &dxf_transform) const
{
if (text.isEmpty()) return;
QFont text_font = TitleBlockTemplate::fontForCell(cell);
@@ -2038,6 +2049,8 @@ void TitleBlockTemplate::renderTextCellDxf(
const QStringList lines = text.split(QRegularExpression(QStringLiteral("\r\n|\r|\n")));
const qreal line_spacing = textHeight * Createdxf::yScale * 1.6;
const int last = lines.size() - 1;
const qreal rotation = qRadiansToDegrees(
qAtan2(dxf_transform.m12(), dxf_transform.m11()));
for (int i = 0 ; i < lines.size() ; ++i)
{
const QString &line = lines.at(i);
@@ -2064,17 +2077,21 @@ void TitleBlockTemplate::renderTextCellDxf(
}
// x offset value below currently set heuristically based on appearance...
const QPointF insert = dxf_transform.map(
QPointF(x - 2*Createdxf::xScale, y1 + offset));
const QPointF align = dxf_transform.map(QPointF(x2, y1 + offset));
Createdxf::drawTextAligned(
file_path,
line,
x - 2*Createdxf::xScale,
y1 + offset,
insert.x(),
insert.y(),
textHeight*Createdxf::yScale,
0,
rotation,
0,
hAlign,
vAlign,
x2,
align.x(),
align.y(),
ratio,
color);
}
+4 -2
View File
@@ -176,7 +176,8 @@ class TitleBlockTemplate : public QObject {
const DiagramContext &,
int,
QString &,
int) const;
int,
const QTransform & = QTransform()) const;
void renderCell(QPainter &,
const TitleBlockCell &,
const DiagramContext &,
@@ -245,7 +246,8 @@ class TitleBlockTemplate : public QObject {
qreal,
qreal,
qreal,
int) const;
int,
const QTransform &) const;
// attributes
private:
+5 -2
View File
@@ -91,15 +91,18 @@ void TitleBlockTemplateRenderer::render(QPainter *provided_painter,
@param titleblock_width
@param file_path
@param color
@param dxf_transform : see TitleBlockTemplate::renderDxf()
*/
void TitleBlockTemplateRenderer::renderDxf(QRectF &title_block_rect,
int titleblock_width,
QString &file_path,
int color) {
int color,
const QTransform &dxf_transform) {
if (!m_titleblock_template) return;
m_titleblock_template -> renderDxf(title_block_rect,
m_context,
titleblock_width,
file_path, color);
file_path, color,
dxf_transform);
}
+3 -1
View File
@@ -18,6 +18,7 @@
#ifndef TITLEBLOCK_TEMPLATE_RENDERER_H
#define TITLEBLOCK_TEMPLATE_RENDERER_H
#include <QPicture>
#include <QTransform>
#include "diagramcontext.h"
class TitleBlockTemplate;
@@ -40,7 +41,8 @@ class TitleBlockTemplateRenderer : public QObject
int height() const;
void render(QPainter *, int);
void renderDxf(QRectF &, int, QString &, int);
void renderDxf(QRectF &, int, QString &, int,
const QTransform & = QTransform());
private:
const TitleBlockTemplate *m_titleblock_template;
+109
View File
@@ -0,0 +1,109 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#include "toolbarsettings.h"
#include <QApplication>
#include <QMainWindow>
#include <QSettings>
#include <QToolBar>
namespace {
const QString ICON_SIZE = QStringLiteral("toolbars/icon_size");
const QString BUTTON_STYLE = QStringLiteral("toolbars/button_style");
const QString LOCKED = QStringLiteral("toolbars/locked");
//Set on a window by applyTo()
const char *const APPLIED = "qetToolbarSettings";
}
int ToolbarSettings::iconSize()
{
return QSettings().value(ICON_SIZE, 0).toInt();
}
Qt::ToolButtonStyle ToolbarSettings::buttonStyle()
{
const int style = QSettings().value(BUTTON_STYLE, int(Qt::ToolButtonIconOnly)).toInt();
switch (style) {
case Qt::ToolButtonTextBesideIcon:
case Qt::ToolButtonTextUnderIcon:
return Qt::ToolButtonStyle(style);
default:
return Qt::ToolButtonIconOnly;
}
}
bool ToolbarSettings::locked()
{
return QSettings().value(LOCKED, false).toBool();
}
/**
@brief ToolbarSettings::save
Store the three values; a default value removes its key.
*/
void ToolbarSettings::save(int icon_size, Qt::ToolButtonStyle style, bool locked)
{
QSettings settings;
if (icon_size > 0) settings.setValue(ICON_SIZE, icon_size);
else settings.remove(ICON_SIZE);
if (style != Qt::ToolButtonIconOnly) settings.setValue(BUTTON_STYLE, int(style));
else settings.remove(BUTTON_STYLE);
if (locked) settings.setValue(LOCKED, true);
else settings.remove(LOCKED);
}
/**
@brief ToolbarSettings::applyTo
Apply the settings to \a window and its toolbars. The window passes
its icon size and button style on to every toolbar that has none of
its own; an invalid size gives the style's default back. The window
is marked, so applyToAll() updates it again later.
*/
void ToolbarSettings::applyTo(QMainWindow *window)
{
if (!window) {
return;
}
window->setProperty(APPLIED, true);
const int size = iconSize();
window->setIconSize(size > 0 ? QSize(size, size) : QSize());
window->setToolButtonStyle(buttonStyle());
const bool movable = !locked();
for (QToolBar *toolbar : window->findChildren<QToolBar *>()) {
//Not a toolbar inside a panel: only the window's own
if (window->toolBarArea(toolbar) != Qt::NoToolBarArea) {
toolbar->setMovable(movable);
}
}
}
/**
@brief ToolbarSettings::applyToAll
Apply the settings again to every open window that took them with
applyTo(), after they change. Other windows are left alone: they
would lose the settings the next time they open.
*/
void ToolbarSettings::applyToAll()
{
for (QWidget *widget : QApplication::topLevelWidgets()) {
auto *window = qobject_cast<QMainWindow *>(widget);
if (window && window->property(APPLIED).toBool()) {
applyTo(window);
}
}
}
+45
View File
@@ -0,0 +1,45 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#ifndef TOOLBARSETTINGS_H
#define TOOLBARSETTINGS_H
#include <QSize>
#include <Qt>
class QMainWindow;
/**
@brief How every window's toolbars look: icon size, text with the
icons, and whether they can be moved. Stored in QSettings; a value
the user never changed keeps Qt's default, so nothing changes for
anyone who does not set one.
*/
class ToolbarSettings
{
public:
/// Icon size in pixels, or 0 for the style's default size
static int iconSize();
static Qt::ToolButtonStyle buttonStyle();
static bool locked();
static void save(int icon_size, Qt::ToolButtonStyle style, bool locked);
static void applyTo(QMainWindow *window);
static void applyToAll();
};
#endif // TOOLBARSETTINGS_H
+229
View File
@@ -18,8 +18,89 @@
#include "borderpropertieswidget.h"
#include "../diagram.h"
#include "../qetapp.h"
#include "../titleblocktemplate.h"
#include "ui_borderpropertieswidget.h"
#include <iterator>
#include <tuple>
namespace {
/// ISO 216 A sizes in mm, landscape
struct PaperSize { const char *name; int width_mm; int height_mm; };
const PaperSize paper_sizes[] = {
{"A0", 1189, 841},
{"A1", 841, 594},
{"A2", 594, 420},
{"A3", 420, 297},
{"A4", 297, 210},
{"A5", 210, 148}
};
/// The PDF export draws one scene unit as one pixel at 96 dpi
int mmToPx(int mm) { return qRound(mm * 96.0 / 25.4); }
/// The PDF export rounds a page within 3 pt of a paper size to that
/// size, and 4 px left over already rounds to 3.0 pt and misses
const int exact_fit = 3;
/// Smallest column width or row height a paper size may give
const int min_cell = 10;
/// Fewest columns or rows a folio may have: BorderTitleBlock raises
/// any lower count to 3 (MIN_COLUMN_COUNT, MIN_ROW_COUNT)
const int min_count = 3;
/**
@brief fitCells
Split @p available pixels into cells close to @p current pixels.
First choice: cells within 15 %, then 30 %, of the current size
that leave at most exact_fit pixels, the closest to the current
size. Failing that, cells within 15 % that leave the fewest pixels.
@return the number of cells and their size
*/
QPair<int, int> fitCells(int available, int current, int min_size, int max_count)
{
current = qMax(current, min_size);
auto size_for = [&](int count, double spread) {
const int size = available / count;
return (size < min_size || qAbs(size - current) > current * spread)
? 0 : size;
};
for (double spread : {0.15, 0.30})
{
QPair<int, int> best(0, 0);
for (int count = min_count ; count <= max_count ; ++count)
{
const int size = size_for(count, spread);
if (size && available - count * size <= exact_fit &&
(!best.first || qAbs(size - current) < qAbs(best.second - current)))
best = qMakePair(count, size);
}
if (best.first) return best;
}
QPair<int, int> best(0, 0);
std::tuple<int, int> best_score(available, available);
for (int count = min_count ; count <= max_count ; ++count)
{
const int size = size_for(count, 0.15);
const std::tuple<int, int> score(available - count * size,
qAbs(size - current));
if (size && score < best_score) {
best_score = score;
best = qMakePair(count, size);
}
}
if (best.first) return best;
//No size near the current one when the cell count is capped
const int count = qBound(min_count, qRound(double(available) / current), max_count);
return qMakePair(count, available / count);
}
}
/**
@brief BorderPropertiesWidget::BorderPropertiesWidget
default constructor
@@ -31,6 +112,30 @@ BorderPropertiesWidget::BorderPropertiesWidget(const BorderProperties &bp, QWidg
ui(new Ui::BorderPropertiesWidget)
{
ui->setupUi(this);
ui->m_paper_cb->addItem(tr("Personnalisé"));
for (const PaperSize &paper : paper_sizes)
ui->m_paper_cb->addItem(QString::fromLatin1(paper.name));
ui->m_orientation_cb->addItem(tr("Paysage"));
ui->m_orientation_cb->addItem(tr("Portrait"));
if (TitleBlockTemplate *tbt = QETApp::defaultTitleBlockTemplate())
m_titleblock_height = tbt->height();
//activated() is only emitted for a choice the user makes
auto paper_picked = [this]() {
m_paper_picked = ui->m_paper_cb->currentIndex() > 0;
applyPaperSize();
};
connect(ui->m_paper_cb, qOverload<int>(&QComboBox::activated),
this, paper_picked);
connect(ui->m_orientation_cb, qOverload<int>(&QComboBox::activated),
this, paper_picked);
for (QSpinBox *sb : {ui->m_colums_count_sp, ui->m_columns_width_sp,
ui->m_rows_count_sp, ui->m_rows_height_sp})
connect(sb, qOverload<int>(&QSpinBox::valueChanged),
this, &BorderPropertiesWidget::showMatchingPaperSize);
setProperties(bp);
}
@@ -57,6 +162,7 @@ void BorderPropertiesWidget::setProperties(const BorderProperties &bp)
ui -> m_rows_count_sp ->setValue (m_properties.rows_count);
ui -> m_rows_height_sp ->setValue (m_properties.rows_height);
ui -> m_display_rows_cb ->setChecked (m_properties.display_rows);
showMatchingPaperSize();
}
/**
@@ -83,3 +189,126 @@ void BorderPropertiesWidget::setReadOnly(const bool &ro)
{
ui->border_gb->setDisabled(ro);
}
/**
@brief BorderPropertiesWidget::setTitleBlockSize
Tell the widget the title block the folio uses: its height counts in
the size of the page. If the user picked a paper size in this widget,
the columns and rows are fitted again so the folio keeps that size;
otherwise they are left alone.
@param height : height of the title block template
@param edge : edge of the folio the title block sits on
*/
void BorderPropertiesWidget::setTitleBlockSize(int height, Qt::Edge edge)
{
m_titleblock_height = height;
m_titleblock_edge = edge;
if (m_paper_picked)
applyPaperSize();
else
showMatchingPaperSize();
}
/**
@brief BorderPropertiesWidget::applyPaperSize
Set the columns and rows so the folio, title block included, has the
size of the chosen paper. The column width and row height stay close
to the current ones.
*/
void BorderPropertiesWidget::applyPaperSize()
{
const int index = ui->m_paper_cb->currentIndex() - 1;
ui->m_orientation_cb->setEnabled(index >= 0);
if (index < 0) return;
int columns, column_width, rows, row_height;
if (!fitPaperSize(index, ui->m_orientation_cb->currentIndex() == 0,
columns, column_width, rows, row_height))
{
//The title block leaves no room on this paper
showMatchingPaperSize();
return;
}
m_updating = true;
ui->m_colums_count_sp->setValue(columns);
ui->m_columns_width_sp->setValue(column_width);
ui->m_rows_count_sp->setValue(rows);
ui->m_rows_height_sp->setValue(row_height);
m_updating = false;
}
/**
@brief BorderPropertiesWidget::fitPaperSize
Compute the columns and rows that give the folio, title block
included, the size of a paper, with cells close to the current ones.
@param index : index of the paper in paper_sizes
@param landscape : true for landscape, false for portrait
@return false if the paper is too small for the title block
*/
bool BorderPropertiesWidget::fitPaperSize(int index, bool landscape,
int &columns, int &column_width,
int &rows, int &row_height) const
{
const PaperSize &paper = paper_sizes[index];
const int page_width = mmToPx(landscape ? paper.width_mm : paper.height_mm);
const int page_height = mmToPx(landscape ? paper.height_mm : paper.width_mm);
//The page is one pixel larger than the border, for its line
int width = page_width - 1 - qRound(m_properties.rows_header_width);
int height = page_height - 1 - qRound(m_properties.columns_header_height);
if (m_titleblock_edge == Qt::BottomEdge)
height -= m_titleblock_height;
else
width -= m_titleblock_height;
if (width < min_cell * min_count || height < min_cell * min_count)
return false;
const QPair<int, int> c = fitCells(width,
ui->m_columns_width_sp->value(),
min_cell,
ui->m_colums_count_sp->maximum());
const QPair<int, int> r = fitCells(height,
ui->m_rows_height_sp->value(),
min_cell,
ui->m_rows_count_sp->maximum());
columns = c.first;
column_width = c.second;
rows = r.first;
row_height = r.second;
return true;
}
/**
@brief BorderPropertiesWidget::showMatchingPaperSize
Show the paper size the folio has, or "Custom" if it has none: the
folio has a paper size when choosing it would change nothing.
*/
void BorderPropertiesWidget::showMatchingPaperSize()
{
if (m_updating) return;
m_paper_picked = false;
int found = 0;
int orientation = ui->m_orientation_cb->currentIndex();
for (int i = 0 ; i < int(std::size(paper_sizes)) && !found ; ++i)
{
for (int o = 0 ; o < 2 ; ++o)
{
int columns, column_width, rows, row_height;
if (fitPaperSize(i, o == 0, columns, column_width, rows, row_height) &&
columns == ui->m_colums_count_sp->value() &&
column_width == ui->m_columns_width_sp->value() &&
rows == ui->m_rows_count_sp->value() &&
row_height == ui->m_rows_height_sp->value())
{
found = i + 1;
orientation = o;
break;
}
}
}
ui->m_paper_cb->setCurrentIndex(found);
ui->m_orientation_cb->setCurrentIndex(orientation);
ui->m_orientation_cb->setEnabled(found > 0);
}
+16 -2
View File
@@ -41,12 +41,26 @@ class BorderPropertiesWidget : public QWidget
void setProperties(const BorderProperties &bp);
const BorderProperties &properties();
void setReadOnly (const bool &ro);
private slots:
void setTitleBlockSize(int height, Qt::Edge edge);
private:
void applyPaperSize();
void showMatchingPaperSize();
bool fitPaperSize(int index, bool landscape,
int &columns, int &column_width,
int &rows, int &row_height) const;
Ui::BorderPropertiesWidget *ui;
BorderProperties m_properties;
/// height of the title block the folio is drawn with, and
/// the edge it sits on: it counts in the size of the page
int m_titleblock_height = 0;
Qt::Edge m_titleblock_edge = Qt::BottomEdge;
/// true while the widget itself changes the spin boxes
bool m_updating = false;
/// true once the user picked a paper size, until the columns or
/// rows are edited by hand
bool m_paper_picked = false;
};
#endif // BORDERPROPERTIESWIDGET_H
+19
View File
@@ -80,7 +80,24 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="m_paper_label">
<property name="text">
<string>Format :</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="m_paper_cb">
<property name="toolTip">
<string>Règle les colonnes et les lignes pour que le folio, cartouche compris, ait la taille de ce format de papier, à 96 points par pouce comme l'export PDF.</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QComboBox" name="m_orientation_cb"/>
</item>
<item row="3" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -105,6 +122,8 @@
<tabstop>m_rows_height_sp</tabstop>
<tabstop>m_display_columns_cb</tabstop>
<tabstop>m_display_rows_cb</tabstop>
<tabstop>m_paper_cb</tabstop>
<tabstop>m_orientation_cb</tabstop>
</tabstops>
<resources/>
<connections/>
+2 -2
View File
@@ -78,9 +78,9 @@ void CompositeTextEditDialog::setUpComboBox(bool is_report)
}
}
void CompositeTextEditDialog::on_m_info_cb_activated(const QString &arg1)
void CompositeTextEditDialog::on_m_info_cb_activated(int index)
{
Q_UNUSED(arg1)
Q_UNUSED(index)
ui->m_plain_text_edit->insertPlainText(ui->m_info_cb->currentData().toString());
}
+1 -1
View File
@@ -28,7 +28,7 @@ class CompositeTextEditDialog : public QDialog
QString plainText() const;
private slots:
void on_m_info_cb_activated(const QString &arg1);
void on_m_info_cb_activated(int index);
protected:
void focusInEvent(QFocusEvent *event) override;
+4
View File
@@ -139,6 +139,10 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
}
connect(ipw, &TitleBlockPropertiesWidget::openAutoNumFolioEditor, this, &NewDiagramPage::changeToAutoFolioTab);
bpw -> setTitleBlockSize(ipw -> currentTitleBlockHeight(),
ipw -> currentTitleBlockEdge());
connect(ipw, &TitleBlockPropertiesWidget::titleBlockSizeChanged,
bpw, &BorderPropertiesWidget::setTitleBlockSize);
// main tab widget
QTabWidget *tab_widget = new QTabWidget(this);
@@ -0,0 +1,475 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#include "gesturesconfigpage.h"
#include "../../gesturesettings.h"
#include "../../qeticons.h"
#include "../../shortcutmanager.h"
#include <QAction>
#include <QComboBox>
#include <QDataStream>
#include <QDragEnterEvent>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QMimeData>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPushButton>
#include <QVBoxLayout>
#include <QtMath>
namespace {
const int ring_radius = 100;
const int ring_inner = 26;
const int slot_radius = 68;
const int slot_icon = 24;
/// What QListWidget puts on a drag
const QString list_mime = QStringLiteral("application/x-qabstractitemmodeldatalist");
/**
@return the command id carried by a drag from the command list, or an
empty string
*/
QString droppedId(const QMimeData *mime)
{
if (!mime || !mime->hasFormat(list_mime)) {
return QString();
}
QByteArray data = mime->data(list_mime);
QDataStream stream(&data, QIODevice::ReadOnly);
int row, column;
QMap<int, QVariant> roles;
stream >> row >> column >> roles;
return roles.value(Qt::UserRole).toString();
}
}
GestureRingEditor::GestureRingEditor(QWidget *parent) :
QWidget(parent)
{
setAcceptDrops(true);
setFocusPolicy(Qt::StrongFocus);
setMinimumSize(sizeHint());
}
QSize GestureRingEditor::sizeHint() const
{
return QSize(2 * ring_radius + 8, 2 * ring_radius + 8);
}
void GestureRingEditor::setIds(const QStringList &ids)
{
m_ids = ids;
update();
}
void GestureRingEditor::setDirections(int directions)
{
m_directions = directions == 4 ? 4 : 8;
setCurrentSlot(qMin(m_current, m_directions - 1));
update();
}
void GestureRingEditor::setDescriptions(const QHash<QString, QString> &descriptions)
{
m_descriptions = descriptions;
update();
}
void GestureRingEditor::setCurrentSlot(int slot)
{
if (slot < 0 || slot >= m_directions) {
return;
}
const bool changed = slot != m_current;
m_current = slot;
update();
if (changed) {
emit currentSlotChanged(slot);
}
}
/**
@return the slot in the direction of @a pos from the centre, 0 at the
top then clockwise, or -1 at the centre or outside the ring
*/
int GestureRingEditor::slotAt(const QPoint &pos) const
{
const QPointF d = QPointF(pos) - QPointF(rect().center());
const qreal distance = qHypot(d.x(), d.y());
if (distance < ring_inner || distance > ring_radius) {
return -1;
}
qreal angle = qRadiansToDegrees(qAtan2(d.x(), -d.y()));
if (angle < 0) {
angle += 360;
}
return int(qRound(angle / (360.0 / m_directions))) % m_directions;
}
/**
@brief GestureRingEditor::place
Put @a id in the current slot. A command already elsewhere on the ring
moves here, so a command has one direction.
*/
void GestureRingEditor::place(const QString &id)
{
if (id.isEmpty()) {
return;
}
const int old = m_ids.indexOf(id);
if (old >= 0) {
m_ids[old].clear();
}
while (m_ids.count() <= m_current) {
m_ids << QString();
}
m_ids[m_current] = id;
update();
emit edited();
}
void GestureRingEditor::clearCurrent()
{
if (m_current < m_ids.count() && !m_ids.at(m_current).isEmpty()) {
m_ids[m_current].clear();
update();
emit edited();
}
}
QString GestureRingEditor::idAt(int slot) const
{
return slot < m_ids.count() ? m_ids.at(slot) : QString();
}
QPointF GestureRingEditor::slotCenter(int slot) const
{
const qreal a = qDegreesToRadians(slot * 360.0 / m_directions);
const QPointF c = QRectF(rect()).center();
return QPointF(c.x() + slot_radius * qSin(a), c.y() - slot_radius * qCos(a));
}
void GestureRingEditor::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
const QPointF c = QRectF(rect()).center();
const qreal step = 360.0 / m_directions;
const QRectF outer(c.x() - ring_radius, c.y() - ring_radius,
2 * ring_radius, 2 * ring_radius);
QColor line = palette().color(QPalette::Text);
line.setAlpha(110);
painter.setPen(line);
painter.setBrush(palette().color(QPalette::Base));
painter.drawEllipse(outer);
//The chosen slot
QPainterPath wedge;
wedge.moveTo(c);
wedge.arcTo(outer, 90 - m_current * step - step / 2, step);
wedge.closeSubpath();
painter.setPen(Qt::NoPen);
QColor highlight = palette().color(QPalette::Highlight);
if (!hasFocus()) {
highlight.setAlpha(140);
}
painter.setBrush(highlight);
painter.drawPath(wedge);
//Lines between the slots
painter.setPen(line);
for (int i = 0 ; i < m_directions ; ++i) {
const qreal a = qDegreesToRadians(i * step + step / 2);
painter.drawLine(QPointF(c.x() + ring_inner * qSin(a), c.y() - ring_inner * qCos(a)),
QPointF(c.x() + ring_radius * qSin(a), c.y() - ring_radius * qCos(a)));
}
painter.setBrush(palette().color(QPalette::Window));
painter.drawEllipse(c, ring_inner, ring_inner);
for (int i = 0 ; i < m_directions ; ++i)
{
const QString id = idAt(i);
if (id.isEmpty()) {
continue;
}
const QPointF p = slotCenter(i);
const QRect r(int(p.x()) - slot_icon / 2, int(p.y()) - slot_icon / 2,
slot_icon, slot_icon);
QAction *action = ShortcutManager::instance().action(id, nullptr);
if (action && !action->icon().isNull()) {
action->icon().paint(&painter, r);
} else {
painter.setPen(palette().color(i == m_current ? QPalette::HighlightedText
: QPalette::Text));
painter.drawText(r.adjusted(-12, 0, 12, 0), Qt::AlignCenter,
m_descriptions.value(id, id).left(3));
}
}
}
void GestureRingEditor::mousePressEvent(QMouseEvent *event)
{
setCurrentSlot(slotAt(event->position().toPoint()));
event->accept();
}
void GestureRingEditor::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace) {
clearCurrent();
} else if (event->key() == Qt::Key_Right || event->key() == Qt::Key_Down) {
setCurrentSlot((m_current + 1) % m_directions);
} else if (event->key() == Qt::Key_Left || event->key() == Qt::Key_Up) {
setCurrentSlot((m_current + m_directions - 1) % m_directions);
} else {
QWidget::keyPressEvent(event);
}
}
void GestureRingEditor::dragEnterEvent(QDragEnterEvent *event)
{
if (!droppedId(event->mimeData()).isEmpty()) {
event->acceptProposedAction();
}
}
void GestureRingEditor::dragMoveEvent(QDragMoveEvent *event)
{
const int slot = slotAt(event->position().toPoint());
if (slot < 0) {
event->ignore();
return;
}
setCurrentSlot(slot);
event->acceptProposedAction();
}
void GestureRingEditor::dropEvent(QDropEvent *event)
{
const int slot = slotAt(event->position().toPoint());
const QString id = droppedId(event->mimeData());
if (slot < 0 || id.isEmpty()) {
event->ignore();
return;
}
setCurrentSlot(slot);
place(id);
event->acceptProposedAction();
}
/**
@brief GesturesConfigPage::GesturesConfigPage
@param parent
*/
GesturesConfigPage::GesturesConfigPage(QWidget *parent) :
ConfigPage(parent)
{
const QStringList available = ShortcutBarSettings::availableIds();
for (const ShortcutManager::ShortcutInfo &info :
ShortcutManager::instance().allShortcuts()) {
if (available.contains(info.id)) {
m_descriptions.insert(info.id, info.description);
}
}
for (const ShortcutBarSettings::Context c : ShortcutBarSettings::contexts()) {
m_pending.insert(c, GestureSettings::ids(c));
m_custom.insert(c, GestureSettings::isCustom(c));
}
auto *explanation = new QLabel(
tr("Faites glisser avec le bouton droit sur un folio : un anneau "
"de commandes s'ouvre, relâchez vers celle à lancer. Tant que "
"vous ne changez pas l'anneau d'un contexte, il montre les "
"commandes de la barre de raccourcis. Les gestes s'activent "
"ou se désactivent dans la page Général."), this);
explanation->setWordWrap(true);
m_directions = new QComboBox(this);
m_directions->setObjectName(QStringLiteral("directionsCombo"));
m_directions->addItem(tr("8 directions"), 8);
m_directions->addItem(tr("4 directions"), 4);
m_directions->setCurrentIndex(m_directions->findData(GestureSettings::directions()));
m_context = new QComboBox(this);
m_context->setObjectName(QStringLiteral("contextCombo"));
for (const ShortcutBarSettings::Context c : ShortcutBarSettings::contexts()) {
m_context->addItem(ShortcutBarSettings::title(c), c);
}
m_available = new QListWidget(this);
m_available->setObjectName(QStringLiteral("availableList"));
m_available->setSortingEnabled(true);
m_available->setDragEnabled(true);
m_available->setDragDropMode(QAbstractItemView::DragOnly);
for (auto it = m_descriptions.cbegin(); it != m_descriptions.cend(); ++it) {
QIcon icon;
if (QAction *action = ShortcutManager::instance().action(it.key(), nullptr)) {
icon = action->icon();
}
auto *item = new QListWidgetItem(icon, it.value(), m_available);
item->setData(Qt::UserRole, it.key());
}
m_ring = new GestureRingEditor(this);
m_ring->setObjectName(QStringLiteral("ringEditor"));
m_ring->setDescriptions(m_descriptions);
m_ring->setDirections(GestureSettings::directions());
m_slot_name = new QLabel(this);
m_slot_name->setAlignment(Qt::AlignCenter);
m_slot_name->setWordWrap(true);
m_state = new QLabel(this);
m_state->setWordWrap(true);
auto *place = new QPushButton(tr("Placer →"), this);
place->setObjectName(QStringLiteral("placeButton"));
auto *clear = new QPushButton(tr("Vider la direction"), this);
auto *follow = new QPushButton(tr("Comme la barre de raccourcis"), this);
follow->setObjectName(QStringLiteral("followBarButton"));
auto *ring_column = new QVBoxLayout();
ring_column->addWidget(m_ring, 0, Qt::AlignHCenter);
ring_column->addWidget(m_slot_name);
ring_column->addWidget(clear);
ring_column->addStretch();
auto *grid = new QGridLayout();
grid->addWidget(new QLabel(tr("Commandes disponibles"), this), 0, 0);
grid->addWidget(new QLabel(tr("Anneau, la direction choisie en surbrillance"), this), 0, 2);
grid->addWidget(m_available, 1, 0);
grid->addWidget(place, 1, 1, Qt::AlignVCenter);
grid->addLayout(ring_column, 1, 2);
grid->setColumnStretch(0, 1);
grid->setColumnStretch(2, 1);
auto *top_row = new QHBoxLayout();
top_row->addWidget(new QLabel(tr("Directions :"), this));
top_row->addWidget(m_directions);
top_row->addSpacing(12);
top_row->addWidget(new QLabel(tr("Contexte :"), this));
top_row->addWidget(m_context, 1);
auto *state_row = new QHBoxLayout();
state_row->addWidget(m_state, 1);
state_row->addWidget(follow);
auto *layout = new QVBoxLayout(this);
layout->addWidget(explanation);
layout->addLayout(top_row);
layout->addLayout(state_row);
layout->addLayout(grid);
connect(m_directions, qOverload<int>(&QComboBox::currentIndexChanged), this, [this]() {
m_ring->setDirections(m_directions->currentData().toInt());
updateState();
});
connect(m_context, qOverload<int>(&QComboBox::currentIndexChanged),
this, &GesturesConfigPage::showContext);
connect(m_ring, &GestureRingEditor::edited, this, [this]() {
m_pending.insert(m_shown, m_ring->ids());
m_custom.insert(m_shown, true);
updateState();
});
connect(m_ring, &GestureRingEditor::currentSlotChanged,
this, &GesturesConfigPage::updateState);
connect(place, &QPushButton::clicked, this, &GesturesConfigPage::placeSelected);
connect(m_available, &QListWidget::itemDoubleClicked,
this, &GesturesConfigPage::placeSelected);
connect(clear, &QPushButton::clicked, m_ring, &GestureRingEditor::clearCurrent);
connect(follow, &QPushButton::clicked, this, &GesturesConfigPage::followBar);
showContext();
}
/**
@brief GesturesConfigPage::applyConf
Save the directions and every context's ring.
*/
void GesturesConfigPage::applyConf()
{
GestureSettings::setDirections(m_directions->currentData().toInt());
for (const ShortcutBarSettings::Context c : ShortcutBarSettings::contexts()) {
if (m_custom.value(c)) {
GestureSettings::setIds(c, m_pending.value(c));
} else {
GestureSettings::reset(c);
}
}
}
QString GesturesConfigPage::title() const
{
return tr("Gestes de la souris", "configuration page title");
}
QIcon GesturesConfigPage::icon() const
{
return QET::Icons::ConfigureShortcuts;
}
void GesturesConfigPage::showContext()
{
m_shown = static_cast<ShortcutBarSettings::Context>(
m_context->currentData().toInt());
m_ring->setIds(m_pending.value(m_shown));
updateState();
}
void GesturesConfigPage::placeSelected()
{
if (QListWidgetItem *item = m_available->currentItem()) {
m_ring->place(item->data(Qt::UserRole).toString());
}
}
/**
@brief GesturesConfigPage::followBar
Make the shown context's ring follow the shortcut bar again.
*/
void GesturesConfigPage::followBar()
{
m_custom.insert(m_shown, false);
m_pending.insert(m_shown, GestureSettings::defaultIds(m_shown));
m_ring->setIds(m_pending.value(m_shown));
updateState();
}
/**
@brief GesturesConfigPage::updateState
Name the chosen direction's command, and say whether the shown ring is
the user's or the shortcut bar's.
*/
void GesturesConfigPage::updateState()
{
const int slot = m_ring->currentSlot();
const QStringList ids = m_ring->ids();
const QString id = slot < ids.count() ? ids.at(slot) : QString();
m_slot_name->setText(id.isEmpty() ? tr("(direction vide)")
: m_descriptions.value(id, id));
m_state->setText(m_custom.value(m_shown)
? tr("Anneau personnalisé pour ce contexte.")
: tr("Cet anneau suit la barre de raccourcis."));
}
+112
View File
@@ -0,0 +1,112 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#ifndef GESTURESCONFIGPAGE_H
#define GESTURESCONFIGPAGE_H
#include "configpage.h"
#include "../../shortcutbarsettings.h"
#include <QHash>
#include <QStringList>
class QComboBox;
class QLabel;
class QListWidget;
/**
@brief The gesture ring as an editor: one slot per direction, clockwise
from the top. Click a slot to choose it; drop a command from the
command list on a slot to put it there; Delete empties the chosen slot.
*/
class GestureRingEditor : public QWidget
{
Q_OBJECT
public:
explicit GestureRingEditor(QWidget *parent = nullptr);
void setIds(const QStringList &ids);
QStringList ids() const { return m_ids; }
void setDirections(int directions);
void setDescriptions(const QHash<QString, QString> &descriptions);
int currentSlot() const { return m_current; }
void setCurrentSlot(int slot);
int slotAt(const QPoint &pos) const;
void place(const QString &id);
void clearCurrent();
QSize sizeHint() const override;
signals:
void edited();
void currentSlotChanged(int slot);
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
void dragEnterEvent(QDragEnterEvent *event) override;
void dragMoveEvent(QDragMoveEvent *event) override;
void dropEvent(QDropEvent *event) override;
private:
QString idAt(int slot) const;
QPointF slotCenter(int slot) const;
QStringList m_ids;
QHash<QString, QString> m_descriptions;
int m_directions = 8;
int m_current = 0;
};
/**
@brief The GesturesConfigPage class
Choose 4 or 8 directions for the mouse gesture ring, and which command
sits in each direction for each selection context. A context the user
does not edit keeps following the shortcut bar. Saved by applyConf().
*/
class GesturesConfigPage : public ConfigPage
{
Q_OBJECT
public:
explicit GesturesConfigPage(QWidget *parent = nullptr);
void applyConf() override;
QString title() const override;
QIcon icon() const override;
private:
void showContext();
void placeSelected();
void followBar();
void updateState();
QComboBox *m_directions;
QComboBox *m_context;
QListWidget *m_available;
GestureRingEditor *m_ring;
QLabel *m_slot_name;
QLabel *m_state;
ShortcutBarSettings::Context m_shown = ShortcutBarSettings::Canvas;
QHash<int, QStringList> m_pending;
QHash<int, bool> m_custom;
QHash<QString, QString> m_descriptions;
};
#endif // GESTURESCONFIGPAGE_H
@@ -0,0 +1,423 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#include "toolbarcommandsconfigpage.h"
#include "../../qeticons.h"
#include "../../shortcutmanager.h"
#include <QAction>
#include <QComboBox>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QPushButton>
#include <QTimer>
#include <QVBoxLayout>
ToolbarCommandsConfigPage::ToolbarCommandsConfigPage(QWidget *parent) :
ConfigPage(parent)
{
const QStringList available = DiagramToolbarSettings::availableCommandIds();
for (const ShortcutManager::ShortcutInfo &info :
ShortcutManager::instance().allShortcuts()) {
if (available.contains(info.id)) {
m_descriptions.insert(info.id, info.description);
}
}
m_custom = DiagramToolbarSettings::customToolbars();
for (const DiagramToolbarSettings::Toolbar &toolbar : DiagramToolbarSettings::toolbars()) {
m_pending.insert(toolbar.name, DiagramToolbarSettings::ids(toolbar.name));
}
auto *explanation = new QLabel(
tr("Choisissez les commandes de chaque barre d'outils de l'éditeur "
"de schémas. Glissez une commande de la liste de gauche vers la "
"barre, ou double-cliquez dessus ; Suppr la retire."), this);
explanation->setWordWrap(true);
m_toolbar = new QComboBox(this);
m_toolbar->setObjectName(QStringLiteral("toolbarCombo"));
auto *new_toolbar = new QPushButton(tr("Nouvelle barre…"), this);
new_toolbar->setObjectName(QStringLiteral("newToolbarButton"));
m_rename = new QPushButton(tr("Renommer…"), this);
m_delete = new QPushButton(tr("Supprimer la barre"), this);
m_delete->setObjectName(QStringLiteral("deleteToolbarButton"));
auto *reset = new QPushButton(tr("Valeurs par défaut"), this);
reset->setObjectName(QStringLiteral("resetButton"));
m_filter = new QLineEdit(this);
m_filter->setPlaceholderText(tr("Rechercher une commande"));
m_filter->setClearButtonEnabled(true);
m_available = new QListWidget(this);
m_available->setObjectName(QStringLiteral("availableList"));
m_available->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_available->setSortingEnabled(true);
m_available->setDragDropMode(QAbstractItemView::DragOnly);
m_available->setDefaultDropAction(Qt::MoveAction);
m_chosen = new QListWidget(this);
m_chosen->setObjectName(QStringLiteral("chosenList"));
m_chosen->setSelectionMode(QAbstractItemView::ExtendedSelection);
//Takes commands dragged from the list on the left, and reorders
m_chosen->setDragDropMode(QAbstractItemView::DragDrop);
m_chosen->setDefaultDropAction(Qt::MoveAction);
auto *add = new QPushButton(tr("Ajouter →"), this);
auto *separator = new QPushButton(tr("Séparateur"), this);
separator->setObjectName(QStringLiteral("separatorButton"));
auto *remove = new QPushButton(tr("← Retirer"), this);
auto *up = new QPushButton(tr("Monter"), this);
auto *down = new QPushButton(tr("Descendre"), this);
auto *buttons = new QVBoxLayout();
buttons->addStretch();
buttons->addWidget(add);
buttons->addWidget(separator);
buttons->addWidget(remove);
buttons->addSpacing(12);
buttons->addWidget(up);
buttons->addWidget(down);
buttons->addStretch();
auto *grid = new QGridLayout();
grid->addWidget(new QLabel(tr("Commandes disponibles"), this), 0, 0);
grid->addWidget(new QLabel(tr("Dans la barre, dans l'ordre"), this), 0, 2);
grid->addWidget(m_filter, 1, 0);
grid->addWidget(m_available, 2, 0);
grid->addLayout(buttons, 2, 1);
grid->addWidget(m_chosen, 1, 2, 2, 1);
auto *toolbar_row = new QHBoxLayout();
toolbar_row->addWidget(new QLabel(tr("Barre d'outils :"), this));
toolbar_row->addWidget(m_toolbar, 1);
toolbar_row->addWidget(new_toolbar);
toolbar_row->addWidget(m_rename);
toolbar_row->addWidget(m_delete);
toolbar_row->addWidget(reset);
auto *layout = new QVBoxLayout(this);
layout->addWidget(explanation);
layout->addLayout(toolbar_row);
layout->addLayout(grid);
connect(m_toolbar, qOverload<int>(&QComboBox::currentIndexChanged), this, [this]() {
storeToolbar();
showToolbar();
});
connect(m_filter, &QLineEdit::textChanged, this, &ToolbarCommandsConfigPage::fillAvailable);
connect(add, &QPushButton::clicked, this, &ToolbarCommandsConfigPage::addSelected);
connect(separator, &QPushButton::clicked, this, &ToolbarCommandsConfigPage::addSeparator);
connect(remove, &QPushButton::clicked, this, &ToolbarCommandsConfigPage::removeSelected);
connect(up, &QPushButton::clicked, this, [this]() { moveSelected(-1); });
connect(down, &QPushButton::clicked, this, [this]() { moveSelected(1); });
connect(reset, &QPushButton::clicked, this, &ToolbarCommandsConfigPage::resetToolbar);
connect(new_toolbar, &QPushButton::clicked, this, &ToolbarCommandsConfigPage::newToolbar);
connect(m_rename, &QPushButton::clicked, this, &ToolbarCommandsConfigPage::renameToolbar);
connect(m_delete, &QPushButton::clicked, this, &ToolbarCommandsConfigPage::deleteToolbar);
connect(m_available, &QListWidget::itemDoubleClicked, this, &ToolbarCommandsConfigPage::addSelected);
connect(m_chosen, &QListWidget::itemDoubleClicked, this, &ToolbarCommandsConfigPage::removeSelected);
//A command dropped from the left is no longer available. Later,
//not now: a dropped row is inserted before its data is set.
connect(m_chosen->model(), &QAbstractItemModel::rowsInserted, this, [this]() {
if (m_chosen->property("filling").toBool()) return;
QTimer::singleShot(0, this, [this]() {
storeToolbar();
fillAvailable();
});
});
auto *delete_key = new QAction(m_chosen);
delete_key->setShortcut(QKeySequence::Delete);
delete_key->setShortcutContext(Qt::WidgetShortcut);
m_chosen->addAction(delete_key);
connect(delete_key, &QAction::triggered, this, &ToolbarCommandsConfigPage::removeSelected);
fillToolbarCombo(DiagramToolbarSettings::builtInNames().constFirst());
}
/**
@brief ToolbarCommandsConfigPage::applyConf
Save the user's own toolbars and every toolbar's contents, then
rebuild the toolbars of the open diagram editors.
*/
void ToolbarCommandsConfigPage::applyConf()
{
storeToolbar();
DiagramToolbarSettings::setCustomToolbars(m_custom);
for (const DiagramToolbarSettings::Toolbar &toolbar : DiagramToolbarSettings::toolbars()) {
DiagramToolbarSettings::setIds(toolbar.name, m_pending.value(toolbar.name));
}
DiagramToolbarSettings::applyToAll();
}
QString ToolbarCommandsConfigPage::title() const
{
return tr("Contenu des barres", "configuration page title: what each toolbar holds");
}
QIcon ToolbarCommandsConfigPage::icon() const
{
return QET::Icons::ConfigureToolbars;
}
/**
@brief ToolbarCommandsConfigPage::fillToolbarCombo
List the built-in toolbars and the user's own, and show @a current.
*/
void ToolbarCommandsConfigPage::fillToolbarCombo(const QString &current)
{
const QSignalBlocker blocker(m_toolbar);
m_toolbar->clear();
for (const QString &name : DiagramToolbarSettings::builtInNames()) {
m_toolbar->addItem(DiagramToolbarSettings::builtInTitle(name), name);
}
for (const DiagramToolbarSettings::Toolbar &toolbar : std::as_const(m_custom)) {
m_toolbar->addItem(toolbar.title, toolbar.name);
}
const int index = m_toolbar->findData(current);
m_toolbar->setCurrentIndex(index < 0 ? 0 : index);
showToolbar();
}
/**
@brief ToolbarCommandsConfigPage::showToolbar
Fill both lists for the toolbar chosen in the combo box.
*/
void ToolbarCommandsConfigPage::showToolbar()
{
m_shown = m_toolbar->currentData().toString();
const bool custom = !DiagramToolbarSettings::builtInNames().contains(m_shown);
m_rename->setEnabled(custom);
m_delete->setEnabled(custom);
m_chosen->setProperty("filling", true);
m_chosen->clear();
for (const QString &id : m_pending.value(m_shown)) {
m_chosen->addItem(makeItem(id));
}
m_chosen->setProperty("filling", false);
fillAvailable();
}
/**
@brief ToolbarCommandsConfigPage::storeToolbar
Keep the shown toolbar's list, in the order on screen.
*/
void ToolbarCommandsConfigPage::storeToolbar()
{
if (m_shown.isEmpty()) {
return;
}
QStringList ids;
for (int i = 0 ; i < m_chosen->count() ; ++i) {
ids << m_chosen->item(i)->data(Qt::UserRole).toString();
}
m_pending.insert(m_shown, ids);
}
/**
@brief ToolbarCommandsConfigPage::fillAvailable
List what can still go on the shown toolbar and matches the search: a
command not already on it, and a widget button on no toolbar at all
(a widget can only be in one place).
*/
void ToolbarCommandsConfigPage::fillAvailable()
{
QStringList on_shown;
for (int i = 0 ; i < m_chosen->count() ; ++i) {
on_shown << m_chosen->item(i)->data(Qt::UserRole).toString();
}
QStringList on_any = on_shown;
for (auto it = m_pending.cbegin() ; it != m_pending.cend() ; ++it) {
if (it.key() != m_shown) on_any << it.value();
}
QStringList ids;
for (auto it = m_descriptions.cbegin() ; it != m_descriptions.cend() ; ++it) {
if (!on_shown.contains(it.key())) ids << it.key();
}
for (const QString &id : DiagramToolbarSettings::widgetIds()) {
if (!on_any.contains(id)) ids << id;
}
m_available->clear();
const QString filter = m_filter->text().trimmed();
for (const QString &id : std::as_const(ids)) {
QListWidgetItem *item = makeItem(id);
if (filter.isEmpty() || item->text().contains(filter, Qt::CaseInsensitive)) {
m_available->addItem(item);
} else {
delete item;
}
}
}
void ToolbarCommandsConfigPage::addSelected()
{
const QList<QListWidgetItem *> selected = m_available->selectedItems();
if (selected.isEmpty()) {
return;
}
m_chosen->setProperty("filling", true);
for (QListWidgetItem *item : selected) {
m_chosen->addItem(m_available->takeItem(m_available->row(item)));
}
m_chosen->setProperty("filling", false);
storeToolbar();
fillAvailable();
}
/**
@brief ToolbarCommandsConfigPage::addSeparator
Insert a separator after the selected row, or at the end.
*/
void ToolbarCommandsConfigPage::addSeparator()
{
const int row = m_chosen->currentRow();
m_chosen->setProperty("filling", true);
m_chosen->insertItem(row < 0 ? m_chosen->count() : row + 1,
makeItem(DiagramToolbarSettings::separatorId()));
m_chosen->setProperty("filling", false);
storeToolbar();
}
void ToolbarCommandsConfigPage::removeSelected()
{
for (QListWidgetItem *item : m_chosen->selectedItems()) {
delete m_chosen->takeItem(m_chosen->row(item));
}
storeToolbar();
fillAvailable();
}
/**
@brief ToolbarCommandsConfigPage::moveSelected
Move the selected command @a step rows, keeping it selected.
*/
void ToolbarCommandsConfigPage::moveSelected(int step)
{
const int row = m_chosen->currentRow();
const int target = row + step;
if (row < 0 || target < 0 || target >= m_chosen->count()) {
return;
}
m_chosen->setProperty("filling", true);
QListWidgetItem *item = m_chosen->takeItem(row);
m_chosen->insertItem(target, item);
m_chosen->setProperty("filling", false);
m_chosen->setCurrentRow(target);
storeToolbar();
}
/**
@brief ToolbarCommandsConfigPage::resetToolbar
Give the shown toolbar its defaults back; the user's own toolbar is
emptied.
*/
void ToolbarCommandsConfigPage::resetToolbar()
{
m_pending.insert(m_shown, DiagramToolbarSettings::defaultIds(m_shown));
//A widget the defaults put here leaves the toolbar it was moved to
for (const QString &id : DiagramToolbarSettings::defaultIds(m_shown)) {
if (!DiagramToolbarSettings::isWidget(id)) continue;
for (auto it = m_pending.begin() ; it != m_pending.end() ; ++it) {
if (it.key() != m_shown) it.value().removeAll(id);
}
}
showToolbar();
}
void ToolbarCommandsConfigPage::newToolbar()
{
bool ok = false;
const QString title = QInputDialog::getText(this, tr("Nouvelle barre d'outils"),
tr("Nom de la barre d'outils :"),
QLineEdit::Normal, QString(), &ok).trimmed();
if (!ok || title.isEmpty()) {
return;
}
storeToolbar();
DiagramToolbarSettings::Toolbar toolbar;
toolbar.name = DiagramToolbarSettings::newCustomName(m_custom);
toolbar.title = title;
toolbar.custom = true;
m_custom << toolbar;
m_pending.insert(toolbar.name, QStringList());
fillToolbarCombo(toolbar.name);
}
void ToolbarCommandsConfigPage::renameToolbar()
{
for (DiagramToolbarSettings::Toolbar &toolbar : m_custom) {
if (toolbar.name != m_shown) continue;
bool ok = false;
const QString title = QInputDialog::getText(this, tr("Renommer la barre d'outils"),
tr("Nom de la barre d'outils :"),
QLineEdit::Normal, toolbar.title, &ok).trimmed();
if (ok && !title.isEmpty()) {
storeToolbar();
toolbar.title = title;
fillToolbarCombo(m_shown);
}
return;
}
}
void ToolbarCommandsConfigPage::deleteToolbar()
{
const QString name = m_shown;
for (int i = 0 ; i < m_custom.size() ; ++i) {
if (m_custom.at(i).name == name) {
m_custom.removeAt(i);
m_pending.remove(name);
m_shown.clear();
fillToolbarCombo(DiagramToolbarSettings::builtInNames().constFirst());
return;
}
}
}
/**
@brief ToolbarCommandsConfigPage::makeItem
@return a list item for @a id with the command's text and icon. An id
no live action carries (a command from a build without it, or a script
since removed) is still listed, by its id, so saving does not silently
drop it.
*/
QListWidgetItem *ToolbarCommandsConfigPage::makeItem(const QString &id) const
{
QString text = m_descriptions.value(id, id);
QString tip;
QIcon icon;
if (id == DiagramToolbarSettings::separatorId()) {
text = tr("─── Séparateur ───");
} else if (DiagramToolbarSettings::isWidget(id)) {
text = DiagramToolbarSettings::widgetTitle(id);
} else if (QAction *action = ShortcutManager::instance().action(id, nullptr)) {
icon = action->icon();
tip = action->statusTip();
}
auto *item = new QListWidgetItem(icon, text);
item->setData(Qt::UserRole, id);
//Tells apart commands with the same name ("Ajouter une ligne")
item->setToolTip(tip);
return item;
}
@@ -0,0 +1,78 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#ifndef TOOLBARCOMMANDSCONFIGPAGE_H
#define TOOLBARCOMMANDSCONFIGPAGE_H
#include "configpage.h"
#include "../../diagramtoolbarsettings.h"
#include <QHash>
class QComboBox;
class QLineEdit;
class QListWidget;
class QListWidgetItem;
class QPushButton;
/**
@brief The ToolbarCommandsConfigPage class
Choose what each of the diagram editor's toolbars holds, and add,
rename or delete toolbars of one's own. Commands are dragged or added
from the list on the left. Changes are kept per toolbar while the
dialog is open, then saved and applied to the open windows by
applyConf().
*/
class ToolbarCommandsConfigPage : public ConfigPage
{
Q_OBJECT
public:
explicit ToolbarCommandsConfigPage(QWidget *parent = nullptr);
void applyConf() override;
QString title() const override;
QIcon icon() const override;
private:
void fillToolbarCombo(const QString &current);
void showToolbar();
void storeToolbar();
void fillAvailable();
void addSelected();
void addSeparator();
void removeSelected();
void moveSelected(int step);
void resetToolbar();
void newToolbar();
void renameToolbar();
void deleteToolbar();
QListWidgetItem *makeItem(const QString &id) const;
QComboBox *m_toolbar;
QLineEdit *m_filter;
QListWidget *m_available;
QListWidget *m_chosen;
QPushButton *m_rename;
QPushButton *m_delete;
QString m_shown;
QList<DiagramToolbarSettings::Toolbar> m_custom;
QHash<QString, QStringList> m_pending;
QHash<QString, QString> m_descriptions;
};
#endif // TOOLBARCOMMANDSCONFIGPAGE_H
@@ -0,0 +1,88 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#include "toolbarsconfigpage.h"
#include "../../qeticons.h"
#include "../../toolbarsettings.h"
#include <QCheckBox>
#include <QComboBox>
#include <QFormLayout>
#include <QFrame>
#include <QLabel>
#include <QVBoxLayout>
ToolbarsConfigPage::ToolbarsConfigPage(QWidget *parent) :
ConfigPage(parent)
{
auto *vlayout = new QVBoxLayout(this);
vlayout->addWidget(new QLabel(title(), this));
auto *line = new QFrame(this);
line->setFrameShape(QFrame::HLine);
vlayout->addWidget(line);
m_icon_size = new QComboBox(this);
m_icon_size->setObjectName(QStringLiteral("iconSizeCombo"));
m_icon_size->addItem(tr("Par défaut"), 0);
m_icon_size->addItem(tr("Petites (16 px)"), 16);
m_icon_size->addItem(tr("Moyennes (24 px)"), 24);
m_icon_size->addItem(tr("Grandes (32 px)"), 32);
m_icon_size->addItem(tr("Très grandes (48 px)"), 48);
const int size_index = m_icon_size->findData(ToolbarSettings::iconSize());
m_icon_size->setCurrentIndex(size_index < 0 ? 0 : size_index);
m_button_style = new QComboBox(this);
m_button_style->setObjectName(QStringLiteral("buttonStyleCombo"));
m_button_style->addItem(tr("Icône seule"), int(Qt::ToolButtonIconOnly));
m_button_style->addItem(tr("Texte à côté de l'icône"), int(Qt::ToolButtonTextBesideIcon));
m_button_style->addItem(tr("Texte sous l'icône"), int(Qt::ToolButtonTextUnderIcon));
m_button_style->setCurrentIndex(m_button_style->findData(int(ToolbarSettings::buttonStyle())));
m_locked = new QCheckBox(tr("Verrouiller les barres d'outils (elles ne peuvent plus être déplacées)"), this);
m_locked->setObjectName(QStringLiteral("lockedCheck"));
m_locked->setChecked(ToolbarSettings::locked());
auto *form = new QFormLayout();
form->addRow(tr("Taille des icônes :"), m_icon_size);
form->addRow(tr("Boutons :"), m_button_style);
form->addRow(m_locked);
vlayout->addLayout(form);
auto *hint = new QLabel(tr("Pour afficher ou masquer une barre d'outils, faites un clic droit sur une barre d'outils."), this);
hint->setWordWrap(true);
vlayout->addWidget(hint);
vlayout->addStretch();
}
void ToolbarsConfigPage::applyConf()
{
ToolbarSettings::save(m_icon_size->currentData().toInt(),
Qt::ToolButtonStyle(m_button_style->currentData().toInt()),
m_locked->isChecked());
ToolbarSettings::applyToAll();
}
QString ToolbarsConfigPage::title() const
{
return tr("Barres d'outils", "configuration page title");
}
QIcon ToolbarsConfigPage::icon() const
{
return QET::Icons::ConfigureToolbars;
}
@@ -0,0 +1,49 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#ifndef TOOLBARSCONFIGPAGE_H
#define TOOLBARSCONFIGPAGE_H
#include "configpage.h"
class QCheckBox;
class QComboBox;
/**
@brief The ToolbarsConfigPage class
Toolbar icon size, text with the icons, and locking the toolbars in
place, for every window. Saved and applied to the open windows by
applyConf().
*/
class ToolbarsConfigPage : public ConfigPage
{
Q_OBJECT
public:
explicit ToolbarsConfigPage(QWidget *parent = nullptr);
void applyConf() override;
QString title() const override;
QIcon icon() const override;
private:
QComboBox *m_icon_size;
QComboBox *m_button_style;
QCheckBox *m_locked;
};
#endif // TOOLBARSCONFIGPAGE_H
+77
View File
@@ -0,0 +1,77 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#include "customizedialog.h"
#include "configpage/configpage.h"
#include <QDialogButtonBox>
#include <QIcon>
#include <QTabWidget>
#include <QVBoxLayout>
CustomizeDialog::CustomizeDialog(QWidget *parent) :
QDialog(parent)
{
setWindowTitle(tr("Personnaliser", "window title"));
m_tabs = new QTabWidget(this);
m_tabs->setObjectName(QStringLiteral("customizeTabs"));
m_tabs->setDocumentMode(true);
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(buttons, &QDialogButtonBox::accepted, this, [this]() {
applyConf();
accept();
});
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
auto *layout = new QVBoxLayout(this);
layout->addWidget(m_tabs);
layout->addWidget(buttons);
resize(1000, 700);
}
/**
@brief CustomizeDialog::addPage
Add @a page as a tab, named after its title. The dialog takes it.
*/
void CustomizeDialog::addPage(ConfigPage *page)
{
if (!page || m_pages.contains(page)) {
return;
}
m_pages << page;
m_tabs->addTab(page, page->icon(), page->title());
}
QList<ConfigPage *> CustomizeDialog::pages() const
{
return m_pages;
}
/**
@brief CustomizeDialog::applyConf
Apply every page, in tab order.
*/
void CustomizeDialog::applyConf()
{
for (ConfigPage *page : std::as_const(m_pages)) {
page->applyConf();
}
}
+51
View File
@@ -0,0 +1,51 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#ifndef CUSTOMIZEDIALOG_H
#define CUSTOMIZEDIALOG_H
#include <QDialog>
#include <QList>
class ConfigPage;
class QTabWidget;
/**
@brief The CustomizeDialog class
Everything about how the user works in one window, one tab per
configuration page: toolbars, what they hold, the shortcut bar, the
keyboard and the mouse gestures, like SolidWorks' Tools > Customize.
The pages are the ones of the configuration dialog, which still shows
them too. OK applies every page, Cancel none.
*/
class CustomizeDialog : public QDialog
{
Q_OBJECT
public:
explicit CustomizeDialog(QWidget *parent = nullptr);
void addPage(ConfigPage *page);
QList<ConfigPage *> pages() const;
void applyConf();
private:
QTabWidget *m_tabs;
QList<ConfigPage *> m_pages;
};
#endif // CUSTOMIZEDIALOG_H
+1 -1
View File
@@ -85,7 +85,7 @@ DiagramBgColorToolButton::DiagramBgColorToolButton(QETDiagramEditor *editor, QWi
m_recent.removeLast();
}
setMenu(new QMenu(this));
setMenu(new QMenu(tr("Couleur de fond du folio"), this));
rebuildMenu();
setSwatch(m_current);
}
+4
View File
@@ -65,6 +65,10 @@ DiagramPropertiesDialog::DiagramPropertiesDialog(Diagram *diagram, QWidget *pare
titleblock_infos -> setReadOnly(diagram_is_read_only);
connect(titleblock_infos, &TitleBlockPropertiesWidget::openAutoNumFolioEditor, this, &DiagramPropertiesDialog::editAutoFolioNum);
border_infos -> setTitleBlockSize(titleblock_infos -> currentTitleBlockHeight(),
titleblock_infos -> currentTitleBlockEdge());
connect(titleblock_infos, &TitleBlockPropertiesWidget::titleBlockSizeChanged,
border_infos, &BorderPropertiesWidget::setTitleBlockSize);
//titleblock_infos->setMinimumSize(590,480); //Minimum Size needed for correct display
//Conductor widget
+49 -4
View File
@@ -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;
}
+5
View File
@@ -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
+11 -1
View File
@@ -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>
+47
View File
@@ -27,6 +27,17 @@
#include <QLineEdit>
#include <QtGlobal>
/**
@return width as shown in the width spin box: whole pixels, 0 for the
automatic width ("Auto"). The box starts at 0, so no number means Auto
and the first step up is 1 px; IndependentTextItem::setTextWidth()
treats 0 as -1, the automatic width, so the two never disagree.
*/
static int spinBoxWidth(qreal width)
{
return width > 0 ? qRound(width) : 0;
}
/**
@brief IndiTextPropertiesWidget::IndiTextPropertiesWidget
@param text : the text to edit
@@ -84,6 +95,7 @@ void IndiTextPropertiesWidget::setText(IndependentTextItem *text)
m_connect_list << connect(m_text.data(), &IndependentTextItem::rotationChanged, this, &IndiTextPropertiesWidget::updateUi);
m_connect_list << connect(m_text.data(), &IndependentTextItem::fontChanged, this, &IndiTextPropertiesWidget::updateUi);
m_connect_list << connect(m_text.data(), &IndependentTextItem::textEdited, this, &IndiTextPropertiesWidget::updateUi);
m_connect_list << connect(m_text.data(), &IndependentTextItem::textWidthChanged, this, &IndiTextPropertiesWidget::updateUi);
updateUi();
}
@@ -209,6 +221,10 @@ QUndoCommand *IndiTextPropertiesWidget::associatedUndo() const
undo = new QPropertyUndoCommand(m_text.data(), "font", m_text->font(), m_selected_font);
undo->setText(tr("Modifier la police d'un champ texte"));
}
if (ui->m_width_sb->value() != spinBoxWidth(m_text->textWidth())) {
undo = new QPropertyUndoCommand(m_text.data(), "textWidth", m_text->textWidth(), qreal(ui->m_width_sb->value()));
undo->setText(tr("Modifier la largeur d'un champ texte"));
}
return undo;
}
@@ -218,9 +234,11 @@ QUndoCommand *IndiTextPropertiesWidget::associatedUndo() const
bool size_equal = true;
bool angle_equal = true;
bool font_equal = true;
bool width_equal = true;
qreal rotation_ = m_text_list.first()->rotation();
int size_ = m_text_list.first()->font().pointSize();
QFont font_ = m_text_list.first()->font();
int width_ = spinBoxWidth(m_text_list.first()->textWidth());
for (QPointer<IndependentTextItem> piti : m_text_list)
{
if (piti->rotation() != rotation_) {
@@ -232,6 +250,9 @@ QUndoCommand *IndiTextPropertiesWidget::associatedUndo() const
if (piti->font() != font_) {
font_equal = false;
}
if (spinBoxWidth(piti->textWidth()) != width_) {
width_equal = false;
}
}
if ((angle_equal && (ui->m_angle_sb->value() != rotation_)) ||
@@ -279,6 +300,20 @@ QUndoCommand *IndiTextPropertiesWidget::associatedUndo() const
}
}
}
else if ((width_equal && (ui->m_width_sb->value() != width_)) ||
(!width_equal && (ui->m_width_sb->value() != ui->m_width_sb->minimum())))
{
for (QPointer<IndependentTextItem> piti : m_text_list)
{
if (piti)
{
if (!parent_undo) {
parent_undo = new QUndoCommand(tr("Modifier la largeur de plusieurs champs texte"));
}
new QPropertyUndoCommand(piti.data(), "textWidth", piti->textWidth(), qreal(ui->m_width_sb->value()), parent_undo);
}
}
}
return parent_undo;
}
}
@@ -307,6 +342,9 @@ QUndoCommand *IndiTextPropertiesWidget::associatedUndo() const
if (m_font_is_selected && m_selected_font != m_text->font()) {
new QPropertyUndoCommand(m_text.data(), "font", m_text->font(), m_selected_font, undo);
}
if (ui->m_width_sb->value() != spinBoxWidth(m_text->textWidth())) {
new QPropertyUndoCommand(m_text.data(), "textWidth", m_text->textWidth(), qreal(ui->m_width_sb->value()), undo);
}
if (undo->childCount()) {
return undo;
@@ -337,6 +375,7 @@ void IndiTextPropertiesWidget::setUpEditConnection()
m_edit_connection << connect(ui->m_line_edit, &QLineEdit::textEdited, this, &IndiTextPropertiesWidget::apply);
}
m_edit_connection << connect(ui->m_angle_sb, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &IndiTextPropertiesWidget::apply);
m_edit_connection << connect(ui->m_width_sb, QOverload<int>::of(&QSpinBox::valueChanged), this, &IndiTextPropertiesWidget::apply);
m_edit_connection << connect(ui->m_size_sb, QOverload<int>::of(&QSpinBox::valueChanged), [this]()
{
this->m_selected_font.setPointSize(ui->m_size_sb->value());
@@ -372,6 +411,7 @@ void IndiTextPropertiesWidget::updateUi()
ui->m_line_edit->setText(m_text->toPlainText());
ui->m_angle_sb->setValue(m_text->rotation());
ui->m_size_sb->setValue(m_text->font().pointSize());
ui->m_width_sb->setValue(spinBoxWidth(m_text->textWidth()));
ui->m_line_edit->setDisabled(m_text->isHtml() ? true : false);
ui->m_size_sb->setDisabled(m_text->isHtml() ? true : false);
@@ -385,9 +425,11 @@ void IndiTextPropertiesWidget::updateUi()
bool size_equal = true;
bool angle_equal = true;
bool font_equal = true;
bool width_equal = true;
qreal rotation_ = m_text_list.first()->rotation();
int size_ = m_text_list.first()->font().pointSize();
QFont font_ = m_text_list.first()->font();
int width_ = spinBoxWidth(m_text_list.first()->textWidth());
for (QPointer<IndependentTextItem> piti : m_text_list)
{
@@ -400,8 +442,13 @@ void IndiTextPropertiesWidget::updateUi()
if (piti->font() != font_) {
font_equal = false;
}
if (spinBoxWidth(piti->textWidth()) != width_) {
width_equal = false;
}
}
ui->m_angle_sb->setValue(angle_equal ? rotation_ : 0);
//Different widths show the minimum, as the other boxes do
ui->m_width_sb->setValue(width_equal ? width_ : ui->m_width_sb->minimum());
bool valid_ = true;
for (QPointer<IndependentTextItem> piti : m_text_list) {
+36 -3
View File
@@ -64,7 +64,7 @@
</property>
</widget>
</item>
<item row="6" column="0">
<item row="7" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -126,7 +126,39 @@
</property>
</widget>
</item>
<item row="4" column="0" colspan="4">
<item row="4" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Largeur :</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="m_width_sb">
<property name="toolTip">
<string>Largeur du texte, le texte passe à la ligne. Auto : pas de retour à la ligne automatique</string>
</property>
<property name="specialValueText">
<string>Auto</string>
</property>
<property name="suffix">
<string>px</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item row="5" column="0" colspan="4">
<widget class="QLabel" name="m_label">
<property name="text">
<string>Le contenu, la taille et la police du texte ne peuvent être modifié car formaté en html.
@@ -140,7 +172,7 @@ Veuillez utiliser l'éditeur avancé pour cela.</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="4">
<item row="6" column="0" colspan="4">
<widget class="QPushButton" name="m_break_html_pb">
<property name="text">
<string>Cliquez ici pour annuler le formatage html</string>
@@ -171,6 +203,7 @@ Veuillez utiliser l'éditeur avancé pour cela.</string>
<tabstop>m_y_sb</tabstop>
<tabstop>m_angle_sb</tabstop>
<tabstop>m_size_sb</tabstop>
<tabstop>m_width_sb</tabstop>
<tabstop>m_break_html_pb</tabstop>
</tabstops>
<resources/>
+33
View File
@@ -336,6 +336,30 @@ QString TitleBlockPropertiesWidget::currentTitleBlockTemplateName() const
return QString();
}
/**
@brief TitleBlockPropertiesWidget::currentTitleBlockHeight
@return the height of the current title block template
*/
int TitleBlockPropertiesWidget::currentTitleBlockHeight() const
{
const TitleBlockTemplate *tbt = nullptr;
if (!currentTitleBlockTemplateName().isEmpty())
tbt = templateForIndex(ui -> m_tbt_cb -> currentIndex());
if (!tbt)
tbt = QETApp::defaultTitleBlockTemplate();
return tbt ? tbt -> height() : 0;
}
/**
@brief TitleBlockPropertiesWidget::currentTitleBlockEdge
@return the edge of the folio the title block is displayed at
*/
Qt::Edge TitleBlockPropertiesWidget::currentTitleBlockEdge() const
{
return ui -> m_display_at_cb -> currentIndex() == 0 ? Qt::BottomEdge
: Qt::RightEdge;
}
/**
@brief TitleBlockPropertiesWidget::addCollection
add a collection of title block available in the combo box
@@ -388,6 +412,15 @@ void TitleBlockPropertiesWidget::initDialog(
this,
&TitleBlockPropertiesWidget::changeCurrentTitleBlockTemplate);
auto size_changed = [this]() {
emit titleBlockSizeChanged(currentTitleBlockHeight(),
currentTitleBlockEdge());
};
connect(ui->m_tbt_cb, qOverload<int>(&QComboBox::currentIndexChanged),
this, size_changed);
connect(ui->m_display_at_cb, qOverload<int>(&QComboBox::currentIndexChanged),
this, size_changed);
if (project!= nullptr){
keys_2 = project -> folioAutoNum().keys();
foreach (QString str, keys_2) { ui -> auto_page_cb -> addItem(str); }
+3
View File
@@ -70,6 +70,8 @@ class TitleBlockPropertiesWidget : public QWidget
void setTitleBlockTemplatesVisible(const bool &visible);
void setReadOnly (const bool &ro);
int currentTitleBlockHeight() const;
Qt::Edge currentTitleBlockEdge() const;
static void addTemplateVariables(
DiagramContext &context,
@@ -96,6 +98,7 @@ class TitleBlockPropertiesWidget : public QWidget
signals:
void set_auto_page_num() const;
void openAutoNumFolioEditor (QString);
void titleBlockSizeChanged(int height, Qt::Edge edge);
private:
Ui::TitleBlockPropertiesWidget *ui;
+112
View File
@@ -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})
@@ -264,6 +280,80 @@ set_tests_properties(tst_shortcutspage PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=o
target_include_directories(tst_shortcutspage PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_shortcutspage PRIVATE Qt::Test Qt::Widgets Qt::Svg)
# ToolbarSettings and its configuration page: defaults change nothing,
# saved values reach every open window that took them, panels are left alone.
add_executable(
tst_toolbarsettings
tst_toolbarsettings.cpp
${QET_DIR}/sources/toolbarsettings.cpp
${QET_DIR}/sources/toolbarsettings.h
${QET_DIR}/sources/ui/configpage/toolbarsconfigpage.cpp
${QET_DIR}/sources/ui/configpage/toolbarsconfigpage.h
${QET_DIR}/sources/ui/configpage/configpage.h
${QET_DIR}/sources/qeticons.cpp
${QET_DIR}/sources/qetpalette.cpp
${QET_DIR}/sources/qetstyle.cpp)
add_test(NAME tst_toolbarsettings COMMAND tst_toolbarsettings)
set_tests_properties(tst_toolbarsettings PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_toolbarsettings PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_toolbarsettings PRIVATE Qt::Test Qt::Widgets Qt::Svg)
# DiagramToolbarSettings and its configuration page: unchanged toolbars
# store nothing and build as before, unknown ids and stray separators are
# skipped, the user's own toolbars are kept, a widget is in one place only.
add_executable(
tst_diagramtoolbarsettings
tst_diagramtoolbarsettings.cpp
${QET_DIR}/sources/diagramtoolbarsettings.cpp
${QET_DIR}/sources/diagramtoolbarsettings.h
${QET_DIR}/sources/shortcutmanager.cpp
${QET_DIR}/sources/ui/configpage/toolbarcommandsconfigpage.cpp
${QET_DIR}/sources/ui/configpage/toolbarcommandsconfigpage.h
${QET_DIR}/sources/ui/configpage/configpage.h
${QET_DIR}/sources/qeticons.cpp
${QET_DIR}/sources/qetpalette.cpp
${QET_DIR}/sources/qetstyle.cpp)
add_test(NAME tst_diagramtoolbarsettings COMMAND tst_diagramtoolbarsettings)
set_tests_properties(tst_diagramtoolbarsettings PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_diagramtoolbarsettings PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_diagramtoolbarsettings PRIVATE Qt::Test Qt::Widgets Qt::Svg)
# GestureSettings, the gesture ring's 4 / 8 directions and empty slots, and
# its configuration page: the ring follows the shortcut bar until the user
# picks commands, then keeps each one in its direction.
add_executable(
tst_gesturesettings
tst_gesturesettings.cpp
${QET_DIR}/sources/gesturesettings.cpp
${QET_DIR}/sources/gesturesettings.h
${QET_DIR}/sources/shortcutbarsettings.cpp
${QET_DIR}/sources/diagramgestureoverlay.cpp
${QET_DIR}/sources/diagramgestureoverlay.h
${QET_DIR}/sources/ui/configpage/gesturesconfigpage.cpp
${QET_DIR}/sources/ui/configpage/gesturesconfigpage.h
${QET_DIR}/sources/ui/configpage/configpage.h
${QET_DIR}/sources/shortcutmanager.cpp
${QET_DIR}/sources/qeticons.cpp
${QET_DIR}/sources/qetpalette.cpp
${QET_DIR}/sources/qetstyle.cpp)
add_test(NAME tst_gesturesettings COMMAND tst_gesturesettings)
set_tests_properties(tst_gesturesettings PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_gesturesettings PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_gesturesettings PRIVATE Qt::Test Qt::Widgets Qt::Svg Qt::Xml pugixml::pugixml)
# CustomizeDialog: the configuration pages as tabs of one window; OK
# applies every page once, Cancel none.
add_executable(
tst_customizedialog
tst_customizedialog.cpp
${QET_DIR}/sources/ui/customizedialog.cpp
${QET_DIR}/sources/ui/customizedialog.h
${QET_DIR}/sources/ui/configpage/configpage.h)
add_test(NAME tst_customizedialog COMMAND tst_customizedialog)
set_tests_properties(tst_customizedialog PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_customizedialog PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_customizedialog PRIVATE Qt::Test Qt::Widgets)
# scriptheader.h is header-only: the // ==QETScript== block that turns a
# stored script into a button; and ShortcutManager::unregisterAction(), which
# takes a deleted script's command out of the lists again.
@@ -462,6 +552,28 @@ target_link_libraries(tst_dxfwrappedtext PRIVATE Qt::Test)
target_compile_definitions(tst_dxfwrappedtext PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# --export-dxf on a folio with its title block at the right (issue #1339):
# a circle and an arc stay one CIRCLE and one ARC, the title block is turned.
add_executable(
tst_dxfcurves
tst_dxfcurves.cpp)
add_test(NAME tst_dxfcurves COMMAND tst_dxfcurves)
add_dependencies(tst_dxfcurves qelectrotech)
target_link_libraries(tst_dxfcurves PRIVATE Qt::Test)
target_compile_definitions(tst_dxfcurves PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# --export-dxf --dxf-blocks: a symbol placed twice is one BLOCK and two
# INSERTs (issue #1339).
add_executable(
tst_dxfblocks
tst_dxfblocks.cpp)
add_test(NAME tst_dxfblocks COMMAND tst_dxfblocks)
add_dependencies(tst_dxfblocks qelectrotech)
target_link_libraries(tst_dxfblocks PRIVATE Qt::Test)
target_compile_definitions(tst_dxfblocks PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# --export-pdf puts an A3 folio on an A3 page whichever way it is turned:
# QPageSize matches standard sheets upright only, so a wide folio is matched
# upright and turned. fixtures/pdf_page_a3.qet has one folio each way.
+79
View File
@@ -0,0 +1,79 @@
<project title="DXF symbol blocks" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7b10}" version="0.200.1">
<properties>
<property name="saveddate" show="1">10/2/26</property>
<property name="saveddate-eu" show="1">02-10-2026</property>
<property name="saveddate-us" show="1">2026-10-02</property>
<property name="savedfilename" show="1">dxf_blocks</property>
<property name="savedtime" show="1">18:39</property>
</properties>
<usage enabled="true" time_spent="0"/>
<newdiagrams>
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<report label="%f-%l%c"/>
<xrefs>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="coil" xrefpos="AlignBottom"/>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="commutator" xrefpos="AlignBottom"/>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="plc" xrefpos="AlignBottom"/>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="protection" xrefpos="AlignBottom"/>
</xrefs>
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
<folio_autonums/>
<element_autonums current_autonum="" freeze_new_elements="false"/>
<guides/>
</newdiagrams>
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7a01}" version="0.200.1-dev">
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<elements>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="" type="embed://import/curves.elmt" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7a02}" x="300" y="300" z="10">
<terminals/>
<inputs/>
<elementInformations>
<elementInformation name="label" show="1">K1</elementInformation>
<elementInformation name="manufacturer" show="1">Schneider</elementInformation>
</elementInformations>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7b01}" x="0" y="25">
<text>K1</text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
<element freezeLabel="false" is_movable="1" orientation="1" prefix="" type="embed://import/curves.elmt" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7a03}" x="500" y="300" z="10">
<terminals/>
<inputs/>
<elementInformations>
<elementInformation name="label" show="1">K2</elementInformation>
</elementInformations>
<dynamic_texts>
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7b02}" x="0" y="25">
<text>K2</text>
<info_name>label</info_name>
</dynamic_elmt_text>
</dynamic_texts>
<texts_groups/>
</element>
</elements>
</diagram>
<collection>
<category name="import">
<names>
<name lang="en">Imported elements</name>
</names>
<element name="curves.elmt">
<definition height="80" hotspot_x="10" hotspot_y="10" link_type="simple" type="element" version="0.100.0" width="120">
<uuid uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7a05}"/>
<names>
<name lang="en">Circle and arc</name>
</names>
<description>
<ellipse antialias="true" style="line-style:normal;line-weight:normal;filling:none;color:black" x="0" y="0" width="20" height="20"/>
<arc antialias="true" style="line-style:normal;line-weight:normal;filling:none;color:black" x="40" y="0" width="20" height="20" start="30" angle="120"/>
</description>
</definition>
</element>
</category>
</collection>
</project>
+56
View File
@@ -0,0 +1,56 @@
<project title="DXF curves and title block" uuid="{6c1a7d2e-9b54-4d2f-8f68-3f0d0f0a7b10}" version="0.200.1">
<properties>
<property name="saveddate" show="1">10/2/26</property>
<property name="saveddate-eu" show="1">02-10-2026</property>
<property name="saveddate-us" show="1">2026-10-02</property>
<property name="savedfilename" show="1">dxf_curves</property>
<property name="savedtime" show="1">18:39</property>
</properties>
<usage enabled="true" time_spent="0"/>
<newdiagrams>
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<report label="%f-%l%c"/>
<xrefs>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="coil" xrefpos="AlignBottom"/>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="commutator" xrefpos="AlignBottom"/>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="plc" xrefpos="AlignBottom"/>
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="protection" xrefpos="AlignBottom"/>
</xrefs>
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
<folio_autonums/>
<element_autonums current_autonum="" freeze_new_elements="false"/>
<guides/>
</newdiagrams>
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="right" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="" uuid="{6c1a7d2e-9b54-4d2f-8f68-3f0d0f0a7a01}" version="0.200.1-dev">
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
<elements>
<element freezeLabel="false" is_movable="1" orientation="0" prefix="" type="embed://import/curves.elmt" uuid="{6c1a7d2e-9b54-4d2f-8f68-3f0d0f0a7a02}" x="300" y="300" z="10">
<terminals/>
<inputs/>
<dynamic_texts/>
<texts_groups/>
</element>
</elements>
</diagram>
<collection>
<category name="import">
<names>
<name lang="en">Imported elements</name>
</names>
<element name="curves.elmt">
<definition height="80" hotspot_x="10" hotspot_y="10" link_type="simple" type="element" version="0.100.0" width="120">
<uuid uuid="{6c1a7d2e-9b54-4d2f-8f68-3f0d0f0a7a05}"/>
<names>
<name lang="en">Circle and arc</name>
</names>
<description>
<ellipse antialias="true" style="line-style:normal;line-weight:normal;filling:none;color:black" x="0" y="0" width="20" height="20"/>
<arc antialias="true" style="line-style:normal;line-weight:normal;filling:none;color:black" x="40" y="0" width="20" height="20" start="30" angle="120"/>
</description>
</definition>
</element>
</category>
</collection>
</project>
@@ -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;
}
+96
View File
@@ -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)
+93
View File
@@ -0,0 +1,93 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#include "ui/customizedialog.h"
#include "ui/configpage/configpage.h"
#include <QDialogButtonBox>
#include <QIcon>
#include <QPushButton>
#include <QTabWidget>
#include <QTest>
/// A configuration page that counts how often it was applied
class CountingPage : public ConfigPage
{
public:
CountingPage(const QString &title) : ConfigPage(nullptr), m_title(title) {}
void applyConf() override { ++applied; }
QString title() const override { return m_title; }
QIcon icon() const override { return QIcon(); }
int applied = 0;
private:
QString m_title;
};
/**
CustomizeDialog: one tab per page in the order added, OK applies every
page once, Cancel applies none.
*/
class tst_customizedialog : public QObject
{
Q_OBJECT
private slots:
void tabsInOrder()
{
CustomizeDialog dialog;
auto *a = new CountingPage(QStringLiteral("Barres d'outils"));
auto *b = new CountingPage(QStringLiteral("Clavier"));
dialog.addPage(a);
dialog.addPage(b);
dialog.addPage(a); // twice: ignored
dialog.addPage(nullptr);
auto *tabs = dialog.findChild<QTabWidget *>(QStringLiteral("customizeTabs"));
QVERIFY(tabs);
QCOMPARE(tabs->count(), 2);
QCOMPARE(tabs->tabText(0), QStringLiteral("Barres d'outils"));
QCOMPARE(tabs->tabText(1), QStringLiteral("Clavier"));
QCOMPARE(tabs->widget(1), b);
}
void okAppliesEveryPage()
{
CustomizeDialog dialog;
auto *a = new CountingPage(QStringLiteral("a"));
auto *b = new CountingPage(QStringLiteral("b"));
dialog.addPage(a);
dialog.addPage(b);
dialog.show();
dialog.findChild<QDialogButtonBox *>()->button(QDialogButtonBox::Ok)->click();
QCOMPARE(a->applied, 1);
QCOMPARE(b->applied, 1);
QCOMPARE(dialog.result(), int(QDialog::Accepted));
}
void cancelAppliesNothing()
{
CustomizeDialog dialog;
auto *a = new CountingPage(QStringLiteral("a"));
dialog.addPage(a);
dialog.show();
dialog.findChild<QDialogButtonBox *>()->button(QDialogButtonBox::Cancel)->click();
QCOMPARE(a->applied, 0);
QCOMPARE(dialog.result(), int(QDialog::Rejected));
}
};
QTEST_MAIN(tst_customizedialog)
#include "tst_customizedialog.moc"
+129
View File
@@ -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"
+245
View File
@@ -0,0 +1,245 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#include "diagramtoolbarsettings.h"
#include "shortcutmanager.h"
#include "ui/configpage/toolbarcommandsconfigpage.h"
#include <QAction>
#include <QComboBox>
#include <QListWidget>
#include <QPushButton>
#include <QSettings>
#include <QStandardPaths>
#include <QTest>
#include <QToolBar>
#include <QWidget>
/**
A top-level window standing in for the diagram editor: counts the
calls applyToAll() makes.
*/
class RebuildCounter : public QWidget
{
Q_OBJECT
public:
int rebuilds = 0;
public slots:
void rebuildToolBars() { ++rebuilds; }
};
/**
DiagramToolbarSettings and its configuration page: an unchanged
toolbar stores nothing and is built as before, unknown ids and stray
separators are skipped, the user's own toolbars are kept and removed
with their contents, and the page saves what it shows.
*/
class tst_diagramtoolbarsettings : public QObject
{
Q_OBJECT
QObject m_owner;
QHash<QString, QAction *> m_actions;
/// The action for @a id, made on first use, as the editor's resolver
QAction *resolve(const QString &id)
{
if (id.startsWith(QLatin1String("missing."))) return nullptr;
if (!m_actions.contains(id)) {
auto *action = new QAction(id, &m_owner);
m_actions.insert(id, action);
}
return m_actions.value(id);
}
static QStringList shape(QToolBar *toolbar)
{
QStringList list;
for (QAction *action : toolbar->actions()) {
list << (action->isSeparator() ? QStringLiteral("|") : action->text());
}
return list;
}
static void clearSettings()
{
QSettings settings;
settings.remove(QStringLiteral("diagrameditor/toolbars"));
settings.remove(QStringLiteral("diagrameditor/custom_toolbars"));
}
private slots:
void initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
clearSettings();
//A few of the diagram editor's commands, as it registers them
for (const QString &id : {QStringLiteral("diagrameditor.copy"),
QStringLiteral("diagrameditor.paste"),
QStringLiteral("diagrameditor.zoom_fit"),
QStringLiteral("depth.raise"),
QStringLiteral("elementeditor.copy")}) {
ShortcutManager::instance().registerAction(
new QAction(id, &m_owner), id, QStringLiteral("test"), QKeySequence());
}
}
void init() { clearSettings(); }
void defaultsStoreNothing()
{
for (const QString &name : DiagramToolbarSettings::builtInNames()) {
QVERIFY(!DiagramToolbarSettings::defaultIds(name).isEmpty());
QCOMPARE(DiagramToolbarSettings::ids(name), DiagramToolbarSettings::defaultIds(name));
DiagramToolbarSettings::setIds(name, DiagramToolbarSettings::defaultIds(name));
}
QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/toolbars/toolbar")));
DiagramToolbarSettings::setIds(QStringLiteral("toolbar"), {QStringLiteral("diagrameditor.copy")});
QCOMPARE(DiagramToolbarSettings::ids(QStringLiteral("toolbar")),
QStringList{QStringLiteral("diagrameditor.copy")});
//Saving an empty toolbar keeps it empty, not back to the defaults
DiagramToolbarSettings::setIds(QStringLiteral("toolbar"), {});
QVERIFY(DiagramToolbarSettings::ids(QStringLiteral("toolbar")).isEmpty());
}
/// The defaults are built exactly as the toolbars were hand-built
/// before: the main toolbar's three separators in place
void defaultsBuildAsBefore()
{
QToolBar toolbar;
DiagramToolbarSettings::fill(&toolbar, DiagramToolbarSettings::defaultIds(QStringLiteral("toolbar")),
[this](const QString &id) { return resolve(id); });
QCOMPARE(toolbar.actions().size(), 18);
QCOMPARE(shape(&toolbar).mid(6, 2),
(QStringList{QStringLiteral("diagrameditor.export_to_pdf"), QStringLiteral("|")}));
QCOMPARE(shape(&toolbar).count(QStringLiteral("|")), 3);
}
void fillSkipsUnknownAndStraySeparators()
{
const QString sep = DiagramToolbarSettings::separatorId();
QToolBar toolbar;
toolbar.addAction(QStringLiteral("left over"));
DiagramToolbarSettings::fill(&toolbar,
{sep, QStringLiteral("a"), sep, sep, QStringLiteral("missing.b"),
QStringLiteral("c"), sep, QStringLiteral("missing.d"), sep},
[this](const QString &id) { return resolve(id); });
QCOMPARE(shape(&toolbar), (QStringList{QStringLiteral("a"), QStringLiteral("|"), QStringLiteral("c")}));
DiagramToolbarSettings::fill(&toolbar, {}, [this](const QString &id) { return resolve(id); });
QVERIFY(toolbar.actions().isEmpty());
}
void customToolbarsKeptAndRemoved()
{
using T = DiagramToolbarSettings::Toolbar;
const QString first = DiagramToolbarSettings::newCustomName({});
QCOMPARE(first, QStringLiteral("custom_1"));
DiagramToolbarSettings::setCustomToolbars({T{first, QStringLiteral("Mine"), true},
T{QStringLiteral("custom_2"), QStringLiteral("Other"), true}});
DiagramToolbarSettings::setIds(first, {QStringLiteral("diagrameditor.copy")});
QCOMPARE(DiagramToolbarSettings::customToolbars().size(), 2);
QCOMPARE(DiagramToolbarSettings::customToolbars().first().title, QStringLiteral("Mine"));
QCOMPARE(DiagramToolbarSettings::toolbars().size(),
DiagramToolbarSettings::builtInNames().size() + 2);
QCOMPARE(DiagramToolbarSettings::newCustomName({}), QStringLiteral("custom_3"));
//A new toolbar starts empty
QVERIFY(DiagramToolbarSettings::ids(QStringLiteral("custom_2")).isEmpty());
DiagramToolbarSettings::setCustomToolbars({T{QStringLiteral("custom_2"), QStringLiteral("Other"), true}});
QCOMPARE(DiagramToolbarSettings::customToolbars().size(), 1);
QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/toolbars/") + first));
DiagramToolbarSettings::setCustomToolbars({});
QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/custom_toolbars/names")));
}
void availableCommandsAreTheDiagramEditors()
{
const QStringList ids = DiagramToolbarSettings::availableCommandIds();
QVERIFY(ids.contains(QStringLiteral("diagrameditor.copy")));
QVERIFY(ids.contains(QStringLiteral("depth.raise")));
QVERIFY(!ids.contains(QStringLiteral("elementeditor.copy")));
}
void pageSavesAndRebuilds()
{
RebuildCounter marked, unmarked;
DiagramToolbarSettings::markWindow(&marked);
ToolbarCommandsConfigPage page;
auto *combo = page.findChild<QComboBox *>(QStringLiteral("toolbarCombo"));
auto *available = page.findChild<QListWidget *>(QStringLiteral("availableList"));
auto *chosen = page.findChild<QListWidget *>(QStringLiteral("chosenList"));
QVERIFY(combo && available && chosen);
QCOMPARE(combo->count(), DiagramToolbarSettings::builtInNames().size());
QCOMPARE(chosen->count(), DiagramToolbarSettings::defaultIds(QStringLiteral("toolbar")).size());
//OK without a change stores nothing
page.applyConf();
QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/toolbars/toolbar")));
QCOMPARE(marked.rebuilds, 1);
QCOMPARE(unmarked.rebuilds, 0);
//Remove everything but the first command, add a separator
while (chosen->count() > 1) delete chosen->takeItem(1);
chosen->setCurrentRow(0);
page.findChild<QPushButton *>(QStringLiteral("separatorButton"))->click();
page.applyConf();
QCOMPARE(DiagramToolbarSettings::ids(QStringLiteral("toolbar")),
(QStringList{QStringLiteral("diagrameditor.new_file"), DiagramToolbarSettings::separatorId()}));
QCOMPARE(marked.rebuilds, 2);
}
/// A widget button can be in one place only: once moved to another
/// toolbar it is no longer offered, and resetting its own toolbar
/// takes it back
void widgetInOnePlace()
{
ToolbarCommandsConfigPage page;
auto *combo = page.findChild<QComboBox *>(QStringLiteral("toolbarCombo"));
auto *available = page.findChild<QListWidget *>(QStringLiteral("availableList"));
auto *chosen = page.findChild<QListWidget *>(QStringLiteral("chosenList"));
auto offered = [available](const QString &id) {
for (int i = 0 ; i < available->count() ; ++i)
if (available->item(i)->data(Qt::UserRole).toString() == id) return true;
return false;
};
const QString colour = QStringLiteral("widget:conductor_color");
//On "diagram" by default, so not offered for "toolbar"
QVERIFY(!offered(colour));
//Taken off "diagram" (switching toolbars keeps the change)
combo->setCurrentIndex(combo->findData(QStringLiteral("diagram")));
for (int i = chosen->count() - 1 ; i >= 0 ; --i) {
if (chosen->item(i)->data(Qt::UserRole).toString() == colour) delete chosen->takeItem(i);
}
combo->setCurrentIndex(combo->findData(QStringLiteral("toolbar")));
QVERIFY(offered(colour));
//Reset "diagram": the colour button goes back there
combo->setCurrentIndex(combo->findData(QStringLiteral("diagram")));
page.findChild<QPushButton *>(QStringLiteral("resetButton"))->click();
QCOMPARE(chosen->count(), DiagramToolbarSettings::defaultIds(QStringLiteral("diagram")).size());
combo->setCurrentIndex(combo->findData(QStringLiteral("toolbar")));
QVERIFY(!offered(colour));
}
};
QTEST_MAIN(tst_diagramtoolbarsettings)
#include "tst_diagramtoolbarsettings.moc"
+147
View File
@@ -0,0 +1,147 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#include <QtTest>
#include <QProcess>
#include <QTemporaryDir>
/**
--export-dxf --dxf-blocks (issue #1339): a symbol placed twice is one
BLOCK and two INSERTs, the second turned a quarter, and each symbol's
information is a hidden attribute of it; with --dxf-attributes its
label is a visible one. Without the switches there is neither.
*/
class tst_dxfblocks : public QObject
{
Q_OBJECT
/// The group code / value pairs of the exported folio
static QList<QPair<QString, QString>> exportPairs(const QStringList &extra)
{
const QString project = QFINDTESTDATA("fixtures/dxf_blocks.qet");
QTemporaryDir dir;
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
QStringList{QStringLiteral("--export-dxf"), project, dir.path()} + extra);
if (!proc.waitForFinished(60000) || proc.exitCode() != 0)
return {};
QFile file(dir.filePath(QStringLiteral("01_diagram.dxf")));
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return {};
const QStringList values = QString::fromUtf8(file.readAll()).split(QLatin1Char('\n'));
QList<QPair<QString, QString>> pairs;
for (int i = 0 ; i + 1 < values.size() ; i += 2)
pairs << qMakePair(values.at(i).trimmed(), values.at(i + 1).trimmed());
return pairs;
}
/// The values of code @a code in each entity of type @a type
static QStringList values(const QList<QPair<QString, QString>> &pairs,
const QString &type, const QString &code)
{
QStringList found;
for (int i = 0 ; i < pairs.size() ; ++i) {
if (pairs.at(i).first != QLatin1String("0") || pairs.at(i).second != type)
continue;
for (int j = i + 1 ; j < pairs.size() && pairs.at(j).first != QLatin1String("0") ; ++j)
if (pairs.at(j).first == code)
found << pairs.at(j).second;
}
return found;
}
private slots:
void blocks()
{
const auto pairs = exportPairs({QStringLiteral("--dxf-blocks")});
QVERIFY2(!pairs.isEmpty(), "--export-dxf --dxf-blocks failed");
QCOMPARE(values(pairs, QStringLiteral("BLOCK"), QStringLiteral("2")),
QStringList{QStringLiteral("QET_CURVES")});
QCOMPARE(values(pairs, QStringLiteral("INSERT"), QStringLiteral("2")),
(QStringList{QStringLiteral("QET_CURVES"), QStringLiteral("QET_CURVES")}));
QCOMPARE(values(pairs, QStringLiteral("INSERT"), QStringLiteral("50")),
(QStringList{QStringLiteral("0"), QStringLiteral("270")}));
//The circle is drawn once, inside the block
QCOMPARE(values(pairs, QStringLiteral("CIRCLE"), QStringLiteral("8")).size(), 1);
}
void labelsAsAttributes()
{
const auto pairs = exportPairs({QStringLiteral("--dxf-attributes")});
QVERIFY2(!pairs.isEmpty(), "--export-dxf --dxf-attributes failed");
QCOMPARE(values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("2"))
.count(QStringLiteral("LABEL")), 2);
const QStringList texts = values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("1"));
QVERIFY(texts.contains(QStringLiteral("K1")));
QVERIFY(texts.contains(QStringLiteral("K2")));
//Defined once in the block, and no longer a loose text
QCOMPARE(values(pairs, QStringLiteral("ATTDEF"), QStringLiteral("2"))
.count(QStringLiteral("LABEL")), 1);
QVERIFY(!values(pairs, QStringLiteral("TEXT"), QStringLiteral("1")).contains(QStringLiteral("K1")));
}
void labelsStayTextsWithBlocksOnly()
{
const auto pairs = exportPairs({QStringLiteral("--dxf-blocks")});
QVERIFY2(!pairs.isEmpty(), "--export-dxf --dxf-blocks failed");
//Only hidden attributes (70 = 1); the labels are texts
QVERIFY(!values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("70")).contains(QStringLiteral("0")));
QVERIFY(values(pairs, QStringLiteral("TEXT"), QStringLiteral("1")).contains(QStringLiteral("K1")));
}
void dataAsHiddenAttributes()
{
//The label is a text, so it is part data too: K1's label and
//manufacturer, K2's label, all hidden (70 = 1)
auto pairs = exportPairs({QStringLiteral("--dxf-blocks")});
QVERIFY2(!pairs.isEmpty(), "--export-dxf --dxf-blocks failed");
QCOMPARE(values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("2")),
(QStringList{QStringLiteral("LABEL"), QStringLiteral("MANUFACTURER"),
QStringLiteral("LABEL")}));
QCOMPARE(values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("70")),
(QStringList{QStringLiteral("1"), QStringLiteral("1"), QStringLiteral("1")}));
QVERIFY(values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("1"))
.contains(QStringLiteral("Schneider")));
//With the labels as attributes, the label is not repeated
pairs = exportPairs({QStringLiteral("--dxf-attributes")});
QVERIFY2(!pairs.isEmpty(), "--export-dxf --dxf-attributes failed");
QCOMPARE(values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("2")),
(QStringList{QStringLiteral("LABEL"), QStringLiteral("MANUFACTURER"),
QStringLiteral("LABEL")}));
QCOMPARE(values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("70")),
(QStringList{QStringLiteral("0"), QStringLiteral("1"), QStringLiteral("0")}));
}
void flatWithoutSwitch()
{
const auto pairs = exportPairs({});
QVERIFY2(!pairs.isEmpty(), "--export-dxf failed");
QVERIFY(values(pairs, QStringLiteral("BLOCK"), QStringLiteral("2")).isEmpty());
QVERIFY(values(pairs, QStringLiteral("INSERT"), QStringLiteral("2")).isEmpty());
QCOMPARE(values(pairs, QStringLiteral("CIRCLE"), QStringLiteral("8")).size(), 2);
QVERIFY(values(pairs, QStringLiteral("TEXT"), QStringLiteral("1")).contains(QStringLiteral("K1")));
}
};
QTEST_APPLESS_MAIN(tst_dxfblocks)
#include "tst_dxfblocks.moc"
+95
View File
@@ -0,0 +1,95 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#include <QtTest>
#include <QProcess>
#include <QTemporaryDir>
/**
--export-dxf on a folio whose title block is at the right (issue #1339):
a symbol's circle is one CIRCLE and its arc one ARC with the symbol's own
angles, which needs the same scale on both axes; the title block is
turned a quarter, as it is on screen.
*/
class tst_dxfcurves : public QObject
{
Q_OBJECT
/// The entities on @a layer, each as its group codes and values.
static QList<QMultiHash<int, QString>> entities(const QStringList &values,
const QString &type,
const QString &layer)
{
QList<QMultiHash<int, QString>> found;
for (int i = 0 ; i + 1 < values.size() ; i += 2)
{
if (values.at(i).trimmed() != QLatin1String("0")
|| values.at(i + 1).trimmed() != type)
continue;
QMultiHash<int, QString> entity;
for (int j = i + 2 ; j + 1 < values.size()
&& values.at(j).trimmed() != QLatin1String("0") ; j += 2)
entity.insert(values.at(j).trimmed().toInt(), values.at(j + 1).trimmed());
if (entity.value(8) == layer)
found << entity;
}
return found;
}
private slots:
void curvesAndTitleBlock()
{
const QString project = QFINDTESTDATA("fixtures/dxf_curves.qet");
QVERIFY2(!project.isEmpty(), "fixture project not found");
QTemporaryDir dir;
QVERIFY(dir.isValid());
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
QProcess proc;
proc.setProcessEnvironment(env);
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--export-dxf"), project, dir.path()});
QVERIFY2(proc.waitForFinished(60000), "--export-dxf timed out");
QCOMPARE(proc.exitCode(), 0);
QFile file(dir.filePath(QStringLiteral("01_diagram.dxf")));
QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text));
const QStringList values = QString::fromUtf8(file.readAll()).split(QLatin1Char('\n'));
const QString symbols = QStringLiteral("QET_SYMBOLS");
//The 20 px circle: one CIRCLE, nothing approximated
const auto circles = entities(values, QStringLiteral("CIRCLE"), symbols);
QCOMPARE(circles.size(), 1);
QVERIFY(entities(values, QStringLiteral("POLYLINE"), symbols).isEmpty());
//The arc from 30 to 150 degrees, same radius as the circle
const auto arcs = entities(values, QStringLiteral("ARC"), symbols);
QCOMPARE(arcs.size(), 1);
QCOMPARE(arcs.first().value(50).toDouble(), 30.0);
QCOMPARE(arcs.first().value(51).toDouble(), 150.0);
QCOMPARE(arcs.first().value(40).toDouble(), circles.first().value(40).toDouble());
//Every title block text turned a quarter, as drawn on screen
const auto texts = entities(values, QStringLiteral("TEXT"), QStringLiteral("QET_TITLEBLOCK"));
QVERIFY(!texts.isEmpty());
for (const auto &text : texts)
QCOMPARE(text.value(50).toDouble(), 90.0);
}
};
QTEST_APPLESS_MAIN(tst_dxfcurves)
#include "tst_dxfcurves.moc"
+214
View File
@@ -0,0 +1,214 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#include "diagramgestureoverlay.h"
#include "gesturesettings.h"
#include "shortcutmanager.h"
#include "ui/configpage/gesturesconfigpage.h"
#include <QAction>
#include <QComboBox>
#include <QDropEvent>
#include <QListWidget>
#include <QMainWindow>
#include <QMimeData>
#include <QPushButton>
#include <QSettings>
#include <QStandardPaths>
#include <QTest>
/**
The mouse gesture ring's own command list: until the user picks one it
follows the shortcut bar as before; a picked list keeps its directions;
4 or 8 directions; and the configuration page that edits it.
*/
class tst_gesturesettings : public QObject
{
Q_OBJECT
QMainWindow m_window;
QAction *addCommand(const QString &id, const QString &text)
{
auto *action = new QAction(text, &m_window);
ShortcutManager::instance().registerAction(
action, id, QStringLiteral("Éditeur de schémas"), QKeySequence());
return action;
}
QListWidgetItem *listItem(QListWidget *list, const QString &id)
{
for (int i = 0 ; i < list->count() ; ++i) {
if (list->item(i)->data(Qt::UserRole).toString() == id) {
return list->item(i);
}
}
return nullptr;
}
private slots:
void initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
QSettings().remove(QStringLiteral("diagrameditor"));
addCommand(QStringLiteral("diagrameditor.copy"), QStringLiteral("Copier"));
addCommand(QStringLiteral("diagrameditor.cut"), QStringLiteral("Couper"));
addCommand(QStringLiteral("diagrameditor.paste"), QStringLiteral("Coller"));
}
void cleanup()
{
QSettings().remove(QStringLiteral("diagrameditor"));
}
void defaultsFollowTheBar()
{
QCOMPARE(GestureSettings::directions(), 8);
for (const auto c : ShortcutBarSettings::contexts()) {
QVERIFY(!GestureSettings::isCustom(c));
QCOMPARE(GestureSettings::ids(c), ShortcutBarSettings::ids(c));
}
//A changed bar still reaches the ring, without its elements
ShortcutBarSettings::setIds(ShortcutBarSettings::Selection,
{QStringLiteral("diagrameditor.copy"),
QStringLiteral("common://10_electric/relay.elmt"),
QStringLiteral("diagrameditor.cut")});
QCOMPARE(GestureSettings::ids(ShortcutBarSettings::Selection),
QStringList({QStringLiteral("diagrameditor.copy"),
QStringLiteral("diagrameditor.cut")}));
}
void pickedListKeepsDirections()
{
GestureSettings::setIds(ShortcutBarSettings::Canvas,
{QStringLiteral("diagrameditor.copy"), QString(),
QStringLiteral("diagrameditor.paste"), QString(), QString()});
QVERIFY(GestureSettings::isCustom(ShortcutBarSettings::Canvas));
QCOMPARE(GestureSettings::ids(ShortcutBarSettings::Canvas),
QStringList({QStringLiteral("diagrameditor.copy"), QString(),
QStringLiteral("diagrameditor.paste")}));
//The bar no longer drives it
ShortcutBarSettings::setIds(ShortcutBarSettings::Canvas,
{QStringLiteral("diagrameditor.cut")});
QCOMPARE(GestureSettings::ids(ShortcutBarSettings::Canvas).first(),
QStringLiteral("diagrameditor.copy"));
//Other contexts still follow the bar
QVERIFY(!GestureSettings::isCustom(ShortcutBarSettings::Selection));
}
void savingTheDefaultsFollowsTheBarAgain()
{
GestureSettings::setIds(ShortcutBarSettings::Conductor, {QStringLiteral("diagrameditor.cut")});
QVERIFY(GestureSettings::isCustom(ShortcutBarSettings::Conductor));
GestureSettings::setIds(ShortcutBarSettings::Conductor,
GestureSettings::defaultIds(ShortcutBarSettings::Conductor));
QVERIFY(!GestureSettings::isCustom(ShortcutBarSettings::Conductor));
}
void directions()
{
GestureSettings::setDirections(4);
QCOMPARE(GestureSettings::directions(), 4);
GestureSettings::setDirections(8);
QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/gestures/directions")));
QSettings().setValue(QStringLiteral("diagrameditor/gestures/directions"), 5);
QCOMPARE(GestureSettings::directions(), 8);
}
void overlayDirectionsAndEmptySlots()
{
QWidget viewport;
viewport.resize(400, 400);
DiagramGestureOverlay overlay(&viewport);
QAction *copy = ShortcutManager::instance().action(QStringLiteral("diagrameditor.copy"), &m_window);
QAction *paste = ShortcutManager::instance().action(QStringLiteral("diagrameditor.paste"), &m_window);
const QPoint c(200, 200);
const QPoint right(260, 200), down(200, 260);
overlay.showAt(c, {copy, nullptr, paste}, 4);
QCOMPARE(overlay.sectors(), 4);
QCOMPARE(overlay.sectorAt(right), 1);
QCOMPARE(overlay.actionAt(right), nullptr);
QCOMPARE(overlay.actionAt(down), paste);
overlay.setPointer(right);
QVERIFY(!overlay.grab().isNull());
//Today's ring: eight directions
overlay.showAt(c, {copy, nullptr, paste});
QCOMPARE(overlay.sectors(), 8);
QCOMPARE(overlay.sectorAt(right), 2);
QCOMPARE(overlay.actionAt(right), paste);
}
void pagePlacesAndSaves()
{
{
GesturesConfigPage page;
auto *directions = page.findChild<QComboBox *>(QStringLiteral("directionsCombo"));
auto *list = page.findChild<QListWidget *>(QStringLiteral("availableList"));
auto *ring = page.findChild<GestureRingEditor *>(QStringLiteral("ringEditor"));
QCOMPARE(directions->currentData().toInt(), 8);
QCOMPARE(ring->ids(), GestureSettings::defaultIds(ShortcutBarSettings::Canvas));
directions->setCurrentIndex(directions->findData(4));
ring->setCurrentSlot(1);
list->setCurrentItem(listItem(list, QStringLiteral("diagrameditor.cut")));
page.findChild<QPushButton *>(QStringLiteral("placeButton"))->click();
page.applyConf();
}
QCOMPARE(GestureSettings::directions(), 4);
QVERIFY(GestureSettings::isCustom(ShortcutBarSettings::Canvas));
QCOMPARE(GestureSettings::ids(ShortcutBarSettings::Canvas).at(1),
QStringLiteral("diagrameditor.cut"));
//Contexts left alone still follow the bar
QVERIFY(!GestureSettings::isCustom(ShortcutBarSettings::Selection));
{
GesturesConfigPage page;
page.findChild<QPushButton *>(QStringLiteral("followBarButton"))->click();
page.applyConf();
}
QVERIFY(!GestureSettings::isCustom(ShortcutBarSettings::Canvas));
}
void dropOnASlot()
{
GesturesConfigPage page;
auto *list = page.findChild<QListWidget *>(QStringLiteral("availableList"));
auto *ring = page.findChild<GestureRingEditor *>(QStringLiteral("ringEditor"));
ring->resize(ring->sizeHint());
const QModelIndex index = list->model()->index(
list->row(listItem(list, QStringLiteral("diagrameditor.paste"))), 0);
QMimeData *mime = list->model()->mimeData({index});
//The right-hand slot of eight
const QPointF target = QRectF(ring->rect()).center() + QPointF(68, 0);
QCOMPARE(ring->slotAt(target.toPoint()), 2);
QDropEvent drop(target, Qt::CopyAction, mime, Qt::LeftButton, Qt::NoModifier);
//Straight to the widget: QApplication drops drag events when
//no real drag is running
QVERIFY(static_cast<QObject *>(ring)->event(&drop));
QVERIFY(drop.isAccepted());
QCOMPARE(ring->currentSlot(), 2);
QCOMPARE(ring->ids().value(2), QStringLiteral("diagrameditor.paste"));
//Moved, not copied: paste was already on the default canvas ring
QCOMPARE(ring->ids().count(QStringLiteral("diagrameditor.paste")), 1);
delete mime;
}
};
QTEST_MAIN(tst_gesturesettings)
#include "tst_gesturesettings.moc"
+146
View File
@@ -0,0 +1,146 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#include "toolbarsettings.h"
#include "ui/configpage/toolbarsconfigpage.h"
#include <QCheckBox>
#include <QComboBox>
#include <QDockWidget>
#include <QMainWindow>
#include <QSettings>
#include <QStandardPaths>
#include <QTest>
#include <QToolBar>
/**
ToolbarSettings and its configuration page: defaults change nothing,
saved values reach every open window that took them, and a toolbar
inside a panel or a window that never took them is left alone.
*/
class tst_toolbarsettings : public QObject
{
Q_OBJECT
QMainWindow *m_window = nullptr;
QToolBar *m_top = nullptr;
QToolBar *m_left = nullptr;
QToolBar *m_in_dock = nullptr;
QMainWindow *m_other = nullptr;
QToolBar *m_other_bar = nullptr;
QSize m_default_size;
private slots:
void initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
QSettings().remove(QStringLiteral("toolbars"));
m_window = new QMainWindow();
m_default_size = m_window->iconSize();
m_top = new QToolBar(QStringLiteral("top"), m_window);
m_window->addToolBar(Qt::TopToolBarArea, m_top);
//Created without a parent, as some QET toolbars are
m_left = new QToolBar(QStringLiteral("left"));
m_window->addToolBar(Qt::LeftToolBarArea, m_left);
auto *dock = new QDockWidget(m_window);
m_in_dock = new QToolBar(dock);
dock->setWidget(m_in_dock);
m_window->addDockWidget(Qt::LeftDockWidgetArea, dock);
m_window->show();
//A window that never takes the settings, like the print window
m_other = new QMainWindow();
m_other_bar = new QToolBar(QStringLiteral("other"), m_other);
m_other->addToolBar(Qt::TopToolBarArea, m_other_bar);
m_other->show();
}
void cleanupTestCase()
{
delete m_window;
delete m_other;
}
void defaultsChangeNothing()
{
QCOMPARE(ToolbarSettings::iconSize(), 0);
QCOMPARE(ToolbarSettings::buttonStyle(), Qt::ToolButtonIconOnly);
QCOMPARE(ToolbarSettings::locked(), false);
ToolbarSettings::applyTo(m_window);
QCOMPARE(m_window->iconSize(), m_default_size);
QCOMPARE(m_top->iconSize(), m_default_size);
QCOMPARE(m_top->toolButtonStyle(), Qt::ToolButtonIconOnly);
QVERIFY(m_top->isMovable());
QVERIFY(m_left->isMovable());
}
void pageSavesAndAppliesToOpenWindows()
{
{
ToolbarsConfigPage page;
auto *size = page.findChild<QComboBox *>(QStringLiteral("iconSizeCombo"));
auto *style = page.findChild<QComboBox *>(QStringLiteral("buttonStyleCombo"));
size->setCurrentIndex(size->findData(32));
style->setCurrentIndex(style->findData(int(Qt::ToolButtonTextUnderIcon)));
page.findChild<QCheckBox *>(QStringLiteral("lockedCheck"))->setChecked(true);
page.applyConf();
}
QCOMPARE(ToolbarSettings::iconSize(), 32);
QCOMPARE(ToolbarSettings::buttonStyle(), Qt::ToolButtonTextUnderIcon);
QVERIFY(ToolbarSettings::locked());
QCOMPARE(m_top->iconSize(), QSize(32, 32));
QCOMPARE(m_left->iconSize(), QSize(32, 32));
QCOMPARE(m_top->toolButtonStyle(), Qt::ToolButtonTextUnderIcon);
QVERIFY(!m_top->isMovable());
QVERIFY(!m_left->isMovable());
//A toolbar inside a panel is not one of the window's toolbars
QVERIFY(m_in_dock->isMovable());
//Nor is a window that did not take the settings when it opened
QCOMPARE(m_other_bar->iconSize(), m_default_size);
QCOMPARE(m_other_bar->toolButtonStyle(), Qt::ToolButtonIconOnly);
QVERIFY(m_other_bar->isMovable());
//A page opened now shows what was saved
ToolbarsConfigPage page;
QCOMPARE(page.findChild<QComboBox *>(QStringLiteral("iconSizeCombo"))->currentData().toInt(), 32);
QVERIFY(page.findChild<QCheckBox *>(QStringLiteral("lockedCheck"))->isChecked());
}
void backToDefaultsRemovesTheKeys()
{
ToolbarSettings::save(0, Qt::ToolButtonIconOnly, false);
QVERIFY(!QSettings().contains(QStringLiteral("toolbars/icon_size")));
QVERIFY(!QSettings().contains(QStringLiteral("toolbars/button_style")));
QVERIFY(!QSettings().contains(QStringLiteral("toolbars/locked")));
ToolbarSettings::applyToAll();
QCOMPARE(m_top->iconSize(), m_default_size);
QCOMPARE(m_top->toolButtonStyle(), Qt::ToolButtonIconOnly);
QVERIFY(m_top->isMovable());
}
void unknownStyleFallsBack()
{
QSettings().setValue(QStringLiteral("toolbars/button_style"), 99);
QCOMPARE(ToolbarSettings::buttonStyle(), Qt::ToolButtonIconOnly);
QSettings().remove(QStringLiteral("toolbars/button_style"));
}
};
QTEST_MAIN(tst_toolbarsettings)
#include "tst_toolbarsettings.moc"
+6
View File
@@ -89,6 +89,12 @@ private slots:
.contains(QStringLiteral("RESULT placed")));
QVERIFY(QFile::exists(embedded));
#ifdef Q_OS_WIN
// Qt cannot take read permission away on Windows: it only knows the
// read-only flag there, so this returns false. The check needs a
// file the user cannot read, which Windows cannot give it.
QSKIP("Windows cannot make the file unreadable through QFile");
#endif
QVERIFY(QFile::setPermissions(m_symbol, QFileDevice::Permissions()));
QFile probe(m_symbol);
if (probe.open(QIODevice::ReadOnly))