Merge branch 'master' into Replace-automatic-conductors

This commit is contained in:
Laurent Trinques
2026-08-07 14:58:42 +02:00
committed by GitHub
101 changed files with 31833 additions and 21710 deletions
@@ -22,7 +22,39 @@
#include "../qeticons.h"
#include "elementslocation.h"
#include <QApplication>
#include <QDir>
#include <QPainter>
#include <QPixmap>
#include <QStyle>
namespace {
/**
@return the folder icon overlaid with a small warning badge in the
bottom-right corner. Used for a directory whose qet_directory could
not be read (@see FileElementCollectionItem::m_qet_directory_unreadable),
so the problem is visible in the tree itself and not only on hover
via the tooltip. Built once: same folder icon, same badge, every time.
*/
const QIcon &unreadableFolderIcon()
{
static const QIcon icon = []() {
QPixmap pixmap = QET::Icons::Folder.pixmap(16, 16);
const QPixmap badge = QApplication::style()
->standardIcon(QStyle::SP_MessageBoxWarning)
.pixmap(9, 9);
QPainter painter(&pixmap);
painter.drawPixmap(pixmap.width() - badge.width(),
pixmap.height() - badge.height(),
badge);
painter.end();
return QIcon(pixmap);
}();
return icon;
}
}
/**
@brief FileElementCollectionItem::FileElementCollectionItem
@@ -136,18 +168,41 @@ QString FileElementCollectionItem::localName()
}
else
{
// Fall back to the raw directory name (m_path) whenever the
// translated name can't be obtained -- qet_directory missing,
// unreadable (e.g. a Windows path-encoding issue with special
// characters, see bugtracker #332), malformed, or present but
// without a usable name entry -- rather than leaving the item
// blank.
QString display_name;
bool readable = false;
QString str(fileSystemPath() % "/qet_directory");
pugi::xml_document docu;
if(docu.load_file(str.toStdWString().c_str()))
if (docu.load_file(str.toStdWString().c_str()))
{
if (QString(docu.document_element().name())
== "qet-directory")
{
readable = true;
NamesList nl;
nl.fromXml(docu.document_element());
setText(nl.name());
// Deliberately no fallback argument: a non-empty one
// is returned *before* NamesList::name() reaches its
// "first available translation" step, so passing
// m_path here would replace a perfectly good name in
// some other language with the raw directory name.
// The fallback belongs after the chain, not inside it.
display_name = nl.name();
}
}
setText(display_name.isEmpty() ? m_path : display_name);
// Only a file-level failure counts: a readable qet-directory
// with no entry for the current language is not an error,
// NamesList::name() resolves that on its own. Recorded here
// and reported by setUpData(), which sets the tooltip after
// this runs.
m_qet_directory_unreadable = !readable;
}
}
else if (isElement()) {
@@ -350,7 +405,21 @@ void FileElementCollectionItem::setUpData()
}
}
setToolTip(collectionPath());
// Falling back to the raw directory name keeps the folder usable, but
// on its own it hides the fact that a file is broken: the user sees a
// plausible name and never learns there is anything to repair. Say so
// above the collection path, which stays as the last line the way the
// element tooltip above builds it.
QStringList tip;
if (isDir() && m_qet_directory_unreadable)
{
tip << QObject::tr("Le fichier « %1 » est absent ou illisible : "
"le nom traduit de ce dossier n'a pas pu être lu, "
"son nom de dossier est affiché à la place.")
.arg(fileSystemPath() % "/qet_directory");
}
tip << collectionPath();
setToolTip(tip.join(QLatin1Char('\n')));
}
/**
@@ -361,6 +430,19 @@ void FileElementCollectionItem::setUpData()
*/
void FileElementCollectionItem::setUpIcon()
{
// Must return unconditionally once an icon is set: setIcon() calls
// setData(), which emits dataChanged() regardless of whether the new
// icon differs from the old one (QIcon has no meaningful equality).
// QTreeView responds to dataChanged() by recomputing the row's size
// hint, which re-enters data() for this same index -- so without this
// guard, any repeated setIcon() here recurses until the stack
// overflows. Confirmed by crash report on PR #633.
//
// This item's m_qet_directory_unreadable is already final by the time
// this can run at all: ElementsCollectionModel only attaches itself
// to the tree view (making data() reachable) from loadingFinished(),
// which fires after the QtConcurrent::map over every item -- this one
// included -- has completed. So there is no race to work around here.
if (!icon().isNull())
return;
@@ -380,7 +462,8 @@ void FileElementCollectionItem::setUpIcon()
else
{
if (isDir()) {
setIcon(QET::Icons::Folder);
setIcon(m_qet_directory_unreadable ? unreadableFolderIcon()
: QET::Icons::Folder);
} else {
if (m_path.endsWith(".qetmak")) {
setIcon(QIcon());
@@ -64,6 +64,11 @@ class FileElementCollectionItem : public ElementCollectionItem
private:
QString m_path;
/// True when this directory's qet_directory file is missing or
/// unreadable, so setUpData() can say so in the tooltip. Recorded
/// rather than acted on in localName(), because setUpData() resets
/// the tooltip afterwards and would otherwise discard it.
bool m_qet_directory_unreadable = false;
};
#endif // FILEELEMENTCOLLECTIONITEM2_H
+46 -8
View File
@@ -39,6 +39,7 @@ namespace autonum
sequentialNumbers::sequentialNumbers(const sequentialNumbers &other)
{
unit = other.unit;
wrap = other.wrap;
unit_folio = other.unit_folio;
ten = other.ten;
ten_folio = other.ten_folio;
@@ -57,6 +58,7 @@ namespace autonum
return (*this);
unit = other.unit;
wrap = other.wrap;
unit_folio = other.unit_folio;
ten = other.ten;
ten_folio = other.ten_folio;
@@ -70,6 +72,7 @@ namespace autonum
bool sequentialNumbers::operator==(const sequentialNumbers &other) const
{
if (unit == other.unit && \
wrap == other.wrap && \
unit_folio == other.unit_folio && \
ten == other.ten && \
ten_folio == other.ten_folio && \
@@ -107,6 +110,11 @@ namespace autonum
document,
"unit",
unit.join(";")));
if (!wrap.isEmpty())
element.appendChild(QETXML::textToDomElement(
document,
"wrap",
wrap.join(";")));
if (!unit_folio.isEmpty())
element.appendChild(QETXML::textToDomElement(
document,
@@ -156,6 +164,11 @@ namespace autonum
from = element.firstChildElement("unit");
unit = from.text().split(";");
//Absent from files written before cyclic parts could be
//rendered; an empty list is the correct reading of that.
from = element.firstChildElement("wrap");
wrap = from.text().split(";");
from = element.firstChildElement("unitFolio");
unit_folio = from.text().split(";");
@@ -179,6 +192,7 @@ namespace autonum
void sequentialNumbers::clear()
{
unit.clear();
wrap.clear();
unit_folio.clear();
ten.clear();
ten_folio.clear();
@@ -434,7 +448,8 @@ namespace autonum
qMax(
qMax(m_seq_struct.hundred.size(),
m_seq_struct.ten.size()),
m_seq_struct.alpha.size())
qMax(m_seq_struct.alpha.size(),
m_seq_struct.wrap.size()))
);
for (int i=1; i<=max ; i++)
@@ -442,6 +457,9 @@ namespace autonum
if (m_assigned_label.contains("%sequ_" + QString::number(i)) && m_seq_struct.unit.size() >= i) {
m_assigned_label.replace("%sequ_" + QString::number(i),m_seq_struct.unit.at(i-1));
}
if (m_assigned_label.contains("%seqw_" + QString::number(i)) && m_seq_struct.wrap.size() >= i) {
m_assigned_label.replace("%seqw_" + QString::number(i),m_seq_struct.wrap.at(i-1));
}
if (m_assigned_label.contains("%seqt_" + QString::number(i)) && m_seq_struct.ten.size() >= i) {
m_assigned_label.replace("%seqt_" + QString::number(i),m_seq_struct.ten.at(i-1));
}
@@ -479,15 +497,26 @@ namespace autonum
{
if (context.itemAt(i).at(0) == type)
{
const QStringList item = context.itemAt(i);
//A zero-padding mask, spreadsheet style: its length is the
//minimum number of digits. It overrides the width implied
//by the part type, so "Chiffre 01" with a mask of "0000"
//pads to four. An absent mask -- which is every context
//written before the field existed -- falls through to the
//type's own width, so nothing about existing projects
//changes.
const QString mask = NumerotationContext::formatOf(item);
QString number;
if (type == "ten" || type == "tenfolio")
number = QString("%1").arg(context.itemAt(i).at(1).toInt(), 2, 10, QChar('0'));
else if (type == "hundred" || type == "hundredfolio")
number = QString("%1").arg(context.itemAt(i).at(1).toInt(), 3, 10, QChar('0'));
else if (type == "alpha")
if (type == "alpha")
//Alphabetic value, not an integer -- used as-is.
number = context.itemAt(i).at(1);
else number = QString::number(context.itemAt(i).at(1).toInt());
number = item.at(1);
else if (!mask.isEmpty())
number = QString("%1").arg(item.at(1).toInt(), mask.length(), 10, QChar('0'));
else if (type == "ten" || type == "tenfolio")
number = QString("%1").arg(item.at(1).toInt(), 2, 10, QChar('0'));
else if (type == "hundred" || type == "hundredfolio")
number = QString("%1").arg(item.at(1).toInt(), 3, 10, QChar('0'));
else number = QString::number(item.at(1).toInt());
list.append(number);
}
}
@@ -553,6 +582,10 @@ namespace autonum
{
autonum::setSequentialToList(seqStruct.unit, context,"unit");
}
if (label.contains("%seqw_"))
{
autonum::setSequentialToList(seqStruct.wrap, context,"wrap");
}
if (label.contains("%sequf_"))
{
autonum::setSequentialToList(seqStruct.unit_folio, context,"unitfolio");
@@ -594,6 +627,7 @@ namespace autonum
QString value;
QString formula;
int count_unit = 0;
int count_wrap = 0;
int count_unitf = 0;
int count_ten = 0;
int count_tenf = 0;
@@ -636,6 +670,10 @@ namespace autonum
count_unit++;
formula.append("%sequ_" + QString::number(count_unit));
}
else if (type == "wrap") {
count_wrap++;
formula.append("%seqw_" + QString::number(count_wrap));
}
else if (type == "unitfolio") {
count_unitf++;
formula.append("%sequf_" + QString::number(count_unitf));
+2
View File
@@ -47,6 +47,8 @@ namespace autonum
void clear();
QStringList unit;
/// Values of the cyclic (modulo) parts, referenced by %seqw_N.
QStringList wrap;
QStringList unit_folio;
QStringList ten;
QStringList ten_folio;
+26 -4
View File
@@ -52,13 +52,18 @@ void NumerotationContext::clear ()
@param increase the increase number of value
@param initialvalue
@param modulus wrap-and-carry modulus (0 means "not a wrapping part")
@param format zero-padding mask, spreadsheet style: "00" pads to two
digits, "000" to three. Empty keeps the part type's natural width, so
an absent format reproduces exactly the behaviour of every context
written before this field existed.
@return true if value is append
*/
bool NumerotationContext::addValue(const QString &type,
const QVariant &value,
const int increase,
const int initialvalue,
const int modulus) {
const int modulus,
const QString &format) {
if (!keyIsAcceptable(type) && !value.canConvert<QString>())
return false;
if (keyIsNumber(type) && !value.canConvert<int>())
@@ -74,7 +79,9 @@ bool NumerotationContext::addValue(const QString &type,
+ "|"
+ QString::number(initialvalue)
+ "|"
+ QString::number(modulus);
+ QString::number(modulus)
+ "|"
+ QString(format).remove("|");
return true;
}
@@ -179,6 +186,9 @@ QDomElement NumerotationContext::toXml(QDomDocument &d, const QString& str) {
if (strl.at(0) == ("wrap") && strl.size() > 4) {
part.setAttribute("modulus", strl.at(4));
}
if (strl.size() > 5 && !strl.at(5).isEmpty()) {
part.setAttribute("format", strl.at(5));
}
num_auto.appendChild(part);
}
return num_auto;
@@ -190,7 +200,7 @@ QDomElement NumerotationContext::toXml(QDomDocument &d, const QString& str) {
*/
void NumerotationContext::fromXml(QDomElement &e) {
clear();
foreach(QDomElement qde, QET::findInDomElement(e, "part")) addValue(qde.attribute("type"), qde.attribute("value"), qde.attribute("increase").toInt(), qde.attribute("initialvalue").toInt(), qde.attribute("modulus").toInt());
foreach(QDomElement qde, QET::findInDomElement(e, "part")) addValue(qde.attribute("type"), qde.attribute("value"), qde.attribute("increase").toInt(), qde.attribute("initialvalue").toInt(), qde.attribute("modulus").toInt(), qde.attribute("format"));
}
/**
@@ -206,5 +216,17 @@ void NumerotationContext::replaceValue(int index, QString content) {
QString increase = strl.at(2);
QString initvalue = strl.at(3);
QString modulus = strl.size() > 4 ? strl.at(4) : QStringLiteral("0");
content_[index] = type + "|" + value + "|" + increase + "|" + initvalue + "|" + modulus;
QString format = strl.size() > 5 ? strl.at(5) : QString();
content_[index] = type + "|" + value + "|" + increase + "|" + initvalue + "|" + modulus + "|" + format;
}
/**
@brief NumerotationContext::formatOf
@param item : a context item as returned by itemAt()
@return the part's zero-padding mask, or an empty string when it has
none -- which every context written before the field existed will be.
*/
QString NumerotationContext::formatOf(const QStringList &item)
{
return item.size() > 5 ? item.at(5) : QString();
}
+5 -1
View File
@@ -37,7 +37,11 @@ class NumerotationContext
const QVariant & = QVariant(1),
const int = 1,
const int = 0,
const int = 0);
const int = 0,
const QString & = QString());
/// Zero-padding mask of a part, e.g. "00"; empty means the type's
/// own natural width. See addValue().
static QString formatOf(const QStringList &item);
QString operator[] (const int &) const;
void operator << (const NumerotationContext &);
int size() const;
@@ -259,7 +259,7 @@ NumerotationContext NumStrategy::nextString (const NumerotationContext &nc,
{
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
newnc.addValue(strl.at(0), strl.at(1), strl.at(2).toInt());
newnc.addValue(strl.at(0), strl.at(1), strl.at(2).toInt(), 0, 0, NumerotationContext::formatOf(strl));
return (newnc);
}
@@ -273,7 +273,7 @@ NumerotationContext NumStrategy::nextNumber (const NumerotationContext &nc,
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
QString value = QString::number( (strl.at(1).toInt()) + (strl.at(2).toInt()) );
newnc.addValue(strl.at(0), value, strl.at(2).toInt(), strl.at(3).toInt());
newnc.addValue(strl.at(0), value, strl.at(2).toInt(), strl.at(3).toInt(), 0, NumerotationContext::formatOf(strl));
return (newnc);
}
@@ -287,7 +287,7 @@ NumerotationContext NumStrategy::previousNumber(const NumerotationContext &nc,
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
QString value = QString::number( (strl.at(1).toInt()) - (strl.at(2).toInt()) );
newnc.addValue(strl.at(0), value, strl.at(2).toInt(), strl.at(3).toInt());
newnc.addValue(strl.at(0), value, strl.at(2).toInt(), strl.at(3).toInt(), 0, NumerotationContext::formatOf(strl));
return (newnc);
}
@@ -550,7 +550,7 @@ NumerotationContext WrapNum::next (const NumerotationContext &nc, const int i) c
int new_value = strl.at(1).toInt() + increase;
if (modulus > 0)
new_value %= modulus;
newnc.addValue(strl.at(0), QString::number(new_value), increase, strl.at(3).toInt(), modulus);
newnc.addValue(strl.at(0), QString::number(new_value), increase, strl.at(3).toInt(), modulus, NumerotationContext::formatOf(strl));
return (newnc);
}
@@ -570,7 +570,7 @@ NumerotationContext WrapNum::previous(const NumerotationContext &nc, const int i
if (new_value < 0)
new_value += modulus;
}
newnc.addValue(strl.at(0), QString::number(new_value), increase, strl.at(3).toInt(), modulus);
newnc.addValue(strl.at(0), QString::number(new_value), increase, strl.at(3).toInt(), modulus, NumerotationContext::formatOf(strl));
return (newnc);
}
@@ -706,7 +706,7 @@ NumerotationContext AlphaNum::next (const NumerotationContext &nc, const int i)
{
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
newnc.addValue(strl.at(0), incrementAlpha(strl.at(1)), strl.at(2).toInt());
newnc.addValue(strl.at(0), incrementAlpha(strl.at(1)), strl.at(2).toInt(), 0, 0, NumerotationContext::formatOf(strl));
return (newnc);
}
@@ -718,7 +718,7 @@ NumerotationContext AlphaNum::previous(const NumerotationContext &nc, const int
{
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
newnc.addValue(strl.at(0), decrementAlpha(strl.at(1)), strl.at(2).toInt());
newnc.addValue(strl.at(0), decrementAlpha(strl.at(1)), strl.at(2).toInt(), 0, 0, NumerotationContext::formatOf(strl));
return (newnc);
}
@@ -26,6 +26,9 @@
#include "../numerotationcontext.h"
#include "ui_autonumberingdockwidget.h"
#include <QComboBox>
#include <QLineEdit>
/**
@brief AutoNumberingDockWidget::AutoNumberingDockWidget
Constructor
@@ -58,6 +61,9 @@ void AutoNumberingDockWidget::clear()
ui->m_conductor_cb->clear();
ui->m_element_cb->clear();
ui->m_folio_cb->clear();
ui->m_conductor_value_le->clear();
ui->m_element_value_le->clear();
ui->m_folio_value_le->clear();
}
void AutoNumberingDockWidget::projectClosed()
@@ -109,6 +115,8 @@ void AutoNumberingDockWidget::setProject(QETProject *project,
this,SLOT(setActive()));
//Conductor, Element and Folio Signals
disconnect(m_project, &QETProject::autoNumContextUpdated,
this, &AutoNumberingDockWidget::refreshValueFields);
disconnect(m_project, &QETProject::destroyed,
this, &AutoNumberingDockWidget::projectClosed);
}
@@ -146,6 +154,8 @@ void AutoNumberingDockWidget::setProject(QETProject *project,
this,SLOT(setActive()));
//Conductor, Element and Folio Signals
connect(m_project, &QETProject::autoNumContextUpdated,
this, &AutoNumberingDockWidget::refreshValueFields);
connect(m_project, &QETProject::destroyed,
this, &AutoNumberingDockWidget::projectClosed);
@@ -189,6 +199,12 @@ void AutoNumberingDockWidget::setContext()
{ ui->m_folio_cb -> addItem(str);}
}
//The combo boxes have just been repopulated, so the value fields next
//to them are showing whatever the previous project left there.
refreshValueField(ui->m_conductor_cb, ui->m_conductor_value_le, AutoNumCategory::Conductor);
refreshValueField(ui->m_element_cb, ui->m_element_value_le, AutoNumCategory::Element);
refreshValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
this->setActive();
}
@@ -263,6 +279,7 @@ void AutoNumberingDockWidget::on_m_conductor_cb_activated(int)
m_project->setCurrentConductorAutoNum(current_autonum);
m_project_view->currentDiagram()->diagram()->setConductorsAutonumName(current_autonum);
m_project_view->currentDiagram()->diagram()->loadCndFolioSeq();
refreshValueField(ui->m_conductor_cb, ui->m_conductor_value_le, AutoNumCategory::Conductor);
}
/**
@@ -291,6 +308,7 @@ void AutoNumberingDockWidget::on_m_element_cb_activated(int)
{
m_project->setCurrrentElementAutonum(ui->m_element_cb->currentText());
m_project_view->currentDiagram()->diagram()->loadElmtFolioSeq();
refreshValueField(ui->m_element_cb, ui->m_element_value_le, AutoNumCategory::Element);
}
/**
@@ -328,6 +346,7 @@ void AutoNumberingDockWidget::on_m_folio_cb_activated(int) {
m_project->setDefaultTitleBlockProperties(ip);
}
emit(folioAutoNumChanged(current_autonum));
refreshValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
}
void AutoNumberingDockWidget::on_m_configure_pb_clicked()
@@ -339,3 +358,209 @@ void AutoNumberingDockWidget::on_m_configure_pb_clicked()
ppd.exec();
}
}
void AutoNumberingDockWidget::on_m_conductor_reset_start_pb_clicked()
{
resetAutoNum(ui->m_conductor_cb, AutoNumCategory::Conductor);
}
void AutoNumberingDockWidget::on_m_element_reset_start_pb_clicked()
{
resetAutoNum(ui->m_element_cb, AutoNumCategory::Element);
}
void AutoNumberingDockWidget::on_m_folio_reset_start_pb_clicked()
{
resetAutoNum(ui->m_folio_cb, AutoNumCategory::Folio);
}
void AutoNumberingDockWidget::on_m_conductor_value_le_editingFinished()
{
applyValueField(ui->m_conductor_cb, ui->m_conductor_value_le, AutoNumCategory::Conductor);
}
void AutoNumberingDockWidget::on_m_element_value_le_editingFinished()
{
applyValueField(ui->m_element_cb, ui->m_element_value_le, AutoNumCategory::Element);
}
void AutoNumberingDockWidget::on_m_folio_value_le_editingFinished()
{
applyValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
}
/**
@brief AutoNumberingDockWidget::contextFor
@return the numerotation context named by combo_box, for category
*/
NumerotationContext AutoNumberingDockWidget::contextFor(QComboBox *combo_box, AutoNumCategory category) const
{
const QString key = combo_box->currentText();
switch (category) {
case AutoNumCategory::Conductor: return m_project->conductorAutoNum(key);
case AutoNumCategory::Element: return m_project->elementAutoNum(key);
case AutoNumCategory::Folio: return m_project->folioAutoNum(key);
}
return NumerotationContext();
}
/**
@brief AutoNumberingDockWidget::storeContext
Write context back under the name selected in combo_box and flag the
project as modified -- without that last step the change is not saved
and the user is never asked to save it on close.
*/
void AutoNumberingDockWidget::storeContext(QComboBox *combo_box, AutoNumCategory category, const NumerotationContext &context)
{
const QString key = combo_box->currentText();
switch (category) {
case AutoNumCategory::Conductor: m_project->addConductorAutoNum(key, context); break;
case AutoNumCategory::Element: m_project->addElementAutoNum(key, context); break;
case AutoNumCategory::Folio: m_project->addFolioAutoNum(key, context); break;
}
m_project->setModified(true);
}
/**
@brief AutoNumberingDockWidget::counterIndex
@return the index of the part the value field shows: the last one that
actually progresses, i.e. the least significant digit of the number.
-1 when the context has no progressing part at all.
*/
int AutoNumberingDockWidget::counterIndex(const NumerotationContext &context)
{
for (int i = context.size() - 1 ; i >= 0 ; --i)
{
const QString type = context.itemAt(i).at(0);
if (type == QLatin1String("unit")
|| type == QLatin1String("ten")
|| type == QLatin1String("hundred")
|| type == QLatin1String("unitfolio")
|| type == QLatin1String("tenfolio")
|| type == QLatin1String("hundredfolio")
|| type == QLatin1String("wrap")
|| type == QLatin1String("alpha"))
return i;
}
return -1;
}
/**
@brief AutoNumberingDockWidget::refreshValueFields
Re-read all three value fields from the project. Called whenever a
numerotation context's values change, which includes every element or
conductor that consumes the next number -- without this the field only
caught up when the user re-picked a rule from the combo box, because
the combo's activated() signal fires on user interaction alone.
*/
void AutoNumberingDockWidget::refreshValueFields()
{
//Leave alone a field the user is typing in: numbering an element
//refreshes all three, and overwriting a half-typed value under the
//cursor is worse than showing it a moment out of date. Only this
//automatic path skips; an explicit refresh after a reset or an edit
//still writes, so the field always ends up canonical.
if (!ui->m_conductor_value_le->hasFocus())
refreshValueField(ui->m_conductor_cb, ui->m_conductor_value_le, AutoNumCategory::Conductor);
if (!ui->m_element_value_le->hasFocus())
refreshValueField(ui->m_element_cb, ui->m_element_value_le, AutoNumCategory::Element);
if (!ui->m_folio_value_le->hasFocus())
refreshValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
}
/**
@brief AutoNumberingDockWidget::refreshValueField
Show the current value of the selected context's counter, so the field
always reflects where the numbering has actually got to.
*/
void AutoNumberingDockWidget::refreshValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category)
{
if (!m_project || combo_box->currentText().isEmpty())
{
line_edit->clear();
line_edit->setEnabled(false);
return;
}
const NumerotationContext context = contextFor(combo_box, category);
const int index = counterIndex(context);
line_edit->setEnabled(index >= 0);
line_edit->setText(index >= 0 ? context.itemAt(index).at(1) : QString());
}
/**
@brief AutoNumberingDockWidget::applyValueField
Write the value typed in line_edit to the counter it displays. An empty
field is treated as "no change" rather than as an empty value, so
clearing the box by accident cannot wipe the counter.
*/
void AutoNumberingDockWidget::applyValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category)
{
if (!m_project || combo_box->currentText().isEmpty())
return;
NumerotationContext context = contextFor(combo_box, category);
const int index = counterIndex(context);
if (index < 0)
return;
const QString typed = line_edit->text();
if (typed.isEmpty() || typed == context.itemAt(index).at(1))
{
refreshValueField(combo_box, line_edit, category);
return;
}
context.replaceValue(index, typed);
storeContext(combo_box, category, context);
refreshValueField(combo_box, line_edit, category);
}
/**
@brief AutoNumberingDockWidget::resetAutoNum
Reset the numerotation context currently selected in combo_box back to
a per-type starting value, then write it back. Does nothing if no
context is selected.
Only parts that actually progress are touched: folio-anchored numeric
types go back to their own stored initialvalue, plain numeric types go
back to "1", a wrap part goes back to "0" because a modulo counter
cycles over [0, modulus) -- a PLC card addressed %IX0.0..%IX0.31 starts
at 0, not 1 -- and alpha goes back to "a". Non-incrementing types
(string, plant, locmach, idfolio, folio, elementline, elementcolumn,
elementprefix) are left alone: there is no "start" for them distinct
from the fixed or contextual value the user configured.
*/
void AutoNumberingDockWidget::resetAutoNum(QComboBox *combo_box, AutoNumCategory category)
{
if (!m_project || combo_box->currentText().isEmpty())
return;
NumerotationContext context = contextFor(combo_box, category);
for (int i = 0 ; i < context.size() ; ++i)
{
const QStringList item = context.itemAt(i);
const QString &type = item.at(0);
if (type == QLatin1String("unitfolio")
|| type == QLatin1String("tenfolio")
|| type == QLatin1String("hundredfolio"))
context.replaceValue(i, item.size() > 3 ? item.at(3) : QStringLiteral("1"));
else if (type == QLatin1String("unit")
|| type == QLatin1String("ten")
|| type == QLatin1String("hundred"))
context.replaceValue(i, QStringLiteral("1"));
else if (type == QLatin1String("wrap"))
context.replaceValue(i, QStringLiteral("0"));
else if (type == QLatin1String("alpha"))
context.replaceValue(i, QStringLiteral("a"));
}
storeContext(combo_box, category, context);
switch (category) {
case AutoNumCategory::Conductor: refreshValueField(combo_box, ui->m_conductor_value_le, category); break;
case AutoNumCategory::Element: refreshValueField(combo_box, ui->m_element_value_le, category); break;
case AutoNumCategory::Folio: refreshValueField(combo_box, ui->m_folio_value_le, category); break;
}
}
+36 -1
View File
@@ -23,6 +23,9 @@
#include <QDockWidget>
class QComboBox;
class QLineEdit;
namespace Ui {
class AutoNumberingDockWidget;
}
@@ -51,13 +54,45 @@ class AutoNumberingDockWidget : public QDockWidget
void folioAutoNumChanged();
void clear();
void projectClosed();
void refreshValueFields();
void on_m_configure_pb_clicked();
void on_m_conductor_reset_start_pb_clicked();
void on_m_element_reset_start_pb_clicked();
void on_m_folio_reset_start_pb_clicked();
void on_m_conductor_value_le_editingFinished();
void on_m_element_value_le_editingFinished();
void on_m_folio_value_le_editingFinished();
signals:
void folioAutoNumChanged(QString);
private:
enum class AutoNumCategory { Conductor, Element, Folio };
/**
@brief resetAutoNum
Reset the numerotation context currently selected in combo_box
(for the given category) to a per-type starting value. Does
nothing if no context is selected.
*/
void resetAutoNum(QComboBox *combo_box, AutoNumCategory category);
/// Read/write the numerotation context named in combo_box.
NumerotationContext contextFor(QComboBox *combo_box, AutoNumCategory category) const;
void storeContext(QComboBox *combo_box, AutoNumCategory category, const NumerotationContext &context);
/// Index of the counter the value field shows and edits: the last
/// part that actually progresses. Returns -1 when the context has
/// no such part (e.g. it is only fixed text).
static int counterIndex(const NumerotationContext &context);
/// Refresh a value field from its context, and apply a typed value.
void refreshValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category);
void applyValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category);
Ui::AutoNumberingDockWidget *ui;
QETProject* m_project = nullptr;
ProjectView* m_project_view = nullptr;
@@ -28,6 +28,39 @@
<item row="2" column="1">
<widget class="QComboBox" name="m_conductor_cb"/>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="m_conductor_reset_start_pb">
<property name="maximumSize">
<size>
<width>24</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Réinitialiser à la valeur de départ</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/view-refresh.png</normaloff>:/ico/16x16/view-refresh.png</iconset>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QLineEdit" name="m_conductor_value_le">
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Valeur actuelle du compteur. Saisir une nouvelle valeur et valider pour la modifier.</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label">
<property name="text">
@@ -42,9 +75,75 @@
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QPushButton" name="m_element_reset_start_pb">
<property name="maximumSize">
<size>
<width>24</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Réinitialiser à la valeur de départ</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/view-refresh.png</normaloff>:/ico/16x16/view-refresh.png</iconset>
</property>
</widget>
</item>
<item row="3" column="3">
<widget class="QLineEdit" name="m_element_value_le">
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Valeur actuelle du compteur. Saisir une nouvelle valeur et valider pour la modifier.</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="m_folio_cb"/>
</item>
<item row="4" column="2">
<widget class="QPushButton" name="m_folio_reset_start_pb">
<property name="maximumSize">
<size>
<width>24</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Réinitialiser à la valeur de départ</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/view-refresh.png</normaloff>:/ico/16x16/view-refresh.png</iconset>
</property>
</widget>
</item>
<item row="4" column="3">
<widget class="QLineEdit" name="m_folio_value_le">
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Valeur actuelle du compteur. Saisir une nouvelle valeur et valider pour la modifier.</string>
</property>
</widget>
</item>
<item row="6" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
+41 -5
View File
@@ -18,6 +18,8 @@
#include "numparteditorw.h"
#include "ui_numparteditorw.h"
#include "../numerotationcontext.h"
#include <QRegularExpressionValidator>
/**
@@ -35,6 +37,9 @@ NumPartEditorW::NumPartEditorW(int type, QWidget *parent) :
{
ui -> setupUi(this);
setVisibleItems();
//The mask is a run of zeros and nothing else, so it cannot be typed
//into a state the renderer would have to reject.
ui -> format_le -> setValidator(new QRegularExpressionValidator(QRegularExpression("0*"), this));
setType(NumPartEditorW::unit, true);
}
@@ -99,6 +104,7 @@ NumPartEditorW::NumPartEditorW (NumerotationContext &context,
ui -> increase_spinBox -> setValue(strl.at(2).toInt());
if (strl.at(0)=="wrap" && strl.size() > 4)
ui -> modulus_spinBox -> setValue(strl.at(4).toInt());
ui -> format_le -> setText(NumerotationContext::formatOf(strl));
}
}
@@ -219,23 +225,30 @@ NumerotationContext NumPartEditorW::toNumContext()
type_str = "alpha";
break;
}
const QString number_format = ui -> format_le -> text();
if (type_str == "unitfolio"
|| type_str == "tenfolio"
|| type_str == "hundredfolio")
nc.addValue(type_str,
ui -> value_field -> displayText(),
ui -> increase_spinBox -> value(),
ui->value_field->displayText().toInt());
ui->value_field->displayText().toInt(),
0,
number_format);
else if (type_str == "wrap")
nc.addValue(type_str,
ui -> value_field -> displayText(),
ui -> increase_spinBox -> value(),
0,
ui -> modulus_spinBox -> value());
ui -> modulus_spinBox -> value(),
number_format);
else
nc.addValue(type_str,
ui -> value_field -> displayText(),
ui -> increase_spinBox -> value());
ui -> increase_spinBox -> value(),
0,
0,
number_format);
return nc;
}
@@ -317,6 +330,14 @@ void NumPartEditorW::on_increase_spinBox_valueChanged(int) {
@brief NumPartEditorW::on_modulus_spinBox_valueChanged
emit changed when modulus_spinBox value changed
*/
/**
@brief NumPartEditorW::on_format_le_textEdited
emit changed when the display format is edited
*/
void NumPartEditorW::on_format_le_textEdited(const QString &) {
emit changed();
}
void NumPartEditorW::on_modulus_spinBox_valueChanged(int) {
if (!ui -> value_field -> text().isEmpty()) emit changed();
}
@@ -359,8 +380,6 @@ void NumPartEditorW::setType(NumPartEditorW::type t, bool fnum) {
ui -> value_field -> setValidator(intValidator);
ui -> increase_spinBox -> setEnabled(true);
ui -> increase_spinBox -> setValue(1);
if (t == wrap)
ui -> modulus_spinBox -> setValue(8);
}
//@t isn't a numeric type
else if (t == string
@@ -414,7 +433,24 @@ void NumPartEditorW::setType(NumPartEditorW::type t, bool fnum) {
ui -> increase_spinBox -> setDisabled(true);
}
}
//A modulus of 0 means "no cycle", which makes a Cyclique part behave
//exactly like a plain digit. Defaulting it used to live in the numeric
//behavior block above, which is skipped when the previous type was
//itself numeric -- so the ordinary path of turning the default
//"Chiffre 1" into a "Cyclique (modulo)" left the modulus at 0 and
//produced a wrap part that never wrapped. Kept out of that block so it
//applies whatever the part was before, and only when the current value
//is unusable, so a modulus the user chose on purpose is not clobbered.
if (t == wrap && ui -> modulus_spinBox -> value() <= 0)
ui -> modulus_spinBox -> setValue(8);
ui -> modulus_spinBox -> setEnabled(t == wrap);
//A padding mask only means anything for a part rendered as a number.
const bool numeric = (t == unit || t == unitfolio || t == ten
|| t == tenfolio || t == hundred || t == hundredfolio
|| t == wrap);
ui -> format_le -> setEnabled(numeric);
if (!numeric)
ui -> format_le -> clear();
type_= t;
}
+1
View File
@@ -66,6 +66,7 @@ class NumPartEditorW : public QWidget
void on_value_field_textEdited();
void on_increase_spinBox_valueChanged(int);
void on_modulus_spinBox_valueChanged(int);
void on_format_le_textEdited(const QString &);
void setType (NumPartEditorW::type t, bool=false);
signals:
+22
View File
@@ -126,6 +126,28 @@
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="format_le">
<property name="enabled">
<bool>false</bool>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Format d'affichage : une suite de zéros donne le nombre minimum de chiffres (00 = 07, 000 = 007). Vide = largeur naturelle du type.</string>
</property>
<property name="placeholderText">
<string>0</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
+26 -2
View File
@@ -24,6 +24,7 @@
#include "qet.h"
#include "qetdiagrameditor.h"
#include "ui/potentialselectordialog.h"
#include "undocommand/setautonumcontextcommand.h"
/**
@brief ConductorAutoNumerotation::ConductorAutoNumerotation
@@ -156,7 +157,16 @@ void ConductorAutoNumerotation::newProperties(
autonum::setSequential(formula, seq, context, diagram, autoNum_name);
NumerotationContextCommands ncc (context, diagram);
diagram->project()->addConductorAutoNum(autoNum_name, ncc.next());
NumerotationContext new_context = ncc.next();
QETProject *project = diagram->project();
auto *undo = new SetAutoNumContextCommand(
[project](const QString &k, const NumerotationContext &c) {project->addConductorAutoNum(k, c);},
autoNum_name,
context,
new_context);
undo->setText(QObject::tr("Numéroter automatiquement un conducteur", "undo caption"));
diagram->undoStack().push(undo);
}
/**
@@ -245,7 +255,21 @@ void ConductorAutoNumerotation::numerateNewConductor()
autoNum_name);
NumerotationContextCommands ncc (context, m_diagram);
m_diagram->project()->addConductorAutoNum(autoNum_name, ncc.next());
NumerotationContext new_context = ncc.next();
QETProject *project = m_diagram->project();
auto setter = [project](const QString &k, const NumerotationContext &c) {project->addConductorAutoNum(k, c);};
if (m_parent_undo)
{
new SetAutoNumContextCommand(setter, autoNum_name, context, new_context, m_parent_undo);
}
else
{
auto *undo = new SetAutoNumContextCommand(setter, autoNum_name, context, new_context);
undo->setText(QObject::tr("Numéroter automatiquement un conducteur", "undo caption"));
m_diagram->undoStack().push(undo);
}
}
applyText(autonum::AssignVariables::formulaToLabel(
@@ -376,6 +376,7 @@ QString ElementQueryWidget::queryStr() const
if (ui->m_plc_cb->isChecked()) {
if (b) where +=" OR";
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::PLC) += "'";
b = true;
}
where.append(")");
@@ -501,7 +501,12 @@ void DiagramEventAddElement::addElement()
}
m_diagram->addItem(m_element);
//Autonum the new element before pushing undo_object, so the counter
//change it triggers is part of the same undo macro as the element's
//own placement (one Ctrl+Z reverts both, instead of silently
//leaving the counter advanced).
element->setUpFormula(true, undo_object);
m_diagram -> undoStack().push(undo_object);
element->setUpFormula();
element->freezeNewAddedElement();
}
+4
View File
@@ -317,6 +317,10 @@ void ElementScene::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
*/
void ElementScene::drawForeground(QPainter *p, const QRectF &)
{
if (!m_hotspot_visible) {
return;
}
p -> save();
// desactive tout antialiasing, sauf pour le texte
+10
View File
@@ -90,6 +90,8 @@ class ElementScene : public QGraphicsScene
m_y_grid;
QPointer<CustomElementGraphicPart> m_single_selected_item;
bool m_hotspot_visible = true;
// methods
public:
@@ -132,6 +134,14 @@ class ElementScene : public QGraphicsScene
QETElementEditor* editor() const;
void addItems(QVector<QGraphicsItem *> items);
void removeItems(QVector<QGraphicsItem *> items);
/// Whether drawForeground() draws the red hotspot cross. On by
/// default (the normal editing view); turned off around a
/// render() call meant to capture the element's actual content
/// only, e.g. exporting to SVG -- the cross is an editing aid,
/// not part of the drawn symbol.
void setHotspotVisible(bool visible) {m_hotspot_visible = visible;}
bool hotspotVisible() const {return m_hotspot_visible;}
protected:
void mouseMoveEvent (QGraphicsSceneMouseEvent *) override;
+3 -2
View File
@@ -18,6 +18,7 @@
#include "partplctable.h"
#include "../../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../../qetapp.h"
#include "../../QetGraphicsItemModeler/qetgraphicshandleritem.h"
#include "../../QetGraphicsItemModeler/qetgraphicshandlerutility.h"
#include "../../properties/elementdata.h"
@@ -302,7 +303,7 @@ void PartPlcTable::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti
// Draw column headers
QFont header_font = plc_data.headerFont.family().isEmpty()
? painter->font() : plc_data.headerFont;
? QETApp::diagramTextsFont() : plc_data.headerFont;
header_font.setBold(true);
painter->setFont(header_font);
@@ -318,7 +319,7 @@ void PartPlcTable::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti
// Draw IO rows
QFont cell_font = plc_data.cellFont.family().isEmpty()
? painter->font() : plc_data.cellFont;
? QETApp::diagramTextsFont() : plc_data.cellFont;
painter->setFont(cell_font);
int start_idx = block_starts.at(block);
int end_idx = (block + 1 < block_starts.size())
@@ -28,6 +28,8 @@
#include <QSignalBlocker>
#include <QTableWidgetItem>
#include <QHeaderView>
#include <QScrollBar>
#include <QWheelEvent>
#include <QTableWidget>
#include <QCheckBox>
#include <QGroupBox>
@@ -41,6 +43,8 @@
#include <QFont>
#include <QLineEdit>
#include <QSplitter>
#include <QShortcut>
#include <QMenu>
/**
@brief The EditorDelegate class
@@ -666,10 +670,15 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
plc_layout->addLayout(toolbar);
// Tables side by side: IO table (left) + Terminal table (right)
auto *tables_splitter = new QSplitter(Qt::Horizontal, m_plc_gb);
// Both share a single vertical scrollbar on the right
auto *tables_container = new QWidget(m_plc_gb);
auto *tables_layout = new QHBoxLayout(tables_container);
tables_layout->setContentsMargins(0, 0, 0, 0);
auto *splitter = new QSplitter(Qt::Horizontal, tables_container);
// IO Table
m_plc_table = new QTableWidget(tables_splitter);
m_plc_table = new QTableWidget(splitter);
m_plc_table->setColumnCount(5);
m_plc_table->setHorizontalHeaderLabels({
tr("Type"), tr("Adresse"), tr("Fonction"),
@@ -686,10 +695,10 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems);
m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_plc_table->setMinimumHeight(200);
tables_splitter->addWidget(m_plc_table);
m_plc_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
// Terminal table (per IO: Nb + T1-T4)
m_plc_terminal_table = new QTableWidget(tables_splitter);
m_plc_terminal_table = new QTableWidget(splitter);
m_plc_terminal_table->setColumnCount(2);
m_plc_terminal_table->setHorizontalHeaderLabels({
tr("Nb."), tr("T1")
@@ -700,13 +709,61 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
m_plc_terminal_table->setSelectionBehavior(QAbstractItemView::SelectItems);
m_plc_terminal_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_plc_terminal_table->setMinimumHeight(200);
tables_splitter->addWidget(m_plc_terminal_table);
m_plc_terminal_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
tables_splitter->setStretchFactor(0, 3);
tables_splitter->setStretchFactor(1, 1);
tables_splitter->setSizes({500, 150});
// Shared vertical scrollbar
m_plc_shared_scrollbar = new QScrollBar(Qt::Vertical, tables_container);
m_plc_shared_scrollbar->setMinimum(0);
plc_layout->addWidget(tables_splitter);
splitter->addWidget(m_plc_table);
splitter->addWidget(m_plc_terminal_table);
splitter->setStretchFactor(0, 3);
splitter->setStretchFactor(1, 1);
tables_layout->addWidget(splitter);
tables_layout->addWidget(m_plc_shared_scrollbar);
// Bidirectional sync between tables and shared scrollbar
// Use flag to prevent infinite loops
connect(m_plc_shared_scrollbar, &QScrollBar::valueChanged,
this, [this](int value) {
if (m_plc_scroll_sync) return;
m_plc_scroll_sync = true;
m_plc_table->verticalScrollBar()->setValue(value);
m_plc_terminal_table->verticalScrollBar()->setValue(value);
m_plc_scroll_sync = false;
});
connect(m_plc_table->verticalScrollBar(), &QScrollBar::valueChanged,
this, [this](int value) {
if (m_plc_scroll_sync) return;
m_plc_scroll_sync = true;
m_plc_shared_scrollbar->setValue(value);
m_plc_terminal_table->verticalScrollBar()->setValue(value);
m_plc_scroll_sync = false;
});
connect(m_plc_terminal_table->verticalScrollBar(), &QScrollBar::valueChanged,
this, [this](int value) {
if (m_plc_scroll_sync) return;
m_plc_scroll_sync = true;
m_plc_shared_scrollbar->setValue(value);
m_plc_table->verticalScrollBar()->setValue(value);
m_plc_scroll_sync = false;
});
// Update shared scrollbar range from both tables
auto syncRange = [this]() {
int max = qMax(m_plc_table->verticalScrollBar()->maximum(),
m_plc_terminal_table->verticalScrollBar()->maximum());
m_plc_shared_scrollbar->blockSignals(true);
m_plc_shared_scrollbar->setMaximum(max);
m_plc_shared_scrollbar->blockSignals(false);
};
connect(m_plc_table->verticalScrollBar(), &QScrollBar::rangeChanged,
this, syncRange);
connect(m_plc_terminal_table->verticalScrollBar(), &QScrollBar::rangeChanged,
this, syncRange);
plc_layout->addWidget(tables_container);
// Font settings
auto *font_layout = new QHBoxLayout();
@@ -790,12 +847,25 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
}
plc_layout->addLayout(col_layout);
// Add to master group box
ui->m_master_gb->layout()->addWidget(m_plc_gb);
// Add to master group box - row 4, full width
auto *gl = qobject_cast<QGridLayout*>(ui->m_master_gb->layout());
gl->addWidget(m_plc_gb, 4, 0, 1, 2);
// Connect signals
connect(add_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcAddRow);
connect(remove_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcRemoveRow);
// Ctrl+V shortcut for paste
auto *paste_shortcut = new QShortcut(QKeySequence::Paste, m_plc_table);
connect(paste_shortcut, &QShortcut::activated, this, &ElementPropertiesEditorWidget::plcPasteFromClipboard);
// Context menu for the PLC table
m_plc_table->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_plc_table, &QTableWidget::customContextMenuRequested, this, [this](const QPoint &pos) {
QMenu menu;
menu.addAction(tr("Coller depuis le presse-papiers"), this, &ElementPropertiesEditorWidget::plcPasteFromClipboard);
menu.exec(m_plc_table->mapToGlobal(pos));
});
}
/**
@@ -877,12 +947,12 @@ void ElementPropertiesEditorWidget::populatePlcTable()
m_plc_header_font = plc_data.headerFont;
m_plc_cell_font = plc_data.cellFont;
if (m_plc_header_font.family().isEmpty()) {
m_plc_header_font = QFont(m_plc_table->font());
m_plc_header_font = QETApp::diagramTextsFont();
m_plc_header_font.setBold(true);
m_plc_header_font.setPointSize(8);
}
if (m_plc_cell_font.family().isEmpty()) {
m_plc_cell_font = QFont(m_plc_table->font());
m_plc_cell_font = QETApp::diagramTextsFont();
m_plc_cell_font.setPointSize(8);
}
m_plc_header_font_btn->setText(tr("Police des en-têtes: %1 %2pt")
@@ -920,6 +990,21 @@ void ElementPropertiesEditorWidget::populatePlcTable()
hdr->moveSection(hdr->visualIndex(logical), visual);
}
}
// Ensure scrollbars stay hidden and sync shared scrollbar range
if (m_plc_table) {
m_plc_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_plc_table->verticalScrollBar()->setVisible(false);
}
if (m_plc_terminal_table) {
m_plc_terminal_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_plc_terminal_table->verticalScrollBar()->setVisible(false);
}
if (m_plc_shared_scrollbar && m_plc_table) {
int max = qMax(m_plc_table->verticalScrollBar()->maximum(),
m_plc_terminal_table->verticalScrollBar()->maximum());
m_plc_shared_scrollbar->setMaximum(max);
}
}
/**
@@ -1158,53 +1243,80 @@ void ElementPropertiesEditorWidget::plcPasteFromClipboard()
return;
QStringList lines = clipboard_text.split('\n', Qt::SkipEmptyParts);
if (lines.isEmpty())
return;
int start_row = m_plc_table->rowCount();
m_plc_table->setRowCount(start_row + lines.size());
for (int i = 0; i < lines.size(); ++i) {
QStringList cells = lines.at(i).split('\t');
int row = start_row + i;
// Type combo
auto *type_cb = new QComboBox(m_plc_table);
QStringList plc_types = ElementData::plcIOTypeList();
for (int t = 0; t < plc_types.size(); ++t) {
type_cb->addItem(plc_types.at(t), t);
bool has_tabs = false;
for (const QString &line : lines) {
if (line.contains('\t')) {
has_tabs = true;
break;
}
}
// Try to match type from clipboard
if (!cells.isEmpty()) {
QString type_str = cells.at(0).trimmed();
int type_idx = -1;
if (!has_tabs) {
// Vertical paste: values go down the same column
int target_col = m_plc_table->currentColumn();
if (target_col < 0) target_col = 0;
int target_row = m_plc_table->currentRow();
if (target_row < 0) target_row = 0;
int max_rows = m_plc_table->rowCount();
for (int i = 0; i < lines.size(); ++i) {
int row = target_row + i;
if (row >= max_rows) break;
plcSetCellFromValue(row, target_col, lines.at(i).trimmed());
}
} else {
// Horizontal paste: each line is a separate IO row
int target_row = m_plc_table->currentRow();
if (target_row < 0) target_row = 0;
int max_rows = m_plc_table->rowCount();
for (int i = 0; i < lines.size(); ++i) {
int row = target_row + i;
if (row >= max_rows) break;
QStringList cells = lines.at(i).split('\t');
for (int c = 0; c < cells.size(); ++c) {
if (c > 4) break;
plcSetCellFromValue(row, c, cells.at(c).trimmed());
}
}
}
}
/**
* @brief ElementPropertiesEditorWidget::plcSetCellFromValue
* Set a single table cell value, respecting the column widget type.
*/
void ElementPropertiesEditorWidget::plcSetCellFromValue(int row, int col, const QString &val)
{
if (!m_plc_table || row < 0 || col < 0 || col > 4)
return;
if (col == 0) {
auto *type_cb = qobject_cast<QComboBox*>(m_plc_table->cellWidget(row, col));
if (!type_cb)
return;
if (!val.isEmpty()) {
QStringList plc_types = ElementData::plcIOTypeList();
for (int t = 0; t < plc_types.size(); ++t) {
if (plc_types.at(t).compare(type_str, Qt::CaseInsensitive) == 0) {
type_idx = t;
break;
if (plc_types.at(t).compare(val, Qt::CaseInsensitive) == 0) {
type_cb->setCurrentIndex(t);
return;
}
}
if (type_idx >= 0)
type_cb->setCurrentIndex(type_idx);
}
m_plc_table->setCellWidget(row, 0, type_cb);
// Address
m_plc_table->setItem(row, 1, new QTableWidgetItem(
cells.size() > 1 ? cells.at(1).trimmed() : QString()));
// Function text
m_plc_table->setItem(row, 2, new QTableWidgetItem(
cells.size() > 2 ? cells.at(2).trimmed() : QString()));
// Comment
m_plc_table->setItem(row, 3, new QTableWidgetItem(
cells.size() > 3 ? cells.at(3).trimmed() : QString()));
// CrossRef (read-only)
auto *crossref_item = new QTableWidgetItem(
cells.size() > 4 ? cells.at(4).trimmed() : QString());
crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable);
m_plc_table->setItem(row, 4, crossref_item);
}
else if (col == 4) {
// CrossRef - read-only
auto *item = new QTableWidgetItem(val);
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
m_plc_table->setItem(row, col, item);
}
else {
// Text columns: Address, Function, Comment
m_plc_table->setItem(row, col, new QTableWidgetItem(val));
}
}
@@ -30,6 +30,7 @@ class QCheckBox;
class QGroupBox;
class QPushButton;
class QLineEdit;
class QScrollBar;
namespace Ui {
class ElementPropertiesEditorWidget;
@@ -74,6 +75,7 @@ class ElementPropertiesEditorWidget : public QDialog
void plcTerminalCountChanged(int row, int count);
void plcSelectHeaderFont();
void plcSelectCellFont();
void plcSetCellFromValue(int row, int col, const QString &val);
//ATTRIBUTES
private:
@@ -92,9 +94,11 @@ class ElementPropertiesEditorWidget : public QDialog
QCheckBox *m_plc_show_headers_cb = nullptr;
QFont m_plc_header_font;
QFont m_plc_cell_font;
QList<QCheckBox *> m_plc_col_visibility_checkboxes;
QList<QSpinBox *> m_plc_col_width_spinboxes;
QList<QLineEdit *> m_plc_col_name_edits;
QList<QCheckBox *> m_plc_col_visibility_checkboxes;
QList<QSpinBox *> m_plc_col_width_spinboxes;
QList<QLineEdit *> m_plc_col_name_edits;
QScrollBar *m_plc_shared_scrollbar = nullptr;
bool m_plc_scroll_sync = false;
};
#endif // ELEMENTPROPERTIESEDITORWIDGET_H
@@ -39,7 +39,20 @@
<item>
<widget class="QComboBox" name="m_base_type_cb"/>
</item>
</layout>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="m_slave_gb">
@@ -93,17 +106,34 @@
<property name="title">
<string>Élément maître</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Type concret</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="m_master_type_cb"/>
</item>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="type_concret_layout">
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Type concret</string>
</property>
</widget>
</item>
<item>
<spacer name="type_concret_spacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QComboBox" name="m_master_type_cb"/>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="max_slaves_checkbox">
<property name="text">
@@ -202,20 +232,7 @@
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</layout>
</widget>
<widget class="QWidget" name="Informations">
<attribute name="title">
+90
View File
@@ -53,6 +53,8 @@
#include <QSettings>
#include <QActionGroup>
#include <QFileDialog>
#include <QSvgGenerator>
/**
* @brief QETElementEditor::QETElementEditor
@@ -1377,6 +1379,94 @@ bool QETElementEditor::on_m_save_as_file_action_triggered()
return false;
}
/**
@brief QETElementEditor::on_m_export_svg_action_triggered
Export the element currently open in this editor to a standalone SVG
file.
Renders the live ElementScene directly, the same way
ExportDialog::generateSvg() renders the live Diagram for the diagram
editor's own SVG export -- not ElementPictureFactory's cached picture.
That cache is keyed by the element's saved-to-disk uuid and is never
invalidated on edit (nothing in this editor ever tells it to), so it
would silently export stale content for any element already previewed
once in the elements panel, and nothing at all for one that has never
been saved. Rendering the scene directly has neither problem and
always reflects exactly what is currently on screen, saved or not.
@return true if the file was written
*/
bool QETElementEditor::on_m_export_svg_action_triggered()
{
//Suggest the element's own filename (without its .elmt extension) as
//the default export name, per plc-user's review on #637 -- the
//directory-only default below is unchanged for an element that has
//never been saved, since there is no filename to derive one from.
QString suggested_path = QETApp::customElementsDir();
if (!m_file_name.isEmpty()) {
QFileInfo file_info(m_file_name);
suggested_path = QDir(file_info.absolutePath()).filePath(file_info.completeBaseName());
}
QString fn = QFileDialog::getSaveFileName(
this,
tr("Exporter en SVG", "dialog title"),
suggested_path,
tr("Image SVG (*.svg)", "filetypes allowed when exporting an element to SVG"));
if (fn.isEmpty()) {
return false;
}
if (!fn.endsWith(".svg", Qt::CaseInsensitive)) {
fn += ".svg";
}
QFile file(fn);
if (!file.open(QIODevice::WriteOnly)) {
QMessageBox::critical(this, tr("Échec de l'export"),
tr("Impossible d'écrire dans le fichier « %1 ».").arg(fn));
return false;
}
//Margin-less bounding rect of the element's own drawn content
//(lines, rects, terminals, text...), excluding the origin cross
//and other editor-only decoration -- see
//ElementScene::elementSceneGeometricRect()'s own doc comment.
//Falls back to itemsBoundingRect() for the rare element made up
//only of item types that helper deliberately excludes.
QRectF source_rect = m_elmt_scene->elementSceneGeometricRect();
if (source_rect.isEmpty()) {
source_rect = m_elmt_scene->itemsBoundingRect();
}
constexpr qreal margin = 5.0;
source_rect.adjust(-margin, -margin, margin, margin);
QSize target_size = source_rect.size().toSize();
if (target_size.isEmpty()) {
target_size = QSize(1, 1);
}
QSvgGenerator svg_engine;
svg_engine.setSize(target_size);
svg_engine.setViewBox(QRect(QPoint(0, 0), target_size));
svg_engine.setOutputDevice(&file);
QPainter svg_painter(&svg_engine);
svg_painter.setRenderHint(QPainter::Antialiasing, true);
svg_painter.setRenderHint(QPainter::TextAntialiasing, true);
//The hotspot cross is ElementScene::drawForeground()'s editing aid,
//drawn unconditionally on every render() call including this one
//unless told not to -- it is not part of the element being
//exported.
m_elmt_scene->setHotspotVisible(false);
m_elmt_scene->render(&svg_painter, QRectF(QPointF(0, 0), target_size), source_rect);
m_elmt_scene->setHotspotVisible(true);
svg_painter.end();
return true;
}
void QETElementEditor::on_m_reload_action_triggered()
{
//If user already edit the element, ask confirmation to reload
+1
View File
@@ -87,6 +87,7 @@ class QETElementEditor : public QMainWindow
void on_m_open_action_triggered();
void on_m_open_from_file_action_triggered();
bool on_m_save_as_file_action_triggered();
bool on_m_export_svg_action_triggered();
void on_m_reload_action_triggered();
void on_m_quit_action_triggered();
void on_m_deselect_all_action_triggered();
+10
View File
@@ -40,6 +40,7 @@
<addaction name="m_save_action"/>
<addaction name="m_save_as_action"/>
<addaction name="m_save_as_file_action"/>
<addaction name="m_export_svg_action"/>
<addaction name="separator"/>
<addaction name="m_reload_action"/>
<addaction name="separator"/>
@@ -262,6 +263,15 @@
<string>Enregistrer dans un fichier</string>
</property>
</action>
<action name="m_export_svg_action">
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/document-export.png</normaloff>:/ico/22x22/document-export.png</iconset>
</property>
<property name="text">
<string>Exporter en SVG</string>
</property>
</action>
<action name="m_reload_action">
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
+34
View File
@@ -61,6 +61,8 @@ ElementsPanelWidget::ElementsPanelWidget(QWidget *parent) : QWidget(parent) {
prj_edit_prop = new QAction(QET::Icons::DialogInformation, tr("Propriétés du projet"), this);
prj_prop_diagram = new QAction(QET::Icons::DialogInformation, tr("Propriétés du folio"), this);
prj_add_diagram = new QAction(QET::Icons::DiagramAdd, tr("Ajouter un folio"), this);
prj_insert_diagram_above = new QAction(QET::Icons::DiagramAdd, tr("Insérer un folio au-dessus"), this);
prj_insert_diagram_below = new QAction(QET::Icons::DiagramAdd, tr("Insérer un folio en dessous"), this);
prj_duplicate_diagram = new QAction(QET::Icons::IC_CopyFile, tr("Copier et coller"), this);
prj_del_diagram = new QAction(QET::Icons::DiagramDelete, tr("Supprimer ce folio"), this);
prj_move_diagram_up = new QAction(QET::Icons::GoUp, tr("Remonter ce folio"), this);
@@ -101,6 +103,8 @@ ElementsPanelWidget::ElementsPanelWidget(QWidget *parent) : QWidget(parent) {
connect(prj_edit_prop, SIGNAL(triggered()), this, SLOT(editProjectProperties()));
connect(prj_prop_diagram, SIGNAL(triggered()), this, SLOT(editDiagramProperties()));
connect(prj_add_diagram, SIGNAL(triggered()), this, SLOT(newDiagram()));
connect(prj_insert_diagram_above, SIGNAL(triggered()), this, SLOT(insertDiagramAbove()));
connect(prj_insert_diagram_below, SIGNAL(triggered()), this, SLOT(insertDiagramBelow()));
connect(prj_del_diagram, SIGNAL(triggered()), this, SLOT(deleteDiagram()));
connect(prj_duplicate_diagram, SIGNAL(triggered()), this, SLOT(duplicateDiagram()));
connect(prj_move_diagram_up, SIGNAL(triggered()), this, SLOT(moveDiagramUp()));
@@ -245,6 +249,32 @@ void ElementsPanelWidget::newDiagram()
}
}
/**
@brief ElementsPanelWidget::insertDiagramAbove
Emit requestForNewDiagramAt with the position of the currently
selected diagram, inserting the new folio right before it.
*/
void ElementsPanelWidget::insertDiagramAbove()
{
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
QETProject *project = selected_diagram->project();
emit(requestForNewDiagramAt(project, project->folioIndex(selected_diagram)));
}
}
/**
@brief ElementsPanelWidget::insertDiagramBelow
Emit requestForNewDiagramAt with the position right after the
currently selected diagram, inserting the new folio right after it.
*/
void ElementsPanelWidget::insertDiagramBelow()
{
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
QETProject *project = selected_diagram->project();
emit(requestForNewDiagramAt(project, project->folioIndex(selected_diagram) + 1));
}
}
/**
* Emet le signal requestForDiagramsDeletion avec les schemas selectionnes
*/
@@ -451,6 +481,8 @@ void ElementsPanelWidget::updateButtons()
prj_del_diagram -> setEnabled(is_writable);
prj_duplicate_diagram -> setEnabled(is_writable);
prj_insert_diagram_above -> setEnabled(is_writable);
prj_insert_diagram_below -> setEnabled(is_writable);
prj_move_diagram_up -> setEnabled(is_writable && min_position > 0);
prj_move_diagram_down -> setEnabled(is_writable && max_position < project_diagrams_count - 1);
prj_move_diagram_top -> setEnabled(is_writable && min_position > 0);
@@ -504,6 +536,8 @@ void ElementsPanelWidget::handleContextMenu(const QPoint &pos) {
break;
case QET::Diagram:
context_menu -> addAction(prj_prop_diagram);
context_menu -> addAction(prj_insert_diagram_above);
context_menu -> addAction(prj_insert_diagram_below);
context_menu -> addAction(prj_del_diagram);
context_menu -> addAction(prj_duplicate_diagram);
context_menu -> addAction(prj_move_diagram_top);
+5
View File
@@ -46,6 +46,8 @@ class ElementsPanelWidget : public QWidget {
*prj_edit_prop,
*prj_prop_diagram,
*prj_add_diagram,
*prj_insert_diagram_above,
*prj_insert_diagram_below,
*prj_del_diagram,
*prj_duplicate_diagram,
*prj_move_diagram_up,
@@ -66,6 +68,7 @@ class ElementsPanelWidget : public QWidget {
signals:
void requestForProject(QETProject *);
void requestForNewDiagram(QETProject *);
void requestForNewDiagramAt(QETProject *, int);
void requestForProjectClosing(QETProject *);
void requestForProjectPropertiesEdition(QETProject *);
void requestForDiagramPropertiesEdition(Diagram *);
@@ -88,6 +91,8 @@ class ElementsPanelWidget : public QWidget {
void editProjectProperties();
void editDiagramProperties();
void newDiagram();
void insertDiagramAbove();
void insertDiagramBelow();
void deleteDiagram();
void duplicateDiagram();
void moveDiagramUp();
+171
View File
@@ -0,0 +1,171 @@
/*
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 "crashhandler.h"
#include "logring.h"
#include "../qetversion.h"
#include <QByteArray>
#include <QSysInfo>
#include <atomic>
#include <cstring>
#ifdef Q_OS_WIN
#include <fcntl.h>
#include <io.h>
#include <share.h>
#include <sys/stat.h>
#include <windows.h>
#else
#include <cerrno>
#include <csignal>
#include <fcntl.h>
#include <unistd.h>
#endif
namespace {
// Everything the handler touches is preallocated here and filled in by
// install() (normal context, runs once at startup) -- nothing under the
// actual signal/exception path may allocate or touch QString/Qt.
const LogRing *g_ring = nullptr;
char g_dump_path[1024] = {};
char g_header[1024] = {};
int g_header_len = 0;
// Guards against two threads crashing at once, or the handler itself
// faulting while dumping: only the first crash writes a dump. See
// crashhandler.h invariant 4.
std::atomic<bool> g_already_dumped{false};
#ifndef Q_OS_WIN
// A stack-overflow SIGSEGV leaves no usable stack for a handler to run
// on at all, hence the alternate signal stack (invariant: sized well
// above any known SIGSTKSZ so this doesn't depend on
// sysconf(_SC_SIGSTKSZ), which some libc versions require at runtime
// rather than offering as a compile-time constant).
char g_altstack[65536];
const int kHandledSignals[] = {SIGSEGV, SIGABRT, SIGBUS, SIGFPE, SIGILL};
void restoreDefaultAndReraise(int sig)
{
struct sigaction sa {};
sa.sa_handler = SIG_DFL;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(sig, &sa, nullptr);
raise(sig);
}
void signalHandler(int sig)
{
if (g_already_dumped.exchange(true, std::memory_order_acq_rel)) {
// Not the first crash (concurrent fault on another thread, or
// this handler faulting while dumping): skip straight to
// restore-and-re-raise rather than risk a second, interleaved
// write to the same file.
restoreDefaultAndReraise(sig);
return;
}
// open/write/close are all on the POSIX async-signal-safe function
// list; nothing else is called here.
const int fd = ::open(g_dump_path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (fd >= 0) {
if (g_header_len > 0) {
::write(fd, g_header, static_cast<size_t>(g_header_len));
}
if (g_ring) {
g_ring->dumpToFd(fd);
}
::close(fd);
}
restoreDefaultAndReraise(sig);
}
#else // Q_OS_WIN
LONG WINAPI windowsExceptionFilter(EXCEPTION_POINTERS *)
{
bool expected = false;
if (!g_already_dumped.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
return EXCEPTION_CONTINUE_SEARCH;
}
int fd = -1;
errno_t err = _sopen_s(&fd, g_dump_path,
_O_WRONLY | _O_CREAT | _O_TRUNC | _O_BINARY,
_SH_DENYWR, _S_IREAD | _S_IWRITE);
if (err == 0 && fd >= 0) {
if (g_header_len > 0) {
_write(fd, g_header, g_header_len);
}
if (g_ring) {
g_ring->dumpToFd(fd);
}
_close(fd);
}
// Do not suppress Windows Error Reporting / an attached debugger --
// same invariant as re-raising on POSIX (see crashhandler.h,
// invariant 3).
return EXCEPTION_CONTINUE_SEARCH;
}
#endif
} // namespace
void CrashHandler::install(const LogRing *ring, const QString &dump_path)
{
g_ring = ring;
const QByteArray path_utf8 = dump_path.toUtf8();
std::strncpy(g_dump_path, path_utf8.constData(), sizeof(g_dump_path) - 1);
const QByteArray header = QByteArray("QET crash dump\n")
+ "Version: " + QetVersion::displayedVersion().toUtf8() + "\n"
+ "Git: " GIT_COMMIT_SHA "\n"
+ "OS: " + QSysInfo::prettyProductName().toUtf8() + " (" + QSysInfo::currentCpuArchitecture().toUtf8() + ")\n"
+ "Qt: " QT_VERSION_STR "\n"
+ "---\n";
g_header_len = qMin<int>(header.size(), static_cast<int>(sizeof(g_header)) - 1);
std::memcpy(g_header, header.constData(), static_cast<size_t>(g_header_len));
#ifdef Q_OS_WIN
SetUnhandledExceptionFilter(windowsExceptionFilter);
#else
stack_t ss;
ss.ss_sp = g_altstack;
ss.ss_size = sizeof(g_altstack);
ss.ss_flags = 0;
sigaltstack(&ss, nullptr);
struct sigaction sa {};
sa.sa_handler = signalHandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_ONSTACK;
for (int sig : kHandledSignals) {
sigaction(sig, &sa, nullptr);
}
#endif
}
+85
View File
@@ -0,0 +1,85 @@
/*
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 CRASHHANDLER_H
#define CRASHHANDLER_H
#include <QString>
class LogRing;
/**
@brief The CrashHandler class
Discussion #644, step 4: on a fatal crash, flush the in-memory
LogRing to a fixed file before the process dies, so the last N log
lines leading up to the crash survive it -- today they only exist in
memory and are lost with the process.
This is the highest-risk piece of the whole logging rework (the
discussion's own words: "lands last, behind its own switch"), so its
invariants are worth restating plainly:
1. The handler must never block. It takes no locks -- LogRing itself
is lock-free for exactly this reason (see logring.h). A handler
that can hang is worse than no handler: it turns a clean crash
(which at least produces a core dump) into a hung process that has
to be force-killed, producing neither a core dump nor a ring dump.
2. The handler must never allocate. Under heap corruption -- a
plausible *cause* of the very crash being handled -- malloc may
itself deadlock or fault. Every buffer this code touches at crash
time (the dump path, the header, the ring's own storage) is
preallocated by install(), which runs once at startup in normal
(non-signal) context.
3. The handler must not swallow the crash. After writing the dump it
restores the default disposition for the signal and re-raises, so
the OS still produces a core dump (POSIX) / Windows Error
Reporting still sees the exception. A handler that "fixed" the
crash by not re-raising would destroy the post-mortem evidence a
core dump provides.
4. Only the *first* crash writes a dump. An atomic test-and-set
guards against two threads faulting simultaneously (or the handler
itself faulting while dumping) producing an interleaved or
truncated file; every crash after the first goes straight to
restore-and-re-raise.
Tested in this environment: POSIX/Linux only (sigaction, sigaltstack,
SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL). The Windows path
(SetUnhandledExceptionFilter) and macOS-specific behaviour (signal
handling itself is POSIX and shares the Linux code path, but sandbox
profiles can affect where the dump file may be written) are
implemented per the discussion's guidance but could not be exercised
here -- there is no Windows or macOS build available in this sandbox.
Please sanity-check both before relying on them in the field.
*/
class CrashHandler
{
public:
/// Installs the crash handler. Must be called from normal
/// (non-signal) startup code, after the LogRing it will dump
/// exists, and only once. `ring` must outlive the process (in
/// practice: the LogRing owned by QetLogger's function-local
/// static instance, which is never destroyed before exit).
/// `dump_path` is resolved and copied into a fixed-size internal
/// buffer here; nothing under the actual signal/exception path
/// touches QString.
static void install(const LogRing *ring, const QString &dump_path);
private:
CrashHandler() = delete;
};
#endif // CRASHHANDLER_H
+58
View File
@@ -0,0 +1,58 @@
/*
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 "eventloopwatchdog.h"
#include <QDebug>
#include <QProcessEnvironment>
EventLoopWatchdog::EventLoopWatchdog(QObject *parent) :
QObject(parent)
{
m_disabled = QProcessEnvironment::systemEnvironment()
.value(QStringLiteral("QET_WATCHDOG_DISABLE")) == QStringLiteral("1");
// Precise, not the default Coarse: Coarse explicitly trades timing
// accuracy for power/scheduling efficiency (platform-dependent, but
// commonly +/- a double-digit percentage), which would show up as
// noise indistinguishable from a real stall in exactly the
// measurement this class exists to make trustworthy.
m_timer.setTimerType(Qt::PreciseTimer);
connect(&m_timer, &QTimer::timeout, this, &EventLoopWatchdog::tick);
}
void EventLoopWatchdog::start()
{
if (m_disabled)
return;
m_elapsed.start();
m_timer.start(kTickIntervalMs);
}
void EventLoopWatchdog::tick()
{
// restart() returns the elapsed time and resets the clock in one
// call, so this tick's own cost is never counted against the next.
const qint64 actual_ms = m_elapsed.restart();
if (actual_ms > kStallThresholdMs) {
qWarning() << "EventLoopWatchdog: main thread stalled for"
<< actual_ms << "ms (expected a tick every"
<< kTickIntervalMs << "ms)";
}
}
+92
View File
@@ -0,0 +1,92 @@
/*
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 EVENTLOOPWATCHDOG_H
#define EVENTLOOPWATCHDOG_H
#include <QElapsedTimer>
#include <QObject>
#include <QTimer>
/**
@brief The EventLoopWatchdog class
Detects when the main (GUI) thread's event loop goes unresponsive --
QetLogger (discussion #644) can only see what an explicit qDebug()/
qInfo()/qWarning() call already decided to report, and most of a
session (painting, dragging, a slow synchronous operation) produces
no log output at all, so a silent multi-second gap in the log is
indistinguishable from the user simply not doing anything.
This closes that gap the direct way: a repeating QTimer::PreciseTimer
ticks on a short, fixed interval; each tick measures the *actual*
wall-clock time elapsed since the previous one via QElapsedTimer
(monotonic -- unaffected by system clock/NTP adjustments, unlike
QDateTime). Qt does not queue up missed fires for a normal repeating
timer, so if the event loop is blocked for 600ms, the timer fires
once as soon as the loop frees up, with ~600ms measured since the
last tick -- that gap *is* the stall, measured at its source rather
than inferred from log silence.
Only fires a qWarning() (and so only touches the log at all) when a
tick is late by more than kStallThresholdMs, to stay within the
spirit of QetLogger's bounded-log design (see its class comment) --
a healthy session should produce zero output from this class. This
tells you *that* a stall happened and *how long* it was, not what
caused it; pair a reported timestamp with `docker exec`+gdb the way
the CLI hang (PR #661) was diagnosed to go from "it lagged" to a
root cause.
Escape hatch: if QET_WATCHDOG_DISABLE=1 is set in the environment at
construction time, start() does nothing.
*/
class EventLoopWatchdog : public QObject
{
Q_OBJECT
public:
/// How often the watchdog checks in. Small enough to bound the
/// measurement's own granularity, large enough that the tick
/// itself is negligible overhead on the event loop it's watching.
static constexpr int kTickIntervalMs = 50;
/// A tick arriving later than this many ms after the previous one
/// is logged as a stall. Comfortably above kTickIntervalMs so
/// ordinary OS scheduling noise doesn't produce a warning on every
/// tick, and in the range a user would actually notice as lag.
static constexpr int kStallThresholdMs = 200;
explicit EventLoopWatchdog(QObject *parent = nullptr);
/// Starts ticking. Must be called from the main thread, after the
/// event loop it watches is about to run (i.e. immediately before
/// QApplication::exec()) -- constructing this class earlier is
/// harmless, but start() before there is an event loop to tick
/// against would just measure the time until app.exec() is
/// reached. No-op if QET_WATCHDOG_DISABLE=1 was set at
/// construction time.
void start();
private slots:
void tick();
private:
QTimer m_timer;
QElapsedTimer m_elapsed;
bool m_disabled = false;
};
#endif // EVENTLOOPWATCHDOG_H
+152
View File
@@ -0,0 +1,152 @@
/*
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 "logring.h"
#include <cstring>
#ifdef Q_OS_WIN
#include <io.h>
#else
#include <cerrno>
#include <unistd.h>
#endif
static_assert(std::atomic<int>::is_always_lock_free,
"LogRing::Entry::length must be a lock-free atomic<int> -- "
"dumpToFd() reads it from a signal handler and must never block.");
namespace {
/**
@brief writeAllSignalSafe
Loops until length bytes have been written to fd or an unrecoverable
error occurs. write(2) may write fewer bytes than requested and may
return EINTR -- both are *more* likely from inside a signal handler
than in normal code, so a single write() call is not enough here.
Async-signal-safe: only calls write(2)/errno, nothing else.
*/
void writeAllSignalSafe(int fd, const char *data, int length) noexcept
{
int remaining = length;
const char *p = data;
while (remaining > 0) {
#ifdef Q_OS_WIN
const int n = _write(fd, p, static_cast<unsigned int>(remaining));
if (n <= 0) {
return;
}
#else
const ssize_t n = ::write(fd, p, static_cast<size_t>(remaining));
if (n < 0) {
if (errno == EINTR) {
continue;
}
return; // unrecoverable -- give up silently, never block/throw
}
if (n == 0) {
return;
}
#endif
p += n;
remaining -= static_cast<int>(n);
}
}
} // namespace
LogRing::LogRing() :
// The sized constructor value-initialises each Entry in place; unlike
// resize(), it doesn't require Entry to be move/copy-constructible,
// which std::atomic<int> deliberately never is. The only allocation
// this class ever does.
m_entries(kCapacityEntries)
{
}
void LogRing::append(const QByteArray &line) noexcept
{
static const char kMarker[] = "...[ring-truncated]\n";
const int marker_len = static_cast<int>(sizeof(kMarker)) - 1;
const quint64 idx = m_write_cursor.fetch_add(1, std::memory_order_relaxed);
Entry &slot = m_entries[static_cast<size_t>(idx % static_cast<quint64>(kCapacityEntries))];
// Zero the length first so a concurrent reader landing on this exact
// slot mid-copy sees "not ready" rather than the previous lap's
// (now-being-overwritten) content at a stale length.
slot.length.store(0, std::memory_order_relaxed);
int len;
if (line.size() < kEntryBytes) {
std::memcpy(slot.data, line.constData(), static_cast<size_t>(line.size()));
len = line.size();
} else {
const int keep = kEntryBytes - marker_len;
std::memcpy(slot.data, line.constData(), static_cast<size_t>(keep));
std::memcpy(slot.data + keep, kMarker, static_cast<size_t>(marker_len));
len = kEntryBytes;
}
slot.length.store(len, std::memory_order_release);
}
QVector<QByteArray> LogRing::snapshot() const
{
const quint64 cursor = m_write_cursor.load(std::memory_order_acquire);
const quint64 cap = static_cast<quint64>(kCapacityEntries);
const quint64 count = (cursor < cap) ? cursor : cap;
const quint64 start = (cursor < cap) ? 0 : (cursor - cap);
QVector<QByteArray> result;
result.reserve(static_cast<int>(count));
for (quint64 i = 0; i < count; ++i) {
const Entry &slot = m_entries[static_cast<size_t>((start + i) % cap)];
const int len = slot.length.load(std::memory_order_acquire);
if (len > 0) {
result.append(QByteArray(slot.data, len));
}
}
return result;
}
void LogRing::dumpToFd(int fd) const noexcept
{
const quint64 cursor = m_write_cursor.load(std::memory_order_acquire);
const quint64 cap = static_cast<quint64>(kCapacityEntries);
const quint64 count = (cursor < cap) ? cursor : cap;
const quint64 start = (cursor < cap) ? 0 : (cursor - cap);
for (quint64 i = 0; i < count; ++i) {
const Entry &slot = m_entries[static_cast<size_t>((start + i) % cap)];
const int len = slot.length.load(std::memory_order_acquire);
if (len > 0) {
writeAllSignalSafe(fd, slot.data, len);
}
}
}
void LogRing::clear()
{
m_write_cursor.store(0, std::memory_order_relaxed);
for (auto &entry : m_entries) {
entry.length.store(0, std::memory_order_relaxed);
}
}
+85
View File
@@ -0,0 +1,85 @@
/*
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 LOGRING_H
#define LOGRING_H
#include <QByteArray>
#include <QVector>
#include <atomic>
#include <vector>
/**
@brief The LogRing class
Fixed-capacity, always-on in-memory ring of the most recent log
lines, preallocated once at construction -- append() never
allocates.
Lock-free by construction, not just "thread-safe": step 4 (see
crashhandler.h) reads this ring from inside a POSIX signal handler,
where taking any lock is unsafe -- if the crashing thread happens to
be the one that already holds it (or any other thread does and never
gets scheduled again), the handler hangs forever, and you lose both
the ring dump *and* the core dump. So there is no mutex here at all:
append() claims a slot with a single atomic fetch-add, and
dumpToFd()/snapshot() read the preallocated entries directly.
Accepted tradeoff: if dumpToFd() runs while another thread is
mid-append into the exact slot being read (only possible in the
crash-handler case, and only for at most one slot), that one entry
may be read torn -- part old content, part new. Every other entry is
unaffected. This is deliberate: the alternative (a seqlock or similar
to detect and retry torn reads) adds real complexity for a window
that, per discussion #644, is not worth trading "the handler must
never block" against.
*/
class LogRing
{
public:
static constexpr int kCapacityEntries = 4096;
static constexpr int kEntryBytes = 512; // 4096 * 512 = 2 MiB total
LogRing();
/// Append one already-formatted, already-truncated log line.
/// Bytes beyond kEntryBytes are dropped with a truncation marker.
/// Never allocates, never blocks. Safe to call from any normal
/// (non-signal) thread concurrently.
void append(const QByteArray &line) noexcept;
/// Snapshot of the entries currently held, oldest first. Normal
/// (non-signal) context only.
QVector<QByteArray> snapshot() const;
/// Async-signal-safe: writes every entry currently held to fd via
/// write(2) only -- no allocation, no Qt, no locks. May write a
/// torn entry under the rare race described above; never blocks.
void dumpToFd(int fd) const noexcept;
void clear();
private:
struct Entry {
char data[kEntryBytes];
std::atomic<int> length{0}; // 0 = not yet written this lap
};
std::vector<Entry> m_entries; // preallocated once, capacity fixed
std::atomic<quint64> m_write_cursor{0}; // monotonically increasing
};
#endif // LOGRING_H
+431
View File
@@ -0,0 +1,431 @@
/*
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 "qetlogger.h"
#include "crashhandler.h"
#include "../qetapp.h"
#include "../qetversion.h"
#include <QDateTime>
#include <QDir>
#include <QFileInfo>
#include <QSysInfo>
#include <cstdio>
namespace {
/**
@brief legacyStderrOutput
The QET_LOG_DISABLE=1 escape hatch. Deliberately independent of
every other function in this file -- including sanitize()/
formatLine(), which are exactly the new code a problem might be in
-- so this path stays usable even if the rest of the rework
misbehaves. No ring, no file, no rotation, no mutex.
*/
void legacyStderrOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
const QByteArray local_msg = msg.toLocal8Bit();
const char *file = context.file ? context.file : "";
const char *function = context.function ? context.function : "";
const char *level = "Unknown";
switch (type) {
case QtDebugMsg: level = "Debug"; break;
case QtInfoMsg: level = "Info"; break;
case QtWarningMsg: level = "Warning"; break;
case QtCriticalMsg: level = "Critical"; break;
case QtFatalMsg: level = "Fatal"; break;
}
fprintf(stderr, "%s: %s (%s:%u, %s)\n",
level, local_msg.constData(), file, context.line, function);
}
/**
@brief ReentrancyGuard
Sets the referenced flag on construction, clears it on destruction
(including via early return / exception unwinding). Used as the
per-thread guard against the logger recursing into itself.
*/
struct ReentrancyGuard
{
bool &flag;
explicit ReentrancyGuard(bool &f) : flag(f) {flag = true;}
~ReentrancyGuard() {flag = false;}
};
} // namespace
/**
@brief QetLogger::instance
Function-local static: guaranteed constructed exactly once, in a
thread-safe way, on first use -- but the *meaningful* initialisation
(log path resolution, opening the file) happens in init(), called
explicitly from main() at a defined point, not implicitly on
whichever thread happens to log first.
*/
QetLogger &QetLogger::instance()
{
static QetLogger logger;
return logger;
}
void QetLogger::init()
{
m_disabled = (qgetenv("QET_LOG_DISABLE") == "1");
if (m_disabled) {
return;
}
m_log_dir = QETApp::dataDir();
m_base_name = QDate::currentDate().toString(QStringLiteral("yyyyMMdd"));
QMutexLocker locker(&m_file_mutex);
m_file_output_ok = ensureFileOpenLocked();
}
void QetLogger::installCrashHandler()
{
if (m_disabled) {
return;
}
CrashHandler::install(&m_ring, crashDumpPath());
}
QString QetLogger::crashDumpPath() const
{
return m_log_dir % QStringLiteral("/crash_dump.log");
}
QString QetLogger::currentLogFilePath() const
{
return m_log_dir % QStringLiteral("/") % m_base_name % QStringLiteral(".log");
}
/**
@brief QetLogger::ensureFileOpenLocked
Caller must hold m_file_mutex. Opens the current session's log file
if not already open. Refuses to follow a pre-existing symlink at
that path, and creates the file owner-read/write only.
*/
bool QetLogger::ensureFileOpenLocked()
{
if (m_file.isOpen()) {
return true;
}
QDir().mkpath(m_log_dir);
const QString path = currentLogFilePath();
const QFileInfo info(path);
if (info.exists() && info.isSymLink()) {
// Filesystem hardening: refuse a pre-planted symlink rather than
// silently appending to whatever it points at.
return false;
}
m_file.setFileName(path);
if (!m_file.open(QIODevice::WriteOnly | QIODevice::Append)) {
return false;
}
m_file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
m_bytes_written_current_file = m_file.size();
return true;
}
QString QetLogger::rotatedPath(int index) const
{
return m_log_dir % QStringLiteral("/") % m_base_name % QStringLiteral(".") % QString::number(index) % QStringLiteral(".log");
}
/**
@brief QetLogger::rotateLocked
Caller must hold m_file_mutex. Shifts .3.log -> .4.log (dropping the
previous .4.log), .2.log -> .3.log, .1.log -> .2.log, .log -> .1.log,
then opens a fresh, empty current file.
*/
void QetLogger::rotateLocked()
{
m_file.close();
const QString base_path = currentLogFilePath();
for (int i = kRotationKeep; i >= 1; --i) {
const QString from = (i == 1) ? base_path : rotatedPath(i - 1);
const QString to = rotatedPath(i);
if (QFile::exists(to)) {
QFile::remove(to);
}
if (QFile::exists(from)) {
QFile::rename(from, to);
}
}
m_bytes_written_current_file = 0;
m_file_output_ok = ensureFileOpenLocked();
}
void QetLogger::writeToFile(const QByteArray &line, QtMsgType type)
{
QMutexLocker locker(&m_file_mutex);
if (!m_file_output_ok) {
// Write-failure policy: once file output has failed, stop
// attempting it rather than spin-retrying every message. The
// ring keeps running regardless.
return;
}
const qint64 written = m_file.write(line);
if (written != line.size()) {
m_file_output_ok = false;
m_file.close();
return;
}
m_bytes_written_current_file += written;
if (type >= QtWarningMsg) {
m_file.flush();
}
if (m_bytes_written_current_file >= kMaxFileBytes) {
rotateLocked();
}
}
/**
@brief QetLogger::sanitize
Escapes newlines, carriage returns and other control characters.
Much of what QET logs is externally controlled (file paths, element
names, font strings read out of a .qet file); left unescaped, a
crafted string containing '\n' can forge additional log lines.
Operates on already-UTF-8-encoded bytes: this is safe because UTF-8
continuation bytes are always >= 0x80, so any byte < 0x20 found here
is a genuine ASCII control character, never part of a multi-byte
sequence.
*/
QByteArray QetLogger::sanitize(const QByteArray &input)
{
QByteArray out;
out.reserve(input.size());
for (unsigned char c : input) {
if (c == '\n') {
out += "\\n";
} else if (c == '\r') {
out += "\\r";
} else if (c == '\t') {
out += static_cast<char>(c);
} else if (c < 0x20 || c == 0x7F) {
out += "\\x";
out += QByteArray::number(c, 16).rightJustified(2, '0');
} else {
out += static_cast<char>(c);
}
}
return out;
}
/**
@brief QetLogger::truncateMessage
Caps a single message at max_bytes, appending a marker stating how
many bytes were dropped, so one pathological caller (e.g. dumping an
entire XML document to qDebug()) can't consume an unbounded amount
of the ring's or file's byte budget.
*/
QByteArray QetLogger::truncateMessage(const QByteArray &input, int max_bytes)
{
if (input.size() <= max_bytes) {
return input;
}
const int dropped = input.size() - max_bytes;
QByteArray out = input.left(max_bytes);
out += " ...[truncated ";
out += QByteArray::number(dropped);
out += " bytes]";
return out;
}
QByteArray QetLogger::formatLine(QtMsgType type, const QMessageLogContext &context, const QByteArray &sanitized_msg)
{
// Includes the date (not just the time) so that a session crossing
// midnight -- now kept in a single file -- doesn't read as ambiguous.
const QByteArray timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss.zzz")).toUtf8();
const char *level = "Unknown";
switch (type) {
case QtDebugMsg: level = "Debug"; break;
case QtInfoMsg: level = "Info"; break;
case QtWarningMsg: level = "Warning"; break;
case QtCriticalMsg: level = "Critical"; break;
case QtFatalMsg: level = "Fatal"; break;
}
const char *file = context.file ? context.file : "";
const char *function = context.function ? context.function : "";
QByteArray line = timestamp;
line += ' ';
line += level;
line += ": ";
line += sanitized_msg;
if (type == QtInfoMsg) {
line += " \n";
} else {
line += " (";
line += file;
line += ":";
line += QByteArray::number(context.line ? context.line : 0);
line += ", ";
line += function;
line += ")\n";
}
return line;
}
void QetLogger::handleMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
if (m_disabled) {
legacyStderrOutput(type, context, msg);
return;
}
static thread_local bool in_handler = false;
if (in_handler) {
// The logger itself triggered a message (e.g. from inside a Qt
// call it made) -- drop it rather than recurse.
return;
}
ReentrancyGuard guard(in_handler);
const QByteArray sanitized = truncateMessage(sanitize(msg.toUtf8()), kMaxMessageBytes);
const QByteArray line = formatLine(type, context, sanitized);
fwrite(line.constData(), 1, static_cast<size_t>(line.size()), stderr);
m_ring.append(line);
writeToFile(line, type);
}
void QetLogger::pruneOldLogFiles(int days)
{
if (m_disabled) {
return;
}
const QDate today = QDate::currentDate();
const QStringList filters = {
QStringLiteral("????????.log"), // base files, e.g. 20260803.log
QStringLiteral("????????.?.log"), // rotated files, e.g. 20260803.1.log
};
const QDir dir(m_log_dir);
const auto entries = dir.entryInfoList(filters, QDir::Files);
for (const QFileInfo &file_info : entries) {
if (!file_info.isFile()) {
continue;
}
// lastModified(), not lastRead(): reading the log (opening it to
// attach to a bug report, a backup job, an indexer) must not
// reset the retention clock and keep it alive indefinitely.
if (file_info.lastModified().date().daysTo(today) > days) {
QFile::remove(file_info.absoluteFilePath());
}
}
}
// --- Step 5: getting the data back out ----------------------------------
bool QetLogger::hasPendingCrashDump() const
{
if (m_disabled) {
return false;
}
const QFileInfo info(crashDumpPath());
return info.exists() && info.isFile() && info.size() > 0;
}
QByteArray QetLogger::pendingCrashDumpContents() const
{
QFile file(crashDumpPath());
if (!file.open(QIODevice::ReadOnly)) {
return QByteArray();
}
return redact(file.readAll());
}
void QetLogger::clearPendingCrashDump()
{
QFile::remove(crashDumpPath());
}
QByteArray QetLogger::buildDiagnosticsReport() const
{
QByteArray header;
header += "QElectroTech diagnostics report\n";
header += "Generated: " % QDateTime::currentDateTime().toString(Qt::ISODate) % "\n";
header += "Version: " % QetVersion::displayedVersion() % "\n";
header += "Git: " GIT_COMMIT_SHA "\n";
header += "OS: " % QSysInfo::prettyProductName() % " (" % QSysInfo::currentCpuArchitecture() % ")\n";
header += "Qt: " QT_VERSION_STR "\n";
header += "---\n";
QByteArray body;
QFile file(currentLogFilePath());
if (file.open(QIODevice::ReadOnly)) {
body = file.readAll();
} else {
// Fall back to the in-memory ring if the file itself can't be
// read (e.g. file output already failed this session).
for (const QByteArray &line : m_ring.snapshot()) {
body += line;
}
}
return redact(header + body);
}
/**
@brief QetLogger::redact
Replaces the user's home directory with "~" wherever it appears.
Applied before a crash dump or a diagnostics report is ever shown to
the user: both are destined to be attached to a public bug tracker,
and an absolute path under the home directory leaks the account name
(discussion #644's privacy section: "/home/laurent/... leaks a
username"). This is the one redaction implemented here; the
discussion's fancier "optionally redact project filenames too" is
not attempted -- reliably telling a project path apart from
arbitrary log text is a much fuzzier problem than a literal prefix
match against a known directory.
*/
QByteArray QetLogger::redact(const QByteArray &input)
{
const QByteArray home = QDir::homePath().toUtf8();
if (home.isEmpty()) {
return input;
}
QByteArray out = input;
out.replace(home, QByteArrayLiteral("~"));
return out;
}
+159
View File
@@ -0,0 +1,159 @@
/*
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 QETLOGGER_H
#define QETLOGGER_H
#include "logring.h"
#include <QFile>
#include <QMutex>
#include <QString>
#include <QtGlobal>
/**
@brief The QetLogger class
Rework of QET's diagnostic logging (discussion #644, steps 1-3):
- Step 1: one file handle held open for the session under a mutex
instead of opening/closing per message; the log path (including
the date-stamped filename) is resolved exactly once, at init(),
instead of being recomputed on every message -- a session that
crosses midnight now stays in one file; retention now uses
lastModified() instead of lastRead(); stderr and file output both
use UTF-8 explicitly (previously stderr used the local 8-bit
codec and the file's encoding silently differed between Qt5 and
Qt6).
- Step 2: the previously-unbounded daily file is now size-capped
and rotated (kMaxFileBytes per file, kRotationKeep old files kept
beyond the current one); each message is truncated to
kMaxMessageBytes and control characters are escaped before being
written, so one pathological caller can't blow the size budget or
forge log lines; the log file is refused if it already exists as
a symlink and is created owner-read/write only.
- Step 3: every formatted line is also appended to an in-memory
LogRing (see logring.h) -- always on, fixed capacity, allocation-
free on the hot path.
- Step 4: installCrashHandler() wires the ring up to CrashHandler
(see crashhandler.h), so a SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL (or,
on Windows, an unhandled structured exception) flushes the ring to
a fixed crash-dump file before the process dies.
- Step 5: hasPendingCrashDump()/pendingCrashDumpContents()/
clearPendingCrashDump() let startup code (see QETApp::checkBackupFiles())
notice and offer an unretrieved crash dump from the *previous* run;
buildDiagnosticsReport() is the equivalent for a manual "save a
report right now" action on the *current*, still-running session.
Both go through redact() before ever reaching the user, since both
are destined for a public bug tracker.
Deliberately NOT included: log categories, a full session header
beyond what the crash dump/report already carry, repeat collapsing,
rate limiting. Those are listed in discussion #644 under "best
practices worth building in", not part of the numbered steps.
Escape hatch: if QET_LOG_DISABLE=1 is set in the environment at
init() time, this class does nothing beyond a minimal, independent
stderr passthrough -- no ring, no file, no rotation -- so a problem
in this rework can be worked around without a rebuild.
*/
class QetLogger
{
public:
static constexpr qint64 kMaxFileBytes = 2 * 1024 * 1024; // 2 MiB per file
static constexpr int kRotationKeep = 4; // .1.log .. .4.log
static constexpr int kMaxMessageBytes = 4096; // per-message truncation
static QetLogger &instance();
/// Must be called exactly once, from main(), before
/// qInstallMessageHandler(). Resolves the log directory and the
/// session's log filename, and opens the file.
void init();
/// Step 4: installs the crash handler (see crashhandler.h). Must
/// be called after init() (the ring and the dump path must exist
/// first) and, like init(), only once.
void installCrashHandler();
/// The function installed via qInstallMessageHandler() forwards here.
void handleMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg);
/// Replaces the old delete_old_log_files(): same call shape, fixed
/// to use lastModified() (not lastRead()) and to also match rotated
/// file names.
void pruneOldLogFiles(int days);
/// Snapshot of the in-memory ring, oldest first.
QVector<QByteArray> ringSnapshot() const {return m_ring.snapshot();}
// --- Step 5: getting the data back out -------------------------
/// True if a previous run's crash handler left an unretrieved
/// dump behind.
bool hasPendingCrashDump() const;
/// Raw contents of the pending crash dump, or an empty array if
/// there isn't one. Does not delete it -- call
/// clearPendingCrashDump() once it has been offered to the user.
QByteArray pendingCrashDumpContents() const;
/// Deletes the pending crash dump file. Call after the user has
/// been offered it (whether they chose to save it or not) so it
/// is never offered a second time.
void clearPendingCrashDump();
/// Builds a redacted diagnostics bundle from the *current* session
/// (header + this session's log file so far) for the manual
/// "Save report" action -- as opposed to pendingCrashDumpContents(),
/// which is about a *previous*, already-terminated session.
QByteArray buildDiagnosticsReport() const;
/// Replaces occurrences of the user's home directory with "~".
/// Applied to both the crash dump and buildDiagnosticsReport()
/// before they are ever shown to the user, since both are
/// destined for a public bug tracker.
static QByteArray redact(const QByteArray &input);
private:
QetLogger() = default;
QetLogger(const QetLogger &) = delete;
bool ensureFileOpenLocked();
void rotateLocked();
void writeToFile(const QByteArray &line, QtMsgType type);
QString rotatedPath(int index) const;
QString crashDumpPath() const;
QString currentLogFilePath() const;
static QByteArray sanitize(const QByteArray &input);
static QByteArray truncateMessage(const QByteArray &input, int max_bytes);
static QByteArray formatLine(QtMsgType type, const QMessageLogContext &context, const QByteArray &sanitized_msg);
bool m_disabled = false;
QString m_log_dir;
QString m_base_name; // e.g. "20260803", resolved once in init()
QMutex m_file_mutex;
QFile m_file;
qint64 m_bytes_written_current_file = 0;
bool m_file_output_ok = false;
LogRing m_ring;
};
#endif // QETLOGGER_H
@@ -0,0 +1,91 @@
/*
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 "diagnosticsreportdialog.h"
#include "../../qetmessagebox.h"
#include <QDialogButtonBox>
#include <QFile>
#include <QFileDialog>
#include <QFontDatabase>
#include <QLabel>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QVBoxLayout>
DiagnosticsReportDialog::DiagnosticsReportDialog(
const QString &title,
const QString &intro,
const QByteArray &content,
QWidget *parent) :
QDialog(parent)
{
setWindowTitle(title);
resize(700, 500);
auto *layout = new QVBoxLayout(this);
auto *intro_label = new QLabel(intro, this);
intro_label->setWordWrap(true);
layout->addWidget(intro_label);
auto *preview = new QPlainTextEdit(this);
preview->setReadOnly(true);
preview->setLineWrapMode(QPlainTextEdit::NoWrap);
preview->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
preview->setPlainText(QString::fromUtf8(content));
layout->addWidget(preview);
auto *buttons = new QDialogButtonBox(this);
QPushButton *save_button = buttons->addButton(tr("Enregistrer..."), QDialogButtonBox::ActionRole);
buttons->addButton(QDialogButtonBox::Close);
connect(save_button, &QPushButton::clicked, this, &DiagnosticsReportDialog::saveToFile);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(buttons->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &QDialog::accept);
layout->addWidget(buttons);
// Stash the content for saveToFile(); the preview widget already
// holds a QString copy but we save the original UTF-8 bytes to avoid
// any round-trip surprises.
setProperty("qet_report_content", content);
}
void DiagnosticsReportDialog::saveToFile()
{
const QString path = QFileDialog::getSaveFileName(
this,
tr("Enregistrer le rapport de diagnostic"),
QStringLiteral("qet-diagnostic-report.txt"),
tr("Fichiers texte (*.txt);;Tous les fichiers (*)"));
if (path.isEmpty()) {
return;
}
QFile file(path);
if (!file.open(QIODevice::WriteOnly)) {
QET::QetMessageBox::critical(
this,
tr("Erreur"),
tr("Impossible d'écrire dans le fichier « %1 ».").arg(path));
return;
}
file.write(property("qet_report_content").toByteArray());
file.close();
}
@@ -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 DIAGNOSTICSREPORTDIALOG_H
#define DIAGNOSTICSREPORTDIALOG_H
#include <QDialog>
/**
@brief The DiagnosticsReportDialog class
Discussion #644, step 5: "Show what's in it before saving -- the user
is about to attach this to a public tracker." Used for both the
after-a-crash offer (QETApp::checkBackupFiles()) and the manual
"Help > Diagnostics > Save report" action -- the only difference
between the two is the intro text and where the content comes from
(QetLogger::pendingCrashDumpContents() vs. buildDiagnosticsReport()).
The content passed in is expected to already be redacted
(QetLogger::redact()) -- this dialog just displays and optionally
saves whatever it's given.
*/
class DiagnosticsReportDialog : public QDialog
{
Q_OBJECT
public:
explicit DiagnosticsReportDialog(
const QString &title,
const QString &intro,
const QByteArray &content,
QWidget *parent = nullptr);
private slots:
void saveToFile();
};
#endif // DIAGNOSTICSREPORTDIALOG_H
+29 -123
View File
@@ -16,6 +16,8 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "cli_export.h"
#include "logging/eventloopwatchdog.h"
#include "logging/qetlogger.h"
#include "machine_info.h"
#include "qet.h"
#include "qetapp.h"
@@ -62,131 +64,16 @@ class EarlyFileOpenCatcher : public QObject
#endif
/**
@brief myMessageOutput
for debugging
@param type : the messages that can be sent to a message handler
@param context : were? wat?
@param msg : Message
@brief qetLogMessageHandler
Installed via qInstallMessageHandler(); forwards to QetLogger, which
holds all the actual formatting/ring/rotation state. See
logging/qetlogger.h for the rationale (discussion #644).
*/
void myMessageOutput(QtMsgType type,
void qetLogMessageHandler(QtMsgType type,
const QMessageLogContext &context,
const QString &msg)
{
QString txt=QTime::currentTime().toString("hh:mm:ss.zzz");
QByteArray dbs =txt.toLocal8Bit();
QByteArray localMsg = msg.toLocal8Bit();
const char *file = context.file ? context.file : "";
const char *function = context.function ? context.function : "";
switch (type) {
case QtDebugMsg:
fprintf(stderr,
"%s Debug: %s (%s:%u, %s)\n",
dbs.constData(),
localMsg.constData(),
file,
context.line,
function);
txt+=" Debug: ";
break;
case QtInfoMsg:
fprintf(stderr,
"%s Info: %s \n",
dbs.constData(),
localMsg.constData());
txt+=" Info: ";
break;
case QtWarningMsg:
fprintf(stderr,
"%s Warning: %s (%s:%u, %s)\n",
dbs.constData(),
localMsg.constData(),
file, context.line,
function);
txt+=" Warning: ";
break;
case QtCriticalMsg:
fprintf(stderr,
"%s Critical: %s (%s:%u, %s)\n",
dbs.constData(),
localMsg.constData(),
file,
context.line,
function);
txt+=" Critical: ";
break;
case QtFatalMsg:
fprintf(stderr,
"%s Fatal: %s (%s:%u, %s)\n",
dbs.constData(),
localMsg.constData(),
file,
context.line,
function);
txt+=" Fatal: ";
break;
default:
fprintf(stderr,
"%s Unknown: %s (%s:%u, %s)\n",
dbs.constData(),
localMsg.constData(),
file,
context.line,
function);
txt+=" Unknown: ";
}
txt+= msg;
if(type==QtInfoMsg){
txt+=" \n";
} else {
txt+= " (";
txt+= context.file ? context.file : "";
txt+= ":";
txt+=QString::number(context.line ? context.line :0);
txt+= ", ";
txt+= context.function ? context.function : "";
txt+=")\n";
}
QFile outFile(QETApp::dataDir()
+"/"
+QDate::currentDate().toString("yyyyMMdd")
+".log");
if(outFile.open(QIODevice::WriteOnly | QIODevice::Append))
{
QTextStream ts(&outFile);
ts << txt;
}
outFile.close();
}
/**
@brief delete_old_log_files
delete old log files
@param days : max days old
*/
void delete_old_log_files(int days)
{
const QDate today = QDate::currentDate();
const QString path = QETApp::dataDir() % "/";
QString filter("%1%1%1%1%1%1%1%1.log"); // pattern
filter = filter.arg("[0123456789]"); // valid characters
Q_FOREACH (auto fileInfo,
QDir(path).entryInfoList(
QStringList(filter),
QDir::Files))
{
if (fileInfo.lastRead().date().daysTo(today) > days)
{
QString filepath = fileInfo.absoluteFilePath();
QDir deletefile;
deletefile.setPath(filepath);
deletefile.remove(filepath);
qDebug() << "File " % filepath % " is deleted!";
}
}
QetLogger::instance().handleMessage(type, context, msg);
}
/**
@@ -253,13 +140,25 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
}
}
// Resolve the logger's state (log directory, session filename, open
// file handle) explicitly here, immediately before installing the
// handler -- not implicitly on whichever thread happens to log
// first. See QetLogger::init().
//
// Install the log-file message handler BEFORE the application starts:
// QETApp's constructor does the whole startup (collections, editor,
// opening the projects given on the command line), so installing the
// handler afterwards - as was done in the startup worker below - meant
// exactly the interesting lines (collection and project load timers)
// went to stderr, which is invisible in a Windows GUI session.
qInstallMessageHandler(myMessageOutput);
QetLogger::instance().init();
qInstallMessageHandler(qetLogMessageHandler);
// Step 4 (discussion #644): flush the ring to a crash-dump file if
// the process dies from here on. Installed right after the ring
// exists (init() just constructed it) and as early as reasonably
// possible, so it also covers whatever runs between here and
// QETApp's own construction below.
QetLogger::instance().installCrashHandler();
SingleApplication app(argc, argv, true);
#ifdef Q_OS_MACOS
@@ -308,9 +207,16 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
{
qInfo("Start-up");
// delete old log files of max 7 days old.
delete_old_log_files(7);
QetLogger::instance().pruneOldLogFiles(7);
MachineInfo::instance()->send_info_to_debug();
});
// Constructed here rather than earlier: start() measures ticks against
// the event loop app.exec() is about to run, so there is no point
// (and no accurate baseline) before this line.
EventLoopWatchdog watchdog;
watchdog.start();
return app.exec();
}
+1 -1
View File
@@ -842,7 +842,7 @@ void ProjectView::initWidgets()
QHBoxLayout *TopRightCorner_Layout = new QHBoxLayout();
TopRightCorner_Layout->setContentsMargins(0,0,0,0);
// some place left to the 'next_right_view_button' button
TopRightCorner_Layout->insertSpacing(1,10);
TopRightCorner_Layout->addSpacing(10);
QHBoxLayout *TopLeftCorner_Layout = new QHBoxLayout();
TopLeftCorner_Layout->setContentsMargins(0,0,0,0);
+205
View File
@@ -267,6 +267,211 @@ QDomElement ElementData::kindInfoToXml(QDomDocument &document)
return returned_elmt;
}
/**
* @brief ElementData::plcMasterDataToXml
* Serialize PLC master data to XML (used by Element::toXml for diagram instances)
*/
QDomElement ElementData::plcMasterDataToXml(QDomDocument &document) const
{
auto xml_plc = document.createElement(QStringLiteral("plcMasterData"));
xml_plc.setAttribute(QStringLiteral("rowHeight"),
QString::number(m_plc_master_data.rowHeight, 'f', 2));
// Save break positions
{
auto xml_breaks = document.createElement(QStringLiteral("breakPositions"));
for (int bp : m_plc_master_data.breakPositions) {
auto xml_bp = document.createElement(QStringLiteral("break"));
xml_bp.appendChild(document.createTextNode(QString::number(bp)));
xml_breaks.appendChild(xml_bp);
}
xml_plc.appendChild(xml_breaks);
}
// Save column widths
auto xml_col_widths = document.createElement(QStringLiteral("columnWidths"));
for (auto it = m_plc_master_data.colWidths.constBegin();
it != m_plc_master_data.colWidths.constEnd(); ++it) {
auto xml_col = document.createElement(QStringLiteral("column"));
xml_col.setAttribute(QStringLiteral("index"), it.key());
xml_col.setAttribute(QStringLiteral("width"), QString::number(it.value(), 'f', 2));
xml_col_widths.appendChild(xml_col);
}
xml_plc.appendChild(xml_col_widths);
// Save column visibility
auto xml_col_vis = document.createElement(QStringLiteral("columnVisibility"));
for (auto it = m_plc_master_data.colVisible.constBegin();
it != m_plc_master_data.colVisible.constEnd(); ++it) {
auto xml_col = document.createElement(QStringLiteral("column"));
xml_col.setAttribute(QStringLiteral("index"), it.key());
xml_col.setAttribute(QStringLiteral("visible"), it.value() ? "true" : "false");
xml_col_vis.appendChild(xml_col);
}
xml_plc.appendChild(xml_col_vis);
// Save fonts
if (!m_plc_master_data.headerFont.family().isEmpty()) {
auto xml_hfont = document.createElement(QStringLiteral("headerFont"));
xml_hfont.setAttribute(QStringLiteral("family"), m_plc_master_data.headerFont.family());
xml_hfont.setAttribute(QStringLiteral("size"), m_plc_master_data.headerFont.pointSize());
xml_hfont.setAttribute(QStringLiteral("bold"), m_plc_master_data.headerFont.bold() ? "true" : "false");
xml_plc.appendChild(xml_hfont);
}
if (!m_plc_master_data.cellFont.family().isEmpty()) {
auto xml_cfont = document.createElement(QStringLiteral("cellFont"));
xml_cfont.setAttribute(QStringLiteral("family"), m_plc_master_data.cellFont.family());
xml_cfont.setAttribute(QStringLiteral("size"), m_plc_master_data.cellFont.pointSize());
xml_cfont.setAttribute(QStringLiteral("bold"), m_plc_master_data.cellFont.bold() ? "true" : "false");
xml_plc.appendChild(xml_cfont);
}
// Save custom column names
if (!m_plc_master_data.columnNames.isEmpty()) {
auto xml_names = document.createElement(QStringLiteral("columnNames"));
for (int i = 0; i < m_plc_master_data.columnNames.size(); ++i) {
auto xml_name = document.createElement(QStringLiteral("column"));
xml_name.setAttribute(QStringLiteral("index"), i);
xml_name.appendChild(document.createTextNode(m_plc_master_data.columnNames.at(i)));
xml_names.appendChild(xml_name);
}
xml_plc.appendChild(xml_names);
}
// Save column order
if (!m_plc_master_data.columnOrder.isEmpty()) {
auto xml_order = document.createElement(QStringLiteral("columnOrder"));
QString order_str;
for (int i = 0; i < m_plc_master_data.columnOrder.size(); ++i) {
if (i > 0) order_str += QStringLiteral(",");
order_str += QString::number(m_plc_master_data.columnOrder.at(i));
}
xml_order.appendChild(document.createTextNode(order_str));
xml_plc.appendChild(xml_order);
}
// Save showHeaders
{
auto xml_sh = document.createElement(QStringLiteral("showHeaders"));
xml_sh.appendChild(document.createTextNode(
m_plc_master_data.showHeaders ? QStringLiteral("1") : QStringLiteral("0")));
xml_plc.appendChild(xml_sh);
}
// Save IO entries
auto xml_ios = document.createElement(QStringLiteral("plcIOs"));
for (const auto &io : m_plc_master_data.ios) {
auto xml_io = document.createElement(QStringLiteral("plcIO"));
xml_io.setAttribute(QStringLiteral("type"), plcIOTypeToString(io.type));
xml_io.setAttribute(QStringLiteral("address"), io.address);
xml_io.setAttribute(QStringLiteral("functionText"), io.functionText);
xml_io.setAttribute(QStringLiteral("comment"), io.comment);
xml_io.setAttribute(QStringLiteral("crossRef"), io.crossRef);
xml_io.setAttribute(QStringLiteral("terminalCount"), io.terminalCount);
for (const auto &t : io.terminals) {
auto xml_term = document.createElement(QStringLiteral("terminal"));
xml_term.appendChild(document.createTextNode(t));
xml_io.appendChild(xml_term);
}
xml_ios.appendChild(xml_io);
}
xml_plc.appendChild(xml_ios);
return xml_plc;
}
/**
* @brief ElementData::plcMasterDataFromXml
* Deserialize PLC master data from XML
*/
void ElementData::plcMasterDataFromXml(const QDomElement &xml_plc)
{
if (xml_plc.isNull())
return;
// Reset PLC data before loading to avoid appending to existing data
m_plc_master_data = PlcMasterData();
m_plc_master_data.rowHeight = xml_plc.attribute(
QStringLiteral("rowHeight"), QStringLiteral("8.0")).toDouble();
// Load break positions
auto xml_breaks = xml_plc.firstChildElement(QStringLiteral("breakPositions"));
for (const auto &xml_bp : QETXML::findInDomElement(xml_breaks, QStringLiteral("break"))) {
m_plc_master_data.breakPositions.append(xml_bp.text().toInt());
}
// Load column widths
auto xml_col_widths = xml_plc.firstChildElement(QStringLiteral("columnWidths"));
for (const auto &xml_col : QETXML::findInDomElement(xml_col_widths, QStringLiteral("column"))) {
int idx = xml_col.attribute(QStringLiteral("index")).toInt();
qreal w = xml_col.attribute(QStringLiteral("width")).toDouble();
m_plc_master_data.colWidths.insert(idx, w);
}
// Load column visibility
auto xml_col_vis = xml_plc.firstChildElement(QStringLiteral("columnVisibility"));
for (const auto &xml_col : QETXML::findInDomElement(xml_col_vis, QStringLiteral("column"))) {
int idx = xml_col.attribute(QStringLiteral("index")).toInt();
bool vis = xml_col.attribute(QStringLiteral("visible")) == QLatin1String("true");
m_plc_master_data.colVisible.insert(idx, vis);
}
// Load fonts
auto xml_hfont = xml_plc.firstChildElement(QStringLiteral("headerFont"));
if (!xml_hfont.isNull()) {
m_plc_master_data.headerFont.setFamily(xml_hfont.attribute(QStringLiteral("family")));
m_plc_master_data.headerFont.setPointSize(xml_hfont.attribute(QStringLiteral("size")).toInt());
m_plc_master_data.headerFont.setBold(xml_hfont.attribute(QStringLiteral("bold")) == QLatin1String("true"));
}
auto xml_cfont = xml_plc.firstChildElement(QStringLiteral("cellFont"));
if (!xml_cfont.isNull()) {
m_plc_master_data.cellFont.setFamily(xml_cfont.attribute(QStringLiteral("family")));
m_plc_master_data.cellFont.setPointSize(xml_cfont.attribute(QStringLiteral("size")).toInt());
m_plc_master_data.cellFont.setBold(xml_cfont.attribute(QStringLiteral("bold")) == QLatin1String("true"));
}
// Load custom column names
auto xml_names = xml_plc.firstChildElement(QStringLiteral("columnNames"));
for (const auto &xml_col : QETXML::findInDomElement(xml_names, QStringLiteral("column"))) {
int idx = xml_col.attribute(QStringLiteral("index")).toInt();
while (m_plc_master_data.columnNames.size() <= idx)
m_plc_master_data.columnNames.append(QString());
m_plc_master_data.columnNames.replace(idx, xml_col.text());
}
// Load column order
auto xml_order = xml_plc.firstChildElement(QStringLiteral("columnOrder"));
if (!xml_order.isNull()) {
QStringList order_str_list = xml_order.text().split(',');
for (const auto &s : order_str_list) {
m_plc_master_data.columnOrder.append(s.trimmed().toInt());
}
}
// Load showHeaders
auto xml_sh = xml_plc.firstChildElement(QStringLiteral("showHeaders"));
if (!xml_sh.isNull()) {
m_plc_master_data.showHeaders = xml_sh.text() == QLatin1String("1");
}
// Load IO entries
auto xml_ios = xml_plc.firstChildElement(QStringLiteral("plcIOs"));
for (const auto &xml_io : QETXML::findInDomElement(xml_ios, QStringLiteral("plcIO"))) {
PlcIO io;
io.type = plcIOTypeFromString(xml_io.attribute(QStringLiteral("type")));
io.address = xml_io.attribute(QStringLiteral("address"));
io.functionText = xml_io.attribute(QStringLiteral("functionText"));
io.comment = xml_io.attribute(QStringLiteral("comment"));
io.crossRef = xml_io.attribute(QStringLiteral("crossRef"));
io.terminalCount = xml_io.attribute(QStringLiteral("terminalCount")).toInt();
for (const auto &xml_term : QETXML::findInDomElement(xml_io, QStringLiteral("terminal"))) {
io.terminals.append(xml_term.text());
}
m_plc_master_data.ios.append(io);
}
}
/**
* @brief ElementData::setTerminalType
* Override the terminal type by \p t_type
+7
View File
@@ -130,6 +130,11 @@ class ElementData : public PropertiesInterface
QList<int> columnOrder; ///< Column display order (logical indices)
bool showHeaders = true; ///< Show column headers on sheet
PlcMasterData() {
headerFont.setFamily(QString());
cellFont.setFamily(QString());
}
bool operator==(const PlcMasterData &other) const {
return ios == other.ios
&& breakPositions == other.breakPositions
@@ -201,6 +206,8 @@ class ElementData : public PropertiesInterface
QDomElement toXml(QDomDocument &xml_element) const override;
bool fromXml(const QDomElement &xml_element) override;
QDomElement kindInfoToXml(QDomDocument &document);
QDomElement plcMasterDataToXml(QDomDocument &document) const;
void plcMasterDataFromXml(const QDomElement &xml_plc);
void setTerminalType(ElementData::TerminalType t_type);
ElementData::TerminalType terminalType() const;
+2
View File
@@ -17,6 +17,7 @@
*/
#include "terminaldata.h"
#include "../qetapp.h"
#include "../utils/qetutils.h"
#include <QGraphicsObject>
@@ -38,6 +39,7 @@ TerminalData::TerminalData(QGraphicsObject *parent):
void TerminalData::init()
{
m_label_font = QETApp::diagramTextsFont();
}
TerminalData::~TerminalData()
+53
View File
@@ -40,6 +40,8 @@
#include "machine_info.h"
#include "TerminalStrip/ui/terminalstripeditorwindow.h"
#include "qetversion.h"
#include "logging/qetlogger.h"
#include "logging/ui/diagnosticsreportdialog.h"
#include <cstdlib>
#include <iostream>
@@ -2575,6 +2577,10 @@ void QETApp::checkBackupFiles()
}
if (stale_files.isEmpty()) {
// Only offer an unretrieved crash dump when there's no project
// to recover this run -- discussion #644 step 5 is explicit
// that the two prompts must never both show at once.
checkCrashDump();
return;
}
@@ -2628,6 +2634,53 @@ void QETApp::checkBackupFiles()
}
}
/**
@brief QETApp::checkCrashDump
Discussion #644, step 5: if the crash handler (step 4) left an
unretrieved dump from a previous run, offer it to the user. Only
called from checkBackupFiles() when there was no stale project file
to recover this run, so the two prompts never both show at once.
*/
void QETApp::checkCrashDump()
{
QetLogger &logger = QetLogger::instance();
if (!logger.hasPendingCrashDump()) {
return;
}
const QByteArray content = logger.pendingCrashDumpContents();
DiagnosticsReportDialog dialog(
tr("Rapport de plantage"),
tr("QElectroTech ne s'est pas fermé correctement lors de sa dernière exécution.\n"
"Voici les derniers messages enregistrés avant l'arrêt -- vous pouvez les "
"enregistrer pour les joindre à un rapport de bug."),
content);
dialog.exec();
// Offered once, then marked retrieved -- regardless of whether the
// user chose to save it -- so it is never offered a second time.
logger.clearPendingCrashDump();
}
/**
@brief QETApp::showDiagnosticsReport
Discussion #644, step 5: the manual "Help > Diagnostics > Save
report" action. Unlike checkCrashDump(), this is about the *current*,
still-running session, not a previous one.
*/
void QETApp::showDiagnosticsReport()
{
const QByteArray content = QetLogger::instance().buildDiagnosticsReport();
DiagnosticsReportDialog dialog(
tr("Rapport de diagnostic"),
tr("Ceci contient les derniers messages de journalisation de cette session. "
"Vérifiez le contenu avant de le joindre à un rapport de bug public."),
content);
dialog.exec();
}
/**
@brief QETApp::fetchWindowStats
Updates the booleans concerning the state of the windows
+2
View File
@@ -271,6 +271,7 @@ class QETApp : public QObject
void openTitleBlockTemplateFiles(const QStringList &);
void configureQET();
void aboutQET();
void showDiagnosticsReport();
void receiveMessage(int instanceId, QByteArray message);
private:
@@ -287,6 +288,7 @@ class QETApp : public QObject
void initSystemTray();
void buildSystemTrayMenu();
void checkBackupFiles();
void checkCrashDump();
void fetchWindowStats(
const QList<QETDiagramEditor *> &,
const QList<QETElementEditor *> &,
+23
View File
@@ -185,6 +185,7 @@ void QETDiagramEditor::setUpElementsPanel()
connect(pa, SIGNAL(requestForProjectClosing (QETProject *)), this, SLOT(closeProject(QETProject *)));
connect(pa, SIGNAL(requestForProjectPropertiesEdition (QETProject *)), this, SLOT(editProjectProperties(QETProject *)));
connect(pa, SIGNAL(requestForNewDiagram (QETProject *)), this, SLOT(addDiagramToProject(QETProject *)));
connect(pa, SIGNAL(requestForNewDiagramAt (QETProject *, int)), this, SLOT(addDiagramToProjectAt(QETProject *, int)));
connect(pa, SIGNAL(requestForDiagramPropertiesEdition (Diagram *)), this, SLOT(editDiagramProperties(Diagram *)));
connect(pa, SIGNAL(requestForDiagramsDeletion (const QList<Diagram *> &)), this, SLOT(removeDiagrams(const QList<Diagram *> &)));
connect(pa, SIGNAL(requestForDiagramMoveUp (const QList<Diagram *> &)), this, SLOT(moveDiagramUp(const QList<Diagram *>&)));
@@ -897,6 +898,8 @@ void QETDiagramEditor::setUpMenu()
// menu Projet
menu_project -> addAction(m_project_edit_properties);
menu_project -> addAction(m_auto_conductor);
menu_project -> addSeparator();
menu_project -> addAction(m_project_add_diagram);
menu_project -> addAction(m_remove_diagram_from_project);
menu_project -> addAction(m_clean_project);
@@ -932,6 +935,7 @@ void QETDiagramEditor::setUpMenu()
menu_affichage -> addAction(m_mode_visualise);
menu_affichage -> addSeparator();
menu_affichage -> addAction(m_draw_grid);
menu_affichage -> addAction(m_draw_guides);
menu_affichage -> addAction(m_grey_background);
menu_affichage -> addSeparator();
menu_affichage -> addActions(m_zoom_actions_group.actions());
@@ -2295,6 +2299,25 @@ void QETDiagramEditor::addDiagramToProject(QETProject *project)
project_view->project()->addNewDiagram();
}
}
/**
@brief QETDiagramEditor::addDiagramToProjectAt
Add a diagram to project, inserted at a specific position.
@param project
@param pos
*/
void QETDiagramEditor::addDiagramToProjectAt(QETProject *project, int pos)
{
if (!project) {
return;
}
if (ProjectView *project_view = findProject(project))
{
activateProject(project);
project_view->project()->addNewDiagram(pos);
}
}
/**
* @brief QETDiagramEditor::removeDiagram
* Wrapper für einzelne Diagramme, um Abwärtskompatibilität zu erhalten.
+1 -1
View File
@@ -137,6 +137,7 @@ class QETDiagramEditor : public QETMainWindow
void editDiagramProperties(DiagramView *);
void editDiagramProperties(Diagram *);
void addDiagramToProject(QETProject *);
void addDiagramToProjectAt(QETProject *, int);
void removeDiagram(Diagram *);
void removeDiagrams(const QList<Diagram *> &diagrams);
void removeDiagramFromProject();
@@ -200,7 +201,6 @@ class QETDiagramEditor : public QETMainWindow
*m_project_add_diagram, ///< Add a diagram to the current project.
*m_remove_diagram_from_project, ///< Delete a diagram from the current project
*m_clean_project, ///< Clean the content of the current project by removing useless items
*m_project_folio_list, ///< Sommaire des schemas
*m_csv_export, ///< generate nomenclature
*m_add_nomenclature, ///< Add nomenclature graphics item;
*m_add_summary, ///<Add summary graphics item
+38 -5
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "element.h"
#include "../qetapp.h"
#include "../qetproject.h"
#include "../PropertiesEditor/propertieseditordialog.h"
#include "../autoNum/assignvariables.h"
@@ -33,6 +34,7 @@
#include "../qetgraphicsitem/terminal.h"
#include "../ui/elementpropertieswidget.h"
#include "../undocommand/changeelementinformationcommand.h"
#include "../undocommand/setautonumcontextcommand.h"
#include "dynamicelementtextitem.h"
#include "elementtextitemgroup.h"
#include "iostream"
@@ -860,6 +862,15 @@ bool Element::fromXml(QDomElement &e,
}
}
//Load PLC master data override from diagram XML
if (m_data.m_type == ElementData::Master &&
m_data.m_master_type == ElementData::PLC)
{
auto xml_plc = e.firstChildElement(QStringLiteral("plcMasterData"));
if (!xml_plc.isNull())
m_data.plcMasterDataFromXml(xml_plc);
}
//We must block the update of the alignment when loading the information
//otherwise the pos of the text will not be the same as it was at save time.
for(DynamicElementTextItem *deti : m_dynamic_text_list)
@@ -993,6 +1004,15 @@ QDomElement Element::toXml(
element.appendChild(properties);
}
//Save PLC master data override for elements on diagram
if (m_data.m_type == ElementData::Master &&
m_data.m_master_type == ElementData::PLC)
{
auto xml_plc = m_data.plcMasterDataToXml(document);
if (!xml_plc.isNull())
element.appendChild(xml_plc);
}
//Dynamic texts
QDomElement dyn_text = document.createElement(QStringLiteral("dynamic_texts"));
for (DynamicElementTextItem *deti : m_dynamic_text_list)
@@ -1626,7 +1646,7 @@ void Element::hoverLeaveEvent(QGraphicsSceneHoverEvent *e)
(ex K for coil) with condition :
formula is empty, text tagged "label" is emptty or "_";
*/
void Element::setUpFormula(bool code_letter)
void Element::setUpFormula(bool code_letter, QUndoCommand *parent_undo)
{
Q_UNUSED(code_letter)
@@ -1655,8 +1675,21 @@ void Element::setUpFormula(bool code_letter)
nc,
diagram(),
element_currentAutoNum);
diagram()->project()->addElementAutoNum(element_currentAutoNum,
ncc.next());
NumerotationContext new_context = ncc.next();
QETProject *project = diagram()->project();
auto setter = [project](const QString &k, const NumerotationContext &c) {project->addElementAutoNum(k, c);};
if (parent_undo)
{
new SetAutoNumContextCommand(setter, element_currentAutoNum, nc, new_context, parent_undo);
}
else
{
auto *undo = new SetAutoNumContextCommand(setter, element_currentAutoNum, nc, new_context);
undo->setText(tr("Numéroter automatiquement un élément", "undo caption"));
diagram()->undoStack().push(undo);
}
if(!m_freeze_label && !formula.isEmpty())
{
@@ -1876,12 +1909,12 @@ void Element::drawPlcTable(QPainter *painter)
// Fonts
QFont header_font = plc_data.headerFont;
if (header_font.family().isEmpty()) {
header_font = painter->font();
header_font = QETApp::diagramTextsFont();
header_font.setBold(true);
}
QFont cell_font = plc_data.cellFont;
if (cell_font.family().isEmpty()) {
cell_font = painter->font();
cell_font = QETApp::diagramTextsFont();
}
for (const QPointF &pos : positions) {
+2 -1
View File
@@ -35,6 +35,7 @@ class Terminal;
class Conductor;
class DynamicElementTextItem;
class ElementTextItemGroup;
class QUndoCommand;
/**
This is the base class for electrical elements.
@@ -142,7 +143,7 @@ class Element : public QetGraphicsItem
{return m_autoNum_seq;}
autonum::sequentialNumbers& rSequenceStruct()
{return m_autoNum_seq;}
void setUpFormula(bool code_letter = true);
void setUpFormula(bool code_letter = true, QUndoCommand *parent_undo = nullptr);
void setPrefix(QString);
QString getPrefix() const;
void freezeLabel(bool freeze);
+8
View File
@@ -136,6 +136,12 @@ void QETMainWindow::initCommonActions()
about_qt_ = new QAction(QET::Icons::QtLogo, tr("À propos de &Qt"), this);
about_qt_ -> setStatusTip(tr("Affiche des informations sur la bibliothèque Qt", "status bar tip"));
connect(about_qt_, SIGNAL(triggered()), qApp, SLOT(aboutQt()));
diagnostics_action_ = new QAction(QET::Icons::DialogInformation, tr("Enregistrer un rapport de diagnostic..."), this);
diagnostics_action_ -> setStatusTip(tr("Génère un rapport avec les derniers messages de journalisation, pour l'inclure dans un rapport de bug", "status bar tip"));
connect(diagnostics_action_, &QAction::triggered, this, []() {
QETApp::instance()->showDiagnosticsReport();
});
}
/**
@@ -158,6 +164,8 @@ void QETMainWindow::initCommonMenus()
help_menu_ -> addAction(donate_);
help_menu_ -> addAction(about_qt_);
help_menu_ -> addAction(about_qet_);
help_menu_ -> addSeparator();
help_menu_ -> addAction(diagnostics_action_);
#ifdef Q_OS_WIN32
upgrade_ -> setVisible(true);
+2 -1
View File
@@ -60,8 +60,9 @@ class QETMainWindow : public QMainWindow {
QAction *youtube_; ///< Launch browser on QElectroTech Youtube channel
QAction *upgrade_; ///< Launch browser on QElectroTech Windows Nightly builds
QAction *upgrade_M; ///< Launch browser on QElectroTech MAC_OS_X builds
QAction *donate_; ///< Launch browser to donate link
QAction *donate_; ///< Launch browser to donate link
QAction *about_qt_; ///< launch the "About Qt" dialog
QAction *diagnostics_action_; ///< Open the diagnostics report dialog (discussion #644, step 5)
QMenu *settings_menu_; ///< Settings menu
QMenu *help_menu_; ///< Help menu
QMenu *display_toolbars_; ///< Show/hide toolbars/docks
+3
View File
@@ -730,6 +730,7 @@ QHash <QString, NumerotationContext> QETProject::folioAutoNum() const
*/
void QETProject::addConductorAutoNum(const QString& key, const NumerotationContext& context) {
m_conductor_autonum.insert(key, context);
emit autoNumContextUpdated();
}
/**
@@ -743,6 +744,7 @@ void QETProject::addElementAutoNum(const QString& key, const NumerotationContext
{
m_element_autonum.insert(key, context);
emit elementAutoNumAdded(key);
emit autoNumContextUpdated();
}
/**
@@ -754,6 +756,7 @@ void QETProject::addElementAutoNum(const QString& key, const NumerotationContext
*/
void QETProject::addFolioAutoNum(const QString& key, const NumerotationContext& context) {
m_folio_autonum.insert(key, context);
emit autoNumContextUpdated();
}
/**
+6
View File
@@ -237,6 +237,12 @@ class QETProject : public QObject
void conductorAutoNumAdded();
void conductorAutoNumRemoved();
void folioAutoNumAdded();
/// A numerotation context's *values* changed -- as happens every
/// time an element or conductor consumes the next number, not
/// only when a rule is added or removed. Deliberately separate
/// from the *Added/*Removed signals above, which make listeners
/// rebuild their rule lists; this one just says "re-read me".
void autoNumContextUpdated();
void folioAutoNumRemoved();
void folioAutoNumChanged(QString);
void defaultTitleBlockPropertiesChanged();
-117
View File
@@ -1,117 +0,0 @@
/********************************************************************************
** Form generated from reading UI file 'addlinkdialog.ui'
**
** Created: Thu 4. Apr 17:13:59 2013
** by: Qt User Interface Compiler version 4.8.4
**
** WARNING! All changes made in this file will be lost when recompiling UI file!
********************************************************************************/
#ifndef UI_ADDLINKDIALOG_H
#define UI_ADDLINKDIALOG_H
#include <QtCore/QVariant>
#include <QAction>
#include <QApplication>
#include <QButtonGroup>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QFrame>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QSpacerItem>
#include <QVBoxLayout>
QT_BEGIN_NAMESPACE
class Ui_AddLinkDialog
{
public:
QVBoxLayout *verticalLayout;
QFormLayout *formLayout;
QLabel *label;
QLineEdit *titleInput;
QLabel *label_2;
QLineEdit *urlInput;
QSpacerItem *verticalSpacer;
QFrame *line;
QDialogButtonBox *buttonBox;
void setupUi(QDialog *AddLinkDialog)
{
if (AddLinkDialog->objectName().isEmpty())
AddLinkDialog->setObjectName(QString::fromUtf8("AddLinkDialog"));
AddLinkDialog->setSizeGripEnabled(false);
AddLinkDialog->setModal(true);
verticalLayout = new QVBoxLayout(AddLinkDialog);
verticalLayout->setObjectName(QString::fromUtf8("verticalLayout"));
formLayout = new QFormLayout();
formLayout->setObjectName(QString::fromUtf8("formLayout"));
label = new QLabel(AddLinkDialog);
label->setObjectName(QString::fromUtf8("label"));
formLayout->setWidget(0, QFormLayout::LabelRole, label);
titleInput = new QLineEdit(AddLinkDialog);
titleInput->setObjectName(QString::fromUtf8("titleInput"));
titleInput->setMinimumSize(QSize(337, 0));
formLayout->setWidget(0, QFormLayout::FieldRole, titleInput);
label_2 = new QLabel(AddLinkDialog);
label_2->setObjectName(QString::fromUtf8("label_2"));
formLayout->setWidget(1, QFormLayout::LabelRole, label_2);
urlInput = new QLineEdit(AddLinkDialog);
urlInput->setObjectName(QString::fromUtf8("urlInput"));
formLayout->setWidget(1, QFormLayout::FieldRole, urlInput);
verticalLayout->addLayout(formLayout);
verticalSpacer = new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding);
verticalLayout->addItem(verticalSpacer);
line = new QFrame(AddLinkDialog);
line->setObjectName(QString::fromUtf8("line"));
line->setFrameShape(QFrame::HLine);
line->setFrameShadow(QFrame::Sunken);
verticalLayout->addWidget(line);
buttonBox = new QDialogButtonBox(AddLinkDialog);
buttonBox->setObjectName(QString::fromUtf8("buttonBox"));
buttonBox->setOrientation(Qt::Horizontal);
buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok);
verticalLayout->addWidget(buttonBox);
retranslateUi(AddLinkDialog);
QObject::connect(buttonBox, SIGNAL(accepted()), AddLinkDialog, SLOT(accept()));
QObject::connect(buttonBox, SIGNAL(rejected()), AddLinkDialog, SLOT(reject()));
QMetaObject::connectSlotsByName(AddLinkDialog);
} // setupUi
void retranslateUi(QDialog *AddLinkDialog)
{
AddLinkDialog->setWindowTitle(QApplication::translate("AddLinkDialog", "Insert Link", nullptr));
label->setText(QApplication::translate("AddLinkDialog", "Title:", nullptr));
label_2->setText(QApplication::translate("AddLinkDialog", "URL:", nullptr));
} // retranslateUi
};
namespace Ui {
class AddLinkDialog: public Ui_AddLinkDialog {};
} // namespace Ui
QT_END_NAMESPACE
#endif // UI_ADDLINKDIALOG_H
+5 -1
View File
@@ -214,7 +214,11 @@ void NewDiagramPage::applyConf()
rpw->toSettings(settings, "diagrameditor/defaultreport");
// default xref properties
QHash <QString, XRefProperties> hash_xrp = xrefpw -> properties();
const QHash<QString, XRefProperties> hash_xrp = xrefpw->properties();
for (auto it = hash_xrp.constBegin() ; it != hash_xrp.constEnd() ; ++it) {
it.value().toSettings(settings,
QStringLiteral("diagrameditor/defaultxref") % it.key());
}
// Global in QSettings speichern
QList<Diagram::Guide> current_guides = m_gpw->guides();
+113
View File
@@ -0,0 +1,113 @@
/*
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 "customelementinfopartwidget.h"
#include "../diagramcontext.h"
#include "../qeticons.h"
#include <QGridLayout>
#include <QLineEdit>
#include <QToolButton>
/**
@brief CustomElementInfoPartWidget::CustomElementInfoPartWidget
Constructor
@param key initial key name (empty for a freshly added row)
@param value initial value
@param parent parent widget
*/
CustomElementInfoPartWidget::CustomElementInfoPartWidget(
const QString &key,
const QString &value,
QWidget *parent) :
QWidget(parent),
m_key_edit(new QLineEdit(key, this)),
m_value_edit(new QLineEdit(value, this)),
m_remove_button(new QToolButton(this))
{
m_key_edit->setPlaceholderText(tr("nom_de_la_propriete"));
m_key_edit->setToolTip(tr("Lettres minuscules, chiffres, tiret et underscore uniquement"));
m_value_edit->setClearButtonEnabled(true);
m_remove_button->setIcon(QET::Icons::Remove);
m_remove_button->setToolTip(tr("Supprimer cette propriété"));
m_remove_button->setAutoRaise(true);
auto *layout = new QGridLayout(this);
layout->setContentsMargins(0, 2, 0, 2);
layout->setVerticalSpacing(2);
layout->setHorizontalSpacing(0);
layout->addWidget(m_key_edit, 0, 0);
layout->addWidget(m_value_edit, 1, 0);
layout->addWidget(m_remove_button, 0, 1, 2, 1);
connect(m_key_edit, &QLineEdit::textChanged, this, &CustomElementInfoPartWidget::validateKey);
connect(m_key_edit, &QLineEdit::textChanged, this, &CustomElementInfoPartWidget::changed);
connect(m_value_edit, &QLineEdit::textChanged, this, &CustomElementInfoPartWidget::changed);
connect(m_remove_button, &QToolButton::clicked, this, [this]() {
emit removeRequested(this);
});
setFocusProxy(m_key_edit);
validateKey();
}
CustomElementInfoPartWidget::~CustomElementInfoPartWidget()
{
}
/**
@return the key name currently typed in this row
*/
QString CustomElementInfoPartWidget::key() const
{
return m_key_edit->text().trimmed();
}
/**
@return the value currently typed in this row
*/
QString CustomElementInfoPartWidget::value() const
{
return m_value_edit->text();
}
/**
@return true if the typed key is non-empty and matches
DiagramContext::isKeyAcceptable()
*/
bool CustomElementInfoPartWidget::hasValidKey() const
{
const QString k = key();
return !k.isEmpty() && DiagramContext::isKeyAcceptable(k);
}
/**
@brief CustomElementInfoPartWidget::validateKey
Flag the key field when it doesn't match the accepted format,
instead of silently dropping it later.
*/
void CustomElementInfoPartWidget::validateKey()
{
const QString k = key();
if (k.isEmpty() || DiagramContext::isKeyAcceptable(k)) {
m_key_edit->setStyleSheet(QString());
} else {
m_key_edit->setStyleSheet(QStringLiteral("border: 1px solid red;"));
}
}
+61
View File
@@ -0,0 +1,61 @@
/*
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 CUSTOMELEMENTINFOPARTWIDGET_H
#define CUSTOMELEMENTINFOPARTWIDGET_H
#include <QWidget>
class QLineEdit;
class QToolButton;
/**
@brief The CustomElementInfoPartWidget class
A single row letting the user define their own element information
key/value pair, unlike ElementInfoPartWidget which is bound to one
predefined key. The key is validated against
DiagramContext::isKeyAcceptable() as the user types.
*/
class CustomElementInfoPartWidget : public QWidget
{
Q_OBJECT
public:
explicit CustomElementInfoPartWidget(
const QString &key = QString(),
const QString &value = QString(),
QWidget *parent = nullptr);
~CustomElementInfoPartWidget() override;
QString key() const;
QString value() const;
bool hasValidKey() const;
signals:
void changed();
void removeRequested(CustomElementInfoPartWidget *self);
private slots:
void validateKey();
private:
QLineEdit *m_key_edit;
QLineEdit *m_value_edit;
QToolButton *m_remove_button;
};
#endif // CUSTOMELEMENTINFOPARTWIDGET_H
+95
View File
@@ -17,12 +17,14 @@
*/
#include "elementinfowidget.h"
#include <QCheckBox>
#include <QPushButton>
#include "../diagram.h"
#include "../qetapp.h"
#include "../qetgraphicsitem/element.h"
#include "../qetinformation.h"
#include "../ui_elementinfowidget.h"
#include "../undocommand/changeelementinformationcommand.h"
#include "customelementinfopartwidget.h"
#include "elementinfopartwidget.h"
/**
@@ -47,6 +49,7 @@ ElementInfoWidget::ElementInfoWidget(Element *elmt, QWidget *parent) :
ElementInfoWidget::~ElementInfoWidget()
{
qDeleteAll(m_eipw_list);
qDeleteAll(m_custom_eipw_list);
delete ui;
}
@@ -207,6 +210,11 @@ void ElementInfoWidget::buildInterface()
ui->scroll_vlayout->addWidget(eipw);
m_eipw_list << eipw;
}
m_add_custom_property_btn = new QPushButton(tr("Ajouter une propriété personnalisée"), this);
connect(m_add_custom_property_btn, &QPushButton::clicked, this, [this]() { addCustomProperty(); });
ui->scroll_vlayout->addWidget(m_add_custom_property_btn);
ui->scroll_vlayout->addStretch();
// Existing potential isolating checkbox
@@ -235,6 +243,67 @@ void ElementInfoWidget::buildInterface()
m_potential_isolating_cb->setVisible(false);
}
}
/**
@brief ElementInfoWidget::predefinedKeys
@return every key this widget already exposes a dedicated row for,
whether through ElementInfoPartWidget (the ~40 ELMT_* keys) or one
of the standalone checkboxes. Anything present in the element's
informations but absent from this list is a user-defined custom
property.
*/
QStringList ElementInfoWidget::predefinedKeys() const
{
QStringList keys = (m_element.data()->elementData().m_type == ElementData::Terminal)
? QETInformation::terminalElementInfoKeys()
: QETInformation::elementInfoKeys();
keys << QStringLiteral("auto_num_locked")
<< QStringLiteral("potential_isolating")
<< QStringLiteral("exclude_from_bom");
return keys;
}
/**
@brief ElementInfoWidget::addCustomProperty
Append a new user-defined key/value row to the widget.
@param key initial key, left empty for a freshly added row
@param value initial value
*/
void ElementInfoWidget::addCustomProperty(const QString &key, const QString &value)
{
auto *widget = new CustomElementInfoPartWidget(key, value, this);
const int insert_index = ui->scroll_vlayout->indexOf(m_add_custom_property_btn);
ui->scroll_vlayout->insertWidget(insert_index >= 0 ? insert_index : ui->scroll_vlayout->count(), widget);
m_custom_eipw_list << widget;
connect(widget, &CustomElementInfoPartWidget::removeRequested, this, &ElementInfoWidget::removeCustomProperty);
connect(widget, &CustomElementInfoPartWidget::changed, this, [this]() {
if (m_live_edit) apply();
});
if (key.isEmpty()) {
widget->setFocus();
}
}
/**
@brief ElementInfoWidget::removeCustomProperty
Remove a user-defined key/value row.
@param widget the row to remove
*/
void ElementInfoWidget::removeCustomProperty(CustomElementInfoPartWidget *widget)
{
if (!m_custom_eipw_list.removeOne(widget))
return;
ui->scroll_vlayout->removeWidget(widget);
widget->deleteLater();
if (m_live_edit) apply();
}
/**
@brief ElementInfoWidget::infoPartWidgetForKey
@param key
@@ -271,6 +340,21 @@ void ElementInfoWidget::updateUi()
for (ElementInfoPartWidget *eipw : m_eipw_list) {
eipw -> setText (element_info[eipw->key()].toString());
}
// Rebuild the custom-property rows to match whatever
// user-defined keys this element currently carries.
while (!m_custom_eipw_list.isEmpty()) {
CustomElementInfoPartWidget *w = m_custom_eipw_list.takeLast();
ui->scroll_vlayout->removeWidget(w);
delete w;
}
const auto known_keys = predefinedKeys();
for (const QString &key : element_info.keys()) {
if (!known_keys.contains(key)) {
addCustomProperty(key, element_info[key].toString());
}
}
// Load the lock status for auto numbering
if (m_element->elementData().m_type == ElementData::Terminal) {
QString lock_value = element_info.value(QStringLiteral("auto_num_locked")).toString();
@@ -314,6 +398,17 @@ DiagramContext ElementInfoWidget::currentInfo() const
}
}
for (const auto &custom : std::as_const(m_custom_eipw_list))
{
if (custom->hasValidKey() && !custom->value().isEmpty())
{
QString txt{custom->value()};
txt.remove(QStringLiteral("\r"));
txt.remove(QStringLiteral("\n"));
info_.addValue(custom->key(), txt);
}
}
// Save the auto numbering lock status
if (m_element->elementData().m_type == ElementData::Terminal) {
info_.addValue(QStringLiteral("auto_num_locked"), ui->m_auto_num_locked_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"));
+7
View File
@@ -26,8 +26,10 @@
class Element;
class QUndoCommand;
class ElementInfoPartWidget;
class CustomElementInfoPartWidget;
class ChangeElementInformationCommand;
class QCheckBox;
class QPushButton;
namespace Ui {
class ElementInfoWidget;
@@ -63,15 +65,20 @@ class ElementInfoWidget : public AbstractElementPropertiesEditorWidget
private:
void buildInterface();
ElementInfoPartWidget *infoPartWidgetForKey(const QString &key) const;
QStringList predefinedKeys() const;
private slots:
void firstActivated();
void elementInfoChange();
void addCustomProperty(const QString &key = QString(), const QString &value = QString());
void removeCustomProperty(CustomElementInfoPartWidget *widget);
//ATTRIBUTES
private:
Ui::ElementInfoWidget *ui;
QList <ElementInfoPartWidget *> m_eipw_list;
QList <CustomElementInfoPartWidget *> m_custom_eipw_list;
QPushButton *m_add_custom_property_btn = nullptr;
QCheckBox *m_potential_isolating_cb = nullptr;
QCheckBox *m_exclude_from_bom_cb = nullptr;
bool m_first_activation;
+1 -1
View File
@@ -535,7 +535,7 @@ void MasterPropertiesWidget::updateUi()
tr("Commentaire"), tr("Réf. croisée")
});
m_plc_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectRows);
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems);
m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_plc_table->setMinimumHeight(200);
@@ -0,0 +1,52 @@
/*
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 "setautonumcontextcommand.h"
#include <utility>
/**
@brief SetAutoNumContextCommand::SetAutoNumContextCommand
@param setter the QETProject setter to call on undo/redo
(addConductorAutoNum/addElementAutoNum/addFolioAutoNum, bound to a project)
@param key the numerotation context's name/key
@param old_context the context's value before this placement
@param new_context the context's value after this placement
@param parent parent undo command
*/
SetAutoNumContextCommand::SetAutoNumContextCommand(
Setter setter,
const QString &key,
const NumerotationContext &old_context,
const NumerotationContext &new_context,
QUndoCommand *parent) :
QUndoCommand(parent),
m_setter(std::move(setter)),
m_key(key),
m_old_context(old_context),
m_new_context(new_context)
{}
void SetAutoNumContextCommand::redo()
{
m_setter(m_key, m_new_context);
}
void SetAutoNumContextCommand::undo()
{
m_setter(m_key, m_old_context);
}
@@ -0,0 +1,57 @@
/*
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 SETAUTONUMCONTEXTCOMMAND_H
#define SETAUTONUMCONTEXTCOMMAND_H
#include "../autoNum/numerotationcontext.h"
#include <QUndoCommand>
#include <functional>
/**
@brief The SetAutoNumContextCommand class
Undo/redo wrapper around one of QETProject's add*AutoNum() setters
(conductor/element/folio numerotation counters). Placing an
auto-numbered item advances one of these counters as a side effect;
without this command the counter change sits outside the undo stack
entirely, so undoing the placement removes the visible number but
leaves the counter advanced, silently burning it.
*/
class SetAutoNumContextCommand : public QUndoCommand
{
public:
using Setter = std::function<void(const QString &, const NumerotationContext &)>;
SetAutoNumContextCommand(
Setter setter,
const QString &key,
const NumerotationContext &old_context,
const NumerotationContext &new_context,
QUndoCommand *parent = nullptr);
void undo() override;
void redo() override;
private:
Setter m_setter;
QString m_key;
NumerotationContext m_old_context;
NumerotationContext m_new_context;
};
#endif // SETAUTONUMCONTEXTCOMMAND_H