mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-07 20:34:14 +02:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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 ¢er,
|
||||
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);
|
||||
|
||||
@@ -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 ¢er, const QList<QAction *> &actions);
|
||||
void showAt(const QPoint ¢er, 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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "palettegraphicsview.h"
|
||||
|
||||
#include "qetpalette.h"
|
||||
#include "darkimagerendering.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QEvent>
|
||||
@@ -99,12 +100,13 @@ bool PaletteGraphicsView::eventFilter(QObject *watched, QEvent *event)
|
||||
*/
|
||||
void PaletteGraphicsView::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
if (invertsLightness() && !customBackgroundColor())
|
||||
if (invertsLightness())
|
||||
{
|
||||
paintInverted(event);
|
||||
return;
|
||||
}
|
||||
m_buffer = QImage();
|
||||
m_composite = QImage();
|
||||
QGraphicsView::paintEvent(event);
|
||||
}
|
||||
|
||||
@@ -130,7 +132,7 @@ void PaletteGraphicsView::paintInverted(QPaintEvent *event)
|
||||
const QSize size(qCeil(viewport()->width() * ratio), qCeil(viewport()->height() * ratio));
|
||||
if (m_buffer.size() != size || m_buffer.devicePixelRatio() != ratio)
|
||||
{
|
||||
m_buffer = QImage(size, QImage::Format_RGB32);
|
||||
m_buffer = QImage(size, QImage::Format_ARGB32_Premultiplied);
|
||||
m_buffer.setDevicePixelRatio(ratio);
|
||||
}
|
||||
|
||||
@@ -141,9 +143,13 @@ void PaletteGraphicsView::paintInverted(QPaintEvent *event)
|
||||
m_buffer_painter.setClipRect(exposed);
|
||||
m_buffer_painter.setRenderHints(renderHints());
|
||||
m_buffer_painter.setWorldTransform(viewportTransform());
|
||||
m_exposed = exposed;
|
||||
m_layered = false;
|
||||
DarkImageRendering::Context context{&m_buffer_painter, !customBackgroundColor(), [this](bool invert, const QRect &bounds) { flushLayer(invert, bounds); }};
|
||||
DarkImageRendering::Scope rasterScope(&context);
|
||||
|
||||
m_inverting = true;
|
||||
paintingInverted(true);
|
||||
paintingInverted(!customBackgroundColor());
|
||||
const OptimizationFlags flags = optimizationFlags();
|
||||
setOptimizationFlag(QGraphicsView::IndirectPainting, true);
|
||||
QGraphicsView::paintEvent(event);
|
||||
@@ -151,10 +157,44 @@ void PaletteGraphicsView::paintInverted(QPaintEvent *event)
|
||||
paintingInverted(false);
|
||||
m_inverting = false;
|
||||
|
||||
if (m_layered) flushLayer(!customBackgroundColor());
|
||||
m_buffer_painter.end();
|
||||
blitInverted(exposed);
|
||||
}
|
||||
|
||||
void PaletteGraphicsView::flushLayer(bool invert, const QRect &bounds)
|
||||
{
|
||||
const QRect area = bounds.isNull() ? m_exposed : bounds.intersected(m_exposed);
|
||||
if (area.isEmpty()) return;
|
||||
const qreal ratio = m_buffer.devicePixelRatio();
|
||||
const QRect pixels = QRectF(area.topLeft() * ratio, area.size() * ratio).toAlignedRect();
|
||||
QImage layer = m_buffer.copy(pixels);
|
||||
layer.setDevicePixelRatio(ratio);
|
||||
if (invert) {
|
||||
const QPalette palette = QApplication::palette();
|
||||
QET::Palette::invertLightnessLayer(layer, palette.color(QPalette::Base), palette.color(QPalette::Text));
|
||||
}
|
||||
if (!m_layered) {
|
||||
if (m_composite.size() != m_buffer.size() || m_composite.devicePixelRatio() != ratio) {
|
||||
m_composite = QImage(m_buffer.size(), QImage::Format_ARGB32_Premultiplied);
|
||||
m_composite.setDevicePixelRatio(ratio);
|
||||
}
|
||||
m_composite.fill(Qt::transparent);
|
||||
m_layered = true;
|
||||
}
|
||||
QPainter composite(&m_composite);
|
||||
composite.drawImage(QPointF(pixels.topLeft()) / ratio, layer);
|
||||
// Keep Qt's active painter and its saved state stack intact. Replacing
|
||||
// or restarting it here would lose ancestor clipping and opacity.
|
||||
m_buffer_painter.save();
|
||||
m_buffer_painter.resetTransform();
|
||||
m_buffer_painter.setClipping(false);
|
||||
m_buffer_painter.setOpacity(1.0);
|
||||
m_buffer_painter.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
m_buffer_painter.fillRect(QRectF(QPointF(pixels.topLeft()) / ratio, QSizeF(pixels.size()) / ratio), Qt::transparent);
|
||||
m_buffer_painter.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief PaletteGraphicsView::drawBackground
|
||||
Into the off-screen image while painting inverted, else as
|
||||
@@ -199,15 +239,16 @@ void PaletteGraphicsView::drawForeground(QPainter *painter, const QRectF &rect)
|
||||
void PaletteGraphicsView::blitInverted(const QRect &area)
|
||||
{
|
||||
const qreal ratio = m_buffer.devicePixelRatio();
|
||||
QImage part = m_buffer.copy(QRectF(area.topLeft() * ratio, area.size() * ratio).toAlignedRect());
|
||||
const QRect pixels = QRectF(area.topLeft() * ratio, area.size() * ratio).toAlignedRect();
|
||||
QImage part = (m_layered ? m_composite : m_buffer).copy(pixels);
|
||||
part.setDevicePixelRatio(ratio);
|
||||
// The application palette, for the reason given in invertsLightness().
|
||||
const QPalette application_palette = QApplication::palette();
|
||||
QET::Palette::invertLightness(part, application_palette.color(QPalette::Base),
|
||||
if (!m_layered && !customBackgroundColor()) QET::Palette::invertLightness(part, application_palette.color(QPalette::Base),
|
||||
application_palette.color(QPalette::Text));
|
||||
|
||||
QPainter painter(viewport());
|
||||
painter.drawImage(area.topLeft(), part);
|
||||
painter.drawImage(QPointF(pixels.topLeft()) / ratio, part);
|
||||
drawRubberBand(painter);
|
||||
}
|
||||
|
||||
|
||||
@@ -90,11 +90,15 @@ class PaletteGraphicsView : public QGraphicsView
|
||||
void paintInverted(QPaintEvent *event);
|
||||
void blitInverted(const QRect &area);
|
||||
void drawRubberBand(QPainter &painter);
|
||||
void flushLayer(bool invert, const QRect &bounds = QRect());
|
||||
|
||||
/// The off-screen image the hooks paint into while m_inverting:
|
||||
/// the viewport's size, in its coordinates. Kept between paints,
|
||||
/// dropped when the view paints on a light palette again.
|
||||
QImage m_buffer;
|
||||
QImage m_composite;
|
||||
QRect m_exposed;
|
||||
bool m_layered = false;
|
||||
QPainter m_buffer_painter;
|
||||
/// True while paintEvent() paints for an inverted display.
|
||||
bool m_inverting = false;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -83,8 +83,14 @@ bool QET::Palette::isDark(const QPalette &palette)
|
||||
void QET::Palette::invertLightness(QImage &image, const QColor &sheet,
|
||||
const QColor &ink)
|
||||
{
|
||||
if (image.format() != QImage::Format_RGB32)
|
||||
image.convertTo(QImage::Format_RGB32);
|
||||
if (image.format() != QImage::Format_RGB32) image.convertTo(QImage::Format_RGB32);
|
||||
invertLightnessLayer(image, sheet, ink);
|
||||
}
|
||||
|
||||
void QET::Palette::invertLightnessLayer(QImage &image, const QColor &sheet, const QColor &ink)
|
||||
{
|
||||
if (image.format() != QImage::Format_RGB32 && image.format() != QImage::Format_ARGB32)
|
||||
image.convertTo(QImage::Format_ARGB32);
|
||||
|
||||
// One table per channel maps the inverted value (0 = was white,
|
||||
// 255 = was black) onto the sheet..ink span.
|
||||
@@ -106,6 +112,7 @@ void QET::Palette::invertLightness(QImage &image, const QColor &sheet,
|
||||
quint32 *line = reinterpret_cast<quint32 *>(image.scanLine(y));
|
||||
for (int x = 0, width = image.width(); x < width; ++x) {
|
||||
const quint32 pixel = line[x];
|
||||
if ((pixel & 0xff000000u) == 0) continue;
|
||||
const int red = (pixel >> 16) & 0xff;
|
||||
const int green = (pixel >> 8) & 0xff;
|
||||
const int blue = pixel & 0xff;
|
||||
@@ -114,7 +121,7 @@ void QET::Palette::invertLightness(QImage &image, const QColor &sheet,
|
||||
if (blue > highest) highest = blue;
|
||||
if (blue < lowest) lowest = blue;
|
||||
const int offset = 255 - highest - lowest;
|
||||
line[x] = 0xff000000u
|
||||
line[x] = (pixel & 0xff000000u)
|
||||
| (quint32(red_of[red + offset]) << 16)
|
||||
| (quint32(green_of[green + offset]) << 8)
|
||||
| quint32(blue_of[blue + offset]);
|
||||
|
||||
@@ -66,6 +66,9 @@ namespace QET {
|
||||
*/
|
||||
void invertLightness(QImage &image, const QColor &sheet = Qt::black,
|
||||
const QColor &ink = Qt::white);
|
||||
/// The same colour mapping for a transparent composition layer.
|
||||
/// Retains alpha; premultiplied input is converted to straight ARGB32.
|
||||
void invertLightnessLayer(QImage &image, const QColor &sheet, const QColor &ink);
|
||||
|
||||
/**
|
||||
The color of the grid dots on a sheet of color \a sheet: black,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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."));
|
||||
}
|
||||
@@ -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 ¤t)
|
||||
{
|
||||
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 ¤t);
|
||||
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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
#include "../qetgraphicsitem/diagramimageitem.h"
|
||||
#include "../qeticons.h"
|
||||
#include "../ui_imagepropertieswidget.h"
|
||||
#include <QSignalBlocker>
|
||||
#include <QDialog>
|
||||
#include <QEvent>
|
||||
|
||||
/**
|
||||
@brief ImagePropertiesWidget::ImagePropertiesWidget
|
||||
@@ -53,6 +56,19 @@ ImagePropertiesWidget::~ImagePropertiesWidget()
|
||||
delete ui;
|
||||
}
|
||||
|
||||
bool ImagePropertiesWidget::event(QEvent *event)
|
||||
{
|
||||
// The generic properties dialog resets on its Cancel button, but its
|
||||
// title-bar close and Escape reach reject() directly. Roll back the
|
||||
// same preview in all rejection paths, without changing other editors.
|
||||
if (event->type() == QEvent::ParentChange) {
|
||||
if (auto *dialog = qobject_cast<QDialog *>(parentWidget())) {
|
||||
connect(dialog, &QDialog::rejected, this, &ImagePropertiesWidget::reset, Qt::UniqueConnection);
|
||||
}
|
||||
}
|
||||
return PropertiesEditorWidget::event(event);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ImagePropertiesWidget::setImageItem
|
||||
Set the image to edit properties
|
||||
@@ -67,12 +83,15 @@ void ImagePropertiesWidget::setImageItem(DiagramImageItem *image)
|
||||
{
|
||||
disconnect(m_image, &DiagramImageItem::transformChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
disconnect(m_image, &DiagramImageItem::labelChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
disconnect(m_image, &DiagramImageItem::adaptToDarkThemeChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
}
|
||||
|
||||
m_image = image;
|
||||
connect(m_image, &DiagramImageItem::transformChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
connect(m_image, &DiagramImageItem::labelChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
connect(m_image, &DiagramImageItem::adaptToDarkThemeChanged, this, &ImagePropertiesWidget::updateUi);
|
||||
m_movable = image->isMovable();
|
||||
m_adapt_to_dark_theme = image->adaptToDarkTheme();
|
||||
m_scaleX = m_image->scaleFactorX();
|
||||
m_scaleY = m_image->scaleFactorY();
|
||||
m_rotation = m_image->rotationAngle();
|
||||
@@ -107,6 +126,7 @@ void ImagePropertiesWidget::apply()
|
||||
m_skewX = m_image->skewX();
|
||||
m_skewY = m_image->skewY();
|
||||
m_label = m_image->label();
|
||||
m_adapt_to_dark_theme = m_image->adaptToDarkTheme();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,6 +144,9 @@ void ImagePropertiesWidget::reset()
|
||||
m_image->setSkewY(m_skewY);
|
||||
m_image->setLabel(m_label);
|
||||
m_image->setMovable(m_movable);
|
||||
m_preview_dark_theme = true;
|
||||
m_image->setAdaptToDarkTheme(m_adapt_to_dark_theme);
|
||||
m_preview_dark_theme = false;
|
||||
updateUi();
|
||||
}
|
||||
|
||||
@@ -146,6 +169,7 @@ bool ImagePropertiesWidget::setLiveEdit(bool live_edit)
|
||||
connect (ui->m_skew_x_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
connect (ui->m_skew_y_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
connect (ui->m_label_le, &QLineEdit::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
connect (ui->m_adapt_to_dark_theme_cb, &QCheckBox::clicked, this, &ImagePropertiesWidget::apply);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -155,6 +179,7 @@ bool ImagePropertiesWidget::setLiveEdit(bool live_edit)
|
||||
disconnect (ui->m_skew_x_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
disconnect (ui->m_skew_y_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
disconnect (ui->m_label_le, &QLineEdit::editingFinished, this, &ImagePropertiesWidget::apply);
|
||||
disconnect (ui->m_adapt_to_dark_theme_cb, &QCheckBox::clicked, this, &ImagePropertiesWidget::apply);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -179,7 +204,7 @@ QUndoCommand* ImagePropertiesWidget::associatedUndo() const
|
||||
const qreal newSkewX = ui->m_skew_x_sb->value();
|
||||
const qreal newSkewY = ui->m_skew_y_sb->value();
|
||||
|
||||
QPropertyUndoCommand *undo = nullptr;
|
||||
QUndoCommand *undo = nullptr;
|
||||
auto chain = [&](const char *property, qreal oldValue, qreal newValue, const QString &text)
|
||||
{
|
||||
if (qFuzzyCompare(oldValue, newValue))
|
||||
@@ -188,9 +213,10 @@ QUndoCommand* ImagePropertiesWidget::associatedUndo() const
|
||||
new QPropertyUndoCommand(m_image, property, oldValue, newValue, undo);
|
||||
else
|
||||
{
|
||||
undo = new QPropertyUndoCommand(m_image, property, oldValue, newValue);
|
||||
undo->enableAnimation();
|
||||
undo->setText(text);
|
||||
auto *change = new QPropertyUndoCommand(m_image, property, oldValue, newValue);
|
||||
change->enableAnimation();
|
||||
change->setText(text);
|
||||
undo = change;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -213,6 +239,14 @@ QUndoCommand* ImagePropertiesWidget::associatedUndo() const
|
||||
}
|
||||
}
|
||||
|
||||
const bool adapt = ui->m_adapt_to_dark_theme_cb->isChecked();
|
||||
if (adapt != m_adapt_to_dark_theme) {
|
||||
if (undo) new QPropertyUndoCommand(m_image, "adaptToDarkTheme", m_adapt_to_dark_theme, adapt, undo);
|
||||
else {
|
||||
undo = new QUndoCommand(tr("Modifier l'adaptation d'une image au thème sombre"));
|
||||
new QPropertyUndoCommand(m_image, "adaptToDarkTheme", m_adapt_to_dark_theme, adapt, undo);
|
||||
}
|
||||
}
|
||||
return undo;
|
||||
}
|
||||
|
||||
@@ -233,6 +267,9 @@ void ImagePropertiesWidget::updateUi()
|
||||
// signal from a resize handle used directly on the canvas while
|
||||
// this dialog is open).
|
||||
if (!m_image || m_updating_ratio) return;
|
||||
if (m_live_edit && !m_preview_dark_theme) m_adapt_to_dark_theme = m_image->adaptToDarkTheme();
|
||||
const QSignalBlocker darkThemeBlocker(ui->m_adapt_to_dark_theme_cb);
|
||||
ui->m_adapt_to_dark_theme_cb->setChecked(m_image->adaptToDarkTheme());
|
||||
|
||||
m_updating_ratio = true;
|
||||
ui->m_width_sb->setValue(m_image->scaleFactorX() * 100.0);
|
||||
@@ -395,3 +432,11 @@ void ImagePropertiesWidget::on_m_lock_pos_cb_clicked()
|
||||
{
|
||||
m_image->setMovable(!ui->m_lock_pos_cb->isChecked());
|
||||
}
|
||||
|
||||
void ImagePropertiesWidget::on_m_adapt_to_dark_theme_cb_toggled(bool checked)
|
||||
{
|
||||
if (!m_image) return;
|
||||
m_preview_dark_theme = true;
|
||||
m_image->setAdaptToDarkTheme(checked);
|
||||
m_preview_dark_theme = false;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ class ImagePropertiesWidget : public PropertiesEditorWidget
|
||||
void reset() override;
|
||||
bool setLiveEdit(bool live_edit) override;
|
||||
QUndoCommand* associatedUndo() const override;
|
||||
protected:
|
||||
bool event(QEvent *event) override;
|
||||
|
||||
private:
|
||||
void updateUi() override;
|
||||
@@ -56,11 +58,14 @@ class ImagePropertiesWidget : public PropertiesEditorWidget
|
||||
void on_m_lock_ratio_tb_toggled(bool checked);
|
||||
void on_m_restore_ratio_pb_clicked();
|
||||
void on_m_lock_pos_cb_clicked();
|
||||
void on_m_adapt_to_dark_theme_cb_toggled(bool checked);
|
||||
|
||||
private:
|
||||
Ui::ImagePropertiesWidget *ui;
|
||||
DiagramImageItem *m_image;
|
||||
bool m_movable;
|
||||
bool m_adapt_to_dark_theme = false;
|
||||
bool m_preview_dark_theme = false;
|
||||
// All tracked independently -- the image may already be
|
||||
// non-uniformly scaled, rotated, and/or skewed via the resize/
|
||||
// rotate/skew handles before this dialog is even opened, and
|
||||
|
||||
@@ -173,7 +173,17 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<item row="8" column="0" colspan="3">
|
||||
<widget class="QCheckBox" name="m_adapt_to_dark_theme_cb">
|
||||
<property name="text">
|
||||
<string>Adapter l’image au thème sombre</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Adapte les couleurs à l’affichage sombre uniquement. L’image originale, les exports et les impressions restent inchangés.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Link the real image/widget integration probe using an existing Ninja app build.
|
||||
|
||||
Reuses app objects without duplicating the app or introducing a core-library
|
||||
refactor. No application source or existing executable is replaced. The
|
||||
generated manifest, probe binary, reports and export fixtures stay in build/.
|
||||
"""
|
||||
import argparse
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--build', required=True)
|
||||
parser.add_argument('--binary', required=True)
|
||||
parser.add_argument('--source', required=True)
|
||||
parser.add_argument('--fixture', required=True)
|
||||
args = parser.parse_args()
|
||||
build = pathlib.Path(args.build).resolve()
|
||||
binary = pathlib.Path(args.binary).resolve()
|
||||
source = pathlib.Path(args.source).resolve()
|
||||
fixture = pathlib.Path(args.fixture).resolve()
|
||||
manifest = (build / 'build.ninja').read_text(encoding='utf-8')
|
||||
match = re.search(r'^build (\S*?/sources/main\.cpp\.(?:obj|o)):', manifest, re.MULTILINE)
|
||||
if not match:
|
||||
sys.exit('Cannot find the application main object in the Ninja build.')
|
||||
main_object = match.group(1)
|
||||
probe_object = main_object.replace('main.cpp.', 'image_dark_integration_probe.cpp.')
|
||||
probe_binary = binary.with_name('image_dark_integration_probe' + binary.suffix)
|
||||
def escape(path):
|
||||
return pathlib.Path(path).as_posix().replace('$', '$$').replace(':', '$:').replace(' ', '$ ')
|
||||
main_source = source.parents[2] / 'sources' / 'main.cpp'
|
||||
manifest = manifest.replace(main_object, probe_object)
|
||||
manifest = manifest.replace(escape(main_source), escape(source))
|
||||
if binary.suffix:
|
||||
manifest = manifest.replace(binary.name, probe_binary.name)
|
||||
else:
|
||||
old_target = escape(binary.name if binary.parent == build else binary)
|
||||
new_target = escape(probe_binary.name if probe_binary.parent == build else probe_binary)
|
||||
manifest = manifest.replace('build ' + old_target + ':', 'build ' + new_target + ':')
|
||||
old_file = binary.name if binary.parent == build else binary.as_posix()
|
||||
new_file = probe_binary.name if probe_binary.parent == build else probe_binary.as_posix()
|
||||
manifest = re.sub(r'^ TARGET_FILE = ' + re.escape(old_file) + r'$',
|
||||
' TARGET_FILE = ' + new_file, manifest, flags=re.MULTILINE)
|
||||
probe_manifest = build / 'image-dark-probe.ninja'
|
||||
probe_manifest.write_text(manifest, encoding='utf-8')
|
||||
ninja = shutil.which('ninja')
|
||||
if not ninja:
|
||||
sys.exit('Ninja is required for the integration probe.')
|
||||
target = probe_binary.name if probe_binary.parent == build else str(probe_binary)
|
||||
subprocess.run([ninja, '-C', str(build), '-f', probe_manifest.name, target], check=True)
|
||||
output = build / 'image-dark-integration'
|
||||
output.mkdir(exist_ok=True)
|
||||
report = output / 'results.txt'
|
||||
result = subprocess.run([str(probe_binary), str(fixture), str(report), str(output)], timeout=90)
|
||||
if report.exists():
|
||||
print(report.read_text(encoding='utf-8', errors='replace'))
|
||||
if result.returncode == 0:
|
||||
def print_streams(path):
|
||||
streams = re.findall(rb'\nstream\r?\n(.*?)\nendstream', path.read_bytes(), re.DOTALL)
|
||||
decoded = []
|
||||
for stream in streams:
|
||||
try:
|
||||
data = zlib.decompress(stream)
|
||||
except zlib.error:
|
||||
data = stream
|
||||
# Qt assigns every PDF a fresh XMP DocumentID (and dates).
|
||||
# Metadata is not printing content and must not be compared
|
||||
# as if it were a graphics/image/font stream.
|
||||
if b'<x:xmpmeta' not in data:
|
||||
decoded.append(data)
|
||||
return decoded
|
||||
first = print_streams(output / 'print-0.pdf')
|
||||
second = print_streams(output / 'print-1.pdf')
|
||||
if not first or first != second:
|
||||
sys.exit('PDF printing changed the decoded graphics/image/font streams.')
|
||||
print('PASS: PDF print graphics/image/font streams identical for both choices.')
|
||||
try:
|
||||
import pymupdf
|
||||
except ImportError:
|
||||
print('PDF raster comparison not run: optional PyMuPDF is unavailable.')
|
||||
else:
|
||||
documents = [pymupdf.open(output / f'print-{i}.pdf') for i in range(2)]
|
||||
if len(documents[0]) != len(documents[1]):
|
||||
sys.exit('PDF page counts differ.')
|
||||
for page in range(len(documents[0])):
|
||||
pixels = [doc[page].get_pixmap(dpi=144, alpha=False) for doc in documents]
|
||||
if (pixels[0].width, pixels[0].height, pixels[0].samples) != (pixels[1].width, pixels[1].height, pixels[1].samples):
|
||||
sys.exit('Rasterized PDF print pages differ.')
|
||||
for i, image in enumerate(pixels):
|
||||
image.save(output / f'print-{i}-page-{page}.png')
|
||||
print('PASS: PDF print pages rendered at 144 dpi are pixel-identical.')
|
||||
sys.exit(result.returncode)
|
||||
@@ -0,0 +1,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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user