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
-1
View File
@@ -11,7 +11,6 @@ jobs:
permissions: permissions:
contents: write contents: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
+28
View File
@@ -145,6 +145,34 @@ else()
) )
endif() endif()
# Optional precompiled headers -- see QET_ENABLE_PCH in
# cmake/developer_options.cmake for what this trades away.
#
# target_precompile_headers() needs CMake 3.16; the project still declares a
# 3.5 minimum, so guard rather than raise it for an opt-in developer feature.
#
# The generator expressions are load-bearing, not decoration: this target also
# compiles the 18 C files of the bundled LZMA decoder
# (sources/import/edz/lzma/*.c), and an unguarded list applies to every
# language in the target, so the Qt headers would reach the C compiler and fail
# with "unknown type name 'namespace'". $<ANGLE-R> is required because a
# literal '>' would terminate the generator expression.
if(QET_ENABLE_PCH)
if(CMAKE_VERSION VERSION_LESS 3.16)
message(WARNING
"QET_ENABLE_PCH needs CMake 3.16 or newer (found ${CMAKE_VERSION}); "
"building without precompiled headers.")
else()
target_precompile_headers(${PROJECT_NAME} PRIVATE
"$<$<COMPILE_LANGUAGE:CXX>:<QtCore/QtCore$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<QtGui/QtGui$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<QtWidgets/QtWidgets$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<QtXml/QtXml$<ANGLE-R>>"
)
message(STATUS "QET_ENABLE_PCH: precompiled headers enabled")
endif()
endif()
target_link_libraries( target_link_libraries(
${PROJECT_NAME} ${PROJECT_NAME}
PUBLIC PUBLIC
View File
+14
View File
@@ -33,3 +33,17 @@ add_definitions(-DQT_MESSAGELOGCONTEXT)
# Build with KF5 # Build with KF5
option(BUILD_WITH_KF5 "Build with KF5" ON) option(BUILD_WITH_KF5 "Build with KF5" ON)
# Precompiled headers for the Qt umbrella headers.
#
# Off by default and intended for local development only. Building QET is
# dominated by re-parsing Qt's headers: a 214-line .cpp expands to ~198,000
# preprocessed lines, and compiling one translation unit costs ~4.1 s of which
# only ~0.35 s is optimisation (-O0 instead of -O3 saves 8%). A PCH caches the
# parsed header state and takes that ~4.1 s down to ~1.2 s.
#
# It is deliberately NOT on by default: a PCH satisfies includes that a source
# file forgot to make itself, so code written with it enabled can fail to
# compile for everyone else. Leaving it off keeps CI and contributors on the
# strict behaviour, and only developers who opt in trade that for the speed.
option(QET_ENABLE_PCH "Use precompiled headers (developer build speed; may mask missing #includes)" OFF)
+14 -1
View File
@@ -116,6 +116,16 @@ set(QET_RES_FILES
set(QET_SRC_FILES set(QET_SRC_FILES
${QET_DIR}/sources/cli_export.cpp ${QET_DIR}/sources/cli_export.cpp
${QET_DIR}/sources/cli_export.h ${QET_DIR}/sources/cli_export.h
${QET_DIR}/sources/logging/crashhandler.cpp
${QET_DIR}/sources/logging/crashhandler.h
${QET_DIR}/sources/logging/eventloopwatchdog.cpp
${QET_DIR}/sources/logging/eventloopwatchdog.h
${QET_DIR}/sources/logging/logring.cpp
${QET_DIR}/sources/logging/logring.h
${QET_DIR}/sources/logging/qetlogger.cpp
${QET_DIR}/sources/logging/qetlogger.h
${QET_DIR}/sources/logging/ui/diagnosticsreportdialog.cpp
${QET_DIR}/sources/logging/ui/diagnosticsreportdialog.h
${QET_DIR}/sources/pdf_links.cpp ${QET_DIR}/sources/pdf_links.cpp
${QET_DIR}/sources/pdf_links.h ${QET_DIR}/sources/pdf_links.h
${QET_DIR}/sources/import/edz/edzarchive.cpp ${QET_DIR}/sources/import/edz/edzarchive.cpp
@@ -530,7 +540,6 @@ set(QET_SRC_FILES
${QET_DIR}/sources/richtext/richtexteditor.cpp ${QET_DIR}/sources/richtext/richtexteditor.cpp
${QET_DIR}/sources/richtext/richtexteditor_p.h ${QET_DIR}/sources/richtext/richtexteditor_p.h
${QET_DIR}/sources/richtext/ui_addlinkdialog.h
${QET_DIR}/sources/SearchAndReplace/searchandreplaceworker.cpp ${QET_DIR}/sources/SearchAndReplace/searchandreplaceworker.cpp
${QET_DIR}/sources/SearchAndReplace/searchandreplaceworker.h ${QET_DIR}/sources/SearchAndReplace/searchandreplaceworker.h
@@ -683,6 +692,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ui/dynamicelementtextitemeditor.h ${QET_DIR}/sources/ui/dynamicelementtextitemeditor.h
${QET_DIR}/sources/ui/dynamicelementtextmodel.cpp ${QET_DIR}/sources/ui/dynamicelementtextmodel.cpp
${QET_DIR}/sources/ui/dynamicelementtextmodel.h ${QET_DIR}/sources/ui/dynamicelementtextmodel.h
${QET_DIR}/sources/ui/customelementinfopartwidget.cpp
${QET_DIR}/sources/ui/customelementinfopartwidget.h
${QET_DIR}/sources/ui/elementinfopartwidget.cpp ${QET_DIR}/sources/ui/elementinfopartwidget.cpp
${QET_DIR}/sources/ui/elementinfopartwidget.h ${QET_DIR}/sources/ui/elementinfopartwidget.h
${QET_DIR}/sources/ui/elementinfowidget.cpp ${QET_DIR}/sources/ui/elementinfowidget.cpp
@@ -761,6 +772,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/undocommand/movediagramcommand.h ${QET_DIR}/sources/undocommand/movediagramcommand.h
${QET_DIR}/sources/undocommand/removediagramcommand.cpp ${QET_DIR}/sources/undocommand/removediagramcommand.cpp
${QET_DIR}/sources/undocommand/removediagramcommand.h ${QET_DIR}/sources/undocommand/removediagramcommand.h
${QET_DIR}/sources/undocommand/setautonumcontextcommand.cpp
${QET_DIR}/sources/undocommand/setautonumcontextcommand.h
${QET_DIR}/sources/undocommand/rotateselectioncommand.cpp ${QET_DIR}/sources/undocommand/rotateselectioncommand.cpp
${QET_DIR}/sources/undocommand/rotateselectioncommand.h ${QET_DIR}/sources/undocommand/rotateselectioncommand.h
${QET_DIR}/sources/undocommand/rotatetextscommand.cpp ${QET_DIR}/sources/undocommand/rotatetextscommand.cpp
+923 -681
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
+917 -681
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+916 -681
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+917 -681
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+940 -706
View File
File diff suppressed because it is too large Load Diff
+922 -686
View File
File diff suppressed because it is too large Load Diff
+913 -681
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
+913 -681
View File
File diff suppressed because it is too large Load Diff
+913 -681
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
+943 -709
View File
File diff suppressed because it is too large Load Diff
+917 -681
View File
File diff suppressed because it is too large Load Diff
+943 -709
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
+946 -710
View File
File diff suppressed because it is too large Load Diff
+945 -709
View File
File diff suppressed because it is too large Load Diff
+917 -681
View File
File diff suppressed because it is too large Load Diff
+945 -709
View File
File diff suppressed because it is too large Load Diff
+947 -709
View File
File diff suppressed because it is too large Load Diff
+945 -709
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
+913 -681
View File
File diff suppressed because it is too large Load Diff
+917 -681
View File
File diff suppressed because it is too large Load Diff
+913 -681
View File
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -173,7 +173,9 @@ HEADERS += $$files(sources/*.h) \
$$files(sources/qet_elementscaler/*.h) \ $$files(sources/qet_elementscaler/*.h) \
$$files(sources/svg/*.h) \ $$files(sources/svg/*.h) \
$$files(sources/import/edz/*.h) \ $$files(sources/import/edz/*.h) \
$$files(sources/import/edz/lzma/*.h) $$files(sources/import/edz/lzma/*.h) \
$$files(sources/logging/*.h) \
$$files(sources/logging/ui/*.h)
SOURCES += $$files(sources/*.cpp) \ SOURCES += $$files(sources/*.cpp) \
$$files(sources/editor/*.cpp) \ $$files(sources/editor/*.cpp) \
@@ -219,7 +221,10 @@ SOURCES += $$files(sources/*.cpp) \
$$files(sources/qet_elementscaler/*.cpp) \ $$files(sources/qet_elementscaler/*.cpp) \
$$files(sources/svg/*.cpp) \ $$files(sources/svg/*.cpp) \
$$files(sources/import/edz/*.cpp) \ $$files(sources/import/edz/*.cpp) \
$$files(sources/import/edz/lzma/*.c) $$files(sources/import/edz/lzma/*.c) \
$$files(sources/logging/*.cpp) \
$$files(sources/logging/ui/*.cpp)
# Needed for use promote QTreeWidget in terminalstripeditor.ui # Needed for use promote QTreeWidget in terminalstripeditor.ui
INCLUDEPATH += sources/TerminalStrip/ui INCLUDEPATH += sources/TerminalStrip/ui
@@ -22,7 +22,39 @@
#include "../qeticons.h" #include "../qeticons.h"
#include "elementslocation.h" #include "elementslocation.h"
#include <QApplication>
#include <QDir> #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 @brief FileElementCollectionItem::FileElementCollectionItem
@@ -136,6 +168,14 @@ QString FileElementCollectionItem::localName()
} }
else 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"); QString str(fileSystemPath() % "/qet_directory");
pugi::xml_document docu; pugi::xml_document docu;
if (docu.load_file(str.toStdWString().c_str())) if (docu.load_file(str.toStdWString().c_str()))
@@ -143,11 +183,26 @@ QString FileElementCollectionItem::localName()
if (QString(docu.document_element().name()) if (QString(docu.document_element().name())
== "qet-directory") == "qet-directory")
{ {
readable = true;
NamesList nl; NamesList nl;
nl.fromXml(docu.document_element()); 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()) { 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() 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()) if (!icon().isNull())
return; return;
@@ -380,7 +462,8 @@ void FileElementCollectionItem::setUpIcon()
else else
{ {
if (isDir()) { if (isDir()) {
setIcon(QET::Icons::Folder); setIcon(m_qet_directory_unreadable ? unreadableFolderIcon()
: QET::Icons::Folder);
} else { } else {
if (m_path.endsWith(".qetmak")) { if (m_path.endsWith(".qetmak")) {
setIcon(QIcon()); setIcon(QIcon());
@@ -64,6 +64,11 @@ class FileElementCollectionItem : public ElementCollectionItem
private: private:
QString m_path; 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 #endif // FILEELEMENTCOLLECTIONITEM2_H
+46 -8
View File
@@ -39,6 +39,7 @@ namespace autonum
sequentialNumbers::sequentialNumbers(const sequentialNumbers &other) sequentialNumbers::sequentialNumbers(const sequentialNumbers &other)
{ {
unit = other.unit; unit = other.unit;
wrap = other.wrap;
unit_folio = other.unit_folio; unit_folio = other.unit_folio;
ten = other.ten; ten = other.ten;
ten_folio = other.ten_folio; ten_folio = other.ten_folio;
@@ -57,6 +58,7 @@ namespace autonum
return (*this); return (*this);
unit = other.unit; unit = other.unit;
wrap = other.wrap;
unit_folio = other.unit_folio; unit_folio = other.unit_folio;
ten = other.ten; ten = other.ten;
ten_folio = other.ten_folio; ten_folio = other.ten_folio;
@@ -70,6 +72,7 @@ namespace autonum
bool sequentialNumbers::operator==(const sequentialNumbers &other) const bool sequentialNumbers::operator==(const sequentialNumbers &other) const
{ {
if (unit == other.unit && \ if (unit == other.unit && \
wrap == other.wrap && \
unit_folio == other.unit_folio && \ unit_folio == other.unit_folio && \
ten == other.ten && \ ten == other.ten && \
ten_folio == other.ten_folio && \ ten_folio == other.ten_folio && \
@@ -107,6 +110,11 @@ namespace autonum
document, document,
"unit", "unit",
unit.join(";"))); unit.join(";")));
if (!wrap.isEmpty())
element.appendChild(QETXML::textToDomElement(
document,
"wrap",
wrap.join(";")));
if (!unit_folio.isEmpty()) if (!unit_folio.isEmpty())
element.appendChild(QETXML::textToDomElement( element.appendChild(QETXML::textToDomElement(
document, document,
@@ -156,6 +164,11 @@ namespace autonum
from = element.firstChildElement("unit"); from = element.firstChildElement("unit");
unit = from.text().split(";"); 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"); from = element.firstChildElement("unitFolio");
unit_folio = from.text().split(";"); unit_folio = from.text().split(";");
@@ -179,6 +192,7 @@ namespace autonum
void sequentialNumbers::clear() void sequentialNumbers::clear()
{ {
unit.clear(); unit.clear();
wrap.clear();
unit_folio.clear(); unit_folio.clear();
ten.clear(); ten.clear();
ten_folio.clear(); ten_folio.clear();
@@ -434,7 +448,8 @@ namespace autonum
qMax( qMax(
qMax(m_seq_struct.hundred.size(), qMax(m_seq_struct.hundred.size(),
m_seq_struct.ten.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++) 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) { 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)); 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) { 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)); 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) 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; QString number;
if (type == "ten" || type == "tenfolio") if (type == "alpha")
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")
//Alphabetic value, not an integer -- used as-is. //Alphabetic value, not an integer -- used as-is.
number = context.itemAt(i).at(1); number = item.at(1);
else number = QString::number(context.itemAt(i).at(1).toInt()); 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); list.append(number);
} }
} }
@@ -553,6 +582,10 @@ namespace autonum
{ {
autonum::setSequentialToList(seqStruct.unit, context,"unit"); autonum::setSequentialToList(seqStruct.unit, context,"unit");
} }
if (label.contains("%seqw_"))
{
autonum::setSequentialToList(seqStruct.wrap, context,"wrap");
}
if (label.contains("%sequf_")) if (label.contains("%sequf_"))
{ {
autonum::setSequentialToList(seqStruct.unit_folio, context,"unitfolio"); autonum::setSequentialToList(seqStruct.unit_folio, context,"unitfolio");
@@ -594,6 +627,7 @@ namespace autonum
QString value; QString value;
QString formula; QString formula;
int count_unit = 0; int count_unit = 0;
int count_wrap = 0;
int count_unitf = 0; int count_unitf = 0;
int count_ten = 0; int count_ten = 0;
int count_tenf = 0; int count_tenf = 0;
@@ -636,6 +670,10 @@ namespace autonum
count_unit++; count_unit++;
formula.append("%sequ_" + QString::number(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") { else if (type == "unitfolio") {
count_unitf++; count_unitf++;
formula.append("%sequf_" + QString::number(count_unitf)); formula.append("%sequf_" + QString::number(count_unitf));
+2
View File
@@ -47,6 +47,8 @@ namespace autonum
void clear(); void clear();
QStringList unit; QStringList unit;
/// Values of the cyclic (modulo) parts, referenced by %seqw_N.
QStringList wrap;
QStringList unit_folio; QStringList unit_folio;
QStringList ten; QStringList ten;
QStringList ten_folio; QStringList ten_folio;
+26 -4
View File
@@ -52,13 +52,18 @@ void NumerotationContext::clear ()
@param increase the increase number of value @param increase the increase number of value
@param initialvalue @param initialvalue
@param modulus wrap-and-carry modulus (0 means "not a wrapping part") @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 @return true if value is append
*/ */
bool NumerotationContext::addValue(const QString &type, bool NumerotationContext::addValue(const QString &type,
const QVariant &value, const QVariant &value,
const int increase, const int increase,
const int initialvalue, const int initialvalue,
const int modulus) { const int modulus,
const QString &format) {
if (!keyIsAcceptable(type) && !value.canConvert<QString>()) if (!keyIsAcceptable(type) && !value.canConvert<QString>())
return false; return false;
if (keyIsNumber(type) && !value.canConvert<int>()) if (keyIsNumber(type) && !value.canConvert<int>())
@@ -74,7 +79,9 @@ bool NumerotationContext::addValue(const QString &type,
+ "|" + "|"
+ QString::number(initialvalue) + QString::number(initialvalue)
+ "|" + "|"
+ QString::number(modulus); + QString::number(modulus)
+ "|"
+ QString(format).remove("|");
return true; return true;
} }
@@ -179,6 +186,9 @@ QDomElement NumerotationContext::toXml(QDomDocument &d, const QString& str) {
if (strl.at(0) == ("wrap") && strl.size() > 4) { if (strl.at(0) == ("wrap") && strl.size() > 4) {
part.setAttribute("modulus", strl.at(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); num_auto.appendChild(part);
} }
return num_auto; return num_auto;
@@ -190,7 +200,7 @@ QDomElement NumerotationContext::toXml(QDomDocument &d, const QString& str) {
*/ */
void NumerotationContext::fromXml(QDomElement &e) { void NumerotationContext::fromXml(QDomElement &e) {
clear(); 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 increase = strl.at(2);
QString initvalue = strl.at(3); QString initvalue = strl.at(3);
QString modulus = strl.size() > 4 ? strl.at(4) : QStringLiteral("0"); 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 QVariant & = QVariant(1),
const int = 1, const int = 1,
const int = 0, 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; QString operator[] (const int &) const;
void operator << (const NumerotationContext &); void operator << (const NumerotationContext &);
int size() const; int size() const;
@@ -259,7 +259,7 @@ NumerotationContext NumStrategy::nextString (const NumerotationContext &nc,
{ {
QStringList strl = nc.itemAt(i); QStringList strl = nc.itemAt(i);
NumerotationContext newnc; 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); return (newnc);
} }
@@ -273,7 +273,7 @@ NumerotationContext NumStrategy::nextNumber (const NumerotationContext &nc,
QStringList strl = nc.itemAt(i); QStringList strl = nc.itemAt(i);
NumerotationContext newnc; NumerotationContext newnc;
QString value = QString::number( (strl.at(1).toInt()) + (strl.at(2).toInt()) ); 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); return (newnc);
} }
@@ -287,7 +287,7 @@ NumerotationContext NumStrategy::previousNumber(const NumerotationContext &nc,
QStringList strl = nc.itemAt(i); QStringList strl = nc.itemAt(i);
NumerotationContext newnc; NumerotationContext newnc;
QString value = QString::number( (strl.at(1).toInt()) - (strl.at(2).toInt()) ); 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); return (newnc);
} }
@@ -550,7 +550,7 @@ NumerotationContext WrapNum::next (const NumerotationContext &nc, const int i) c
int new_value = strl.at(1).toInt() + increase; int new_value = strl.at(1).toInt() + increase;
if (modulus > 0) if (modulus > 0)
new_value %= modulus; 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); return (newnc);
} }
@@ -570,7 +570,7 @@ NumerotationContext WrapNum::previous(const NumerotationContext &nc, const int i
if (new_value < 0) if (new_value < 0)
new_value += modulus; 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); return (newnc);
} }
@@ -706,7 +706,7 @@ NumerotationContext AlphaNum::next (const NumerotationContext &nc, const int i)
{ {
QStringList strl = nc.itemAt(i); QStringList strl = nc.itemAt(i);
NumerotationContext newnc; 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); return (newnc);
} }
@@ -718,7 +718,7 @@ NumerotationContext AlphaNum::previous(const NumerotationContext &nc, const int
{ {
QStringList strl = nc.itemAt(i); QStringList strl = nc.itemAt(i);
NumerotationContext newnc; 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); return (newnc);
} }
@@ -26,6 +26,9 @@
#include "../numerotationcontext.h" #include "../numerotationcontext.h"
#include "ui_autonumberingdockwidget.h" #include "ui_autonumberingdockwidget.h"
#include <QComboBox>
#include <QLineEdit>
/** /**
@brief AutoNumberingDockWidget::AutoNumberingDockWidget @brief AutoNumberingDockWidget::AutoNumberingDockWidget
Constructor Constructor
@@ -58,6 +61,9 @@ void AutoNumberingDockWidget::clear()
ui->m_conductor_cb->clear(); ui->m_conductor_cb->clear();
ui->m_element_cb->clear(); ui->m_element_cb->clear();
ui->m_folio_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() void AutoNumberingDockWidget::projectClosed()
@@ -109,6 +115,8 @@ void AutoNumberingDockWidget::setProject(QETProject *project,
this,SLOT(setActive())); this,SLOT(setActive()));
//Conductor, Element and Folio Signals //Conductor, Element and Folio Signals
disconnect(m_project, &QETProject::autoNumContextUpdated,
this, &AutoNumberingDockWidget::refreshValueFields);
disconnect(m_project, &QETProject::destroyed, disconnect(m_project, &QETProject::destroyed,
this, &AutoNumberingDockWidget::projectClosed); this, &AutoNumberingDockWidget::projectClosed);
} }
@@ -146,6 +154,8 @@ void AutoNumberingDockWidget::setProject(QETProject *project,
this,SLOT(setActive())); this,SLOT(setActive()));
//Conductor, Element and Folio Signals //Conductor, Element and Folio Signals
connect(m_project, &QETProject::autoNumContextUpdated,
this, &AutoNumberingDockWidget::refreshValueFields);
connect(m_project, &QETProject::destroyed, connect(m_project, &QETProject::destroyed,
this, &AutoNumberingDockWidget::projectClosed); this, &AutoNumberingDockWidget::projectClosed);
@@ -189,6 +199,12 @@ void AutoNumberingDockWidget::setContext()
{ ui->m_folio_cb -> addItem(str);} { 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(); this->setActive();
} }
@@ -263,6 +279,7 @@ void AutoNumberingDockWidget::on_m_conductor_cb_activated(int)
m_project->setCurrentConductorAutoNum(current_autonum); m_project->setCurrentConductorAutoNum(current_autonum);
m_project_view->currentDiagram()->diagram()->setConductorsAutonumName(current_autonum); m_project_view->currentDiagram()->diagram()->setConductorsAutonumName(current_autonum);
m_project_view->currentDiagram()->diagram()->loadCndFolioSeq(); 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->setCurrrentElementAutonum(ui->m_element_cb->currentText());
m_project_view->currentDiagram()->diagram()->loadElmtFolioSeq(); 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); m_project->setDefaultTitleBlockProperties(ip);
} }
emit(folioAutoNumChanged(current_autonum)); emit(folioAutoNumChanged(current_autonum));
refreshValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
} }
void AutoNumberingDockWidget::on_m_configure_pb_clicked() void AutoNumberingDockWidget::on_m_configure_pb_clicked()
@@ -339,3 +358,209 @@ void AutoNumberingDockWidget::on_m_configure_pb_clicked()
ppd.exec(); 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;
}
}
@@ -23,6 +23,9 @@
#include <QDockWidget> #include <QDockWidget>
class QComboBox;
class QLineEdit;
namespace Ui { namespace Ui {
class AutoNumberingDockWidget; class AutoNumberingDockWidget;
} }
@@ -51,13 +54,45 @@ class AutoNumberingDockWidget : public QDockWidget
void folioAutoNumChanged(); void folioAutoNumChanged();
void clear(); void clear();
void projectClosed(); void projectClosed();
void refreshValueFields();
void on_m_configure_pb_clicked(); 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: signals:
void folioAutoNumChanged(QString); void folioAutoNumChanged(QString);
private: 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; Ui::AutoNumberingDockWidget *ui;
QETProject* m_project = nullptr; QETProject* m_project = nullptr;
ProjectView* m_project_view = nullptr; ProjectView* m_project_view = nullptr;
@@ -28,6 +28,39 @@
<item row="2" column="1"> <item row="2" column="1">
<widget class="QComboBox" name="m_conductor_cb"/> <widget class="QComboBox" name="m_conductor_cb"/>
</item> </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"> <item row="3" column="0">
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
@@ -42,9 +75,75 @@
</property> </property>
</widget> </widget>
</item> </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"> <item row="4" column="1">
<widget class="QComboBox" name="m_folio_cb"/> <widget class="QComboBox" name="m_folio_cb"/>
</item> </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"> <item row="6" column="0">
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">
+41 -5
View File
@@ -18,6 +18,8 @@
#include "numparteditorw.h" #include "numparteditorw.h"
#include "ui_numparteditorw.h" #include "ui_numparteditorw.h"
#include "../numerotationcontext.h"
#include <QRegularExpressionValidator> #include <QRegularExpressionValidator>
/** /**
@@ -35,6 +37,9 @@ NumPartEditorW::NumPartEditorW(int type, QWidget *parent) :
{ {
ui -> setupUi(this); ui -> setupUi(this);
setVisibleItems(); 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); setType(NumPartEditorW::unit, true);
} }
@@ -99,6 +104,7 @@ NumPartEditorW::NumPartEditorW (NumerotationContext &context,
ui -> increase_spinBox -> setValue(strl.at(2).toInt()); ui -> increase_spinBox -> setValue(strl.at(2).toInt());
if (strl.at(0)=="wrap" && strl.size() > 4) if (strl.at(0)=="wrap" && strl.size() > 4)
ui -> modulus_spinBox -> setValue(strl.at(4).toInt()); 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"; type_str = "alpha";
break; break;
} }
const QString number_format = ui -> format_le -> text();
if (type_str == "unitfolio" if (type_str == "unitfolio"
|| type_str == "tenfolio" || type_str == "tenfolio"
|| type_str == "hundredfolio") || type_str == "hundredfolio")
nc.addValue(type_str, nc.addValue(type_str,
ui -> value_field -> displayText(), ui -> value_field -> displayText(),
ui -> increase_spinBox -> value(), ui -> increase_spinBox -> value(),
ui->value_field->displayText().toInt()); ui->value_field->displayText().toInt(),
0,
number_format);
else if (type_str == "wrap") else if (type_str == "wrap")
nc.addValue(type_str, nc.addValue(type_str,
ui -> value_field -> displayText(), ui -> value_field -> displayText(),
ui -> increase_spinBox -> value(), ui -> increase_spinBox -> value(),
0, 0,
ui -> modulus_spinBox -> value()); ui -> modulus_spinBox -> value(),
number_format);
else else
nc.addValue(type_str, nc.addValue(type_str,
ui -> value_field -> displayText(), ui -> value_field -> displayText(),
ui -> increase_spinBox -> value()); ui -> increase_spinBox -> value(),
0,
0,
number_format);
return nc; return nc;
} }
@@ -317,6 +330,14 @@ void NumPartEditorW::on_increase_spinBox_valueChanged(int) {
@brief NumPartEditorW::on_modulus_spinBox_valueChanged @brief NumPartEditorW::on_modulus_spinBox_valueChanged
emit changed when modulus_spinBox value changed 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) { void NumPartEditorW::on_modulus_spinBox_valueChanged(int) {
if (!ui -> value_field -> text().isEmpty()) emit changed(); 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 -> value_field -> setValidator(intValidator);
ui -> increase_spinBox -> setEnabled(true); ui -> increase_spinBox -> setEnabled(true);
ui -> increase_spinBox -> setValue(1); ui -> increase_spinBox -> setValue(1);
if (t == wrap)
ui -> modulus_spinBox -> setValue(8);
} }
//@t isn't a numeric type //@t isn't a numeric type
else if (t == string else if (t == string
@@ -414,7 +433,24 @@ void NumPartEditorW::setType(NumPartEditorW::type t, bool fnum) {
ui -> increase_spinBox -> setDisabled(true); 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); 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; type_= t;
} }
+1
View File
@@ -66,6 +66,7 @@ class NumPartEditorW : public QWidget
void on_value_field_textEdited(); void on_value_field_textEdited();
void on_increase_spinBox_valueChanged(int); void on_increase_spinBox_valueChanged(int);
void on_modulus_spinBox_valueChanged(int); void on_modulus_spinBox_valueChanged(int);
void on_format_le_textEdited(const QString &);
void setType (NumPartEditorW::type t, bool=false); void setType (NumPartEditorW::type t, bool=false);
signals: signals:
+22
View File
@@ -126,6 +126,28 @@
</property> </property>
</widget> </widget>
</item> </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> </layout>
</widget> </widget>
<resources/> <resources/>
+26 -2
View File
@@ -24,6 +24,7 @@
#include "qet.h" #include "qet.h"
#include "qetdiagrameditor.h" #include "qetdiagrameditor.h"
#include "ui/potentialselectordialog.h" #include "ui/potentialselectordialog.h"
#include "undocommand/setautonumcontextcommand.h"
/** /**
@brief ConductorAutoNumerotation::ConductorAutoNumerotation @brief ConductorAutoNumerotation::ConductorAutoNumerotation
@@ -156,7 +157,16 @@ void ConductorAutoNumerotation::newProperties(
autonum::setSequential(formula, seq, context, diagram, autoNum_name); autonum::setSequential(formula, seq, context, diagram, autoNum_name);
NumerotationContextCommands ncc (context, diagram); 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); autoNum_name);
NumerotationContextCommands ncc (context, m_diagram); 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( applyText(autonum::AssignVariables::formulaToLabel(
@@ -376,6 +376,7 @@ QString ElementQueryWidget::queryStr() const
if (ui->m_plc_cb->isChecked()) { if (ui->m_plc_cb->isChecked()) {
if (b) where +=" OR"; if (b) where +=" OR";
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::PLC) += "'"; where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::PLC) += "'";
b = true;
} }
where.append(")"); where.append(")");
@@ -501,7 +501,12 @@ void DiagramEventAddElement::addElement()
} }
m_diagram->addItem(m_element); 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); m_diagram -> undoStack().push(undo_object);
element->setUpFormula();
element->freezeNewAddedElement(); element->freezeNewAddedElement();
} }
+4
View File
@@ -317,6 +317,10 @@ void ElementScene::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
*/ */
void ElementScene::drawForeground(QPainter *p, const QRectF &) void ElementScene::drawForeground(QPainter *p, const QRectF &)
{ {
if (!m_hotspot_visible) {
return;
}
p -> save(); p -> save();
// desactive tout antialiasing, sauf pour le texte // desactive tout antialiasing, sauf pour le texte
+10
View File
@@ -91,6 +91,8 @@ class ElementScene : public QGraphicsScene
QPointer<CustomElementGraphicPart> m_single_selected_item; QPointer<CustomElementGraphicPart> m_single_selected_item;
bool m_hotspot_visible = true;
// methods // methods
public: public:
ElementData elementData(); ElementData elementData();
@@ -133,6 +135,14 @@ class ElementScene : public QGraphicsScene
void addItems(QVector<QGraphicsItem *> items); void addItems(QVector<QGraphicsItem *> items);
void removeItems(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: protected:
void mouseMoveEvent (QGraphicsSceneMouseEvent *) override; void mouseMoveEvent (QGraphicsSceneMouseEvent *) override;
void mousePressEvent (QGraphicsSceneMouseEvent *) override; void mousePressEvent (QGraphicsSceneMouseEvent *) override;
+3 -2
View File
@@ -18,6 +18,7 @@
#include "partplctable.h" #include "partplctable.h"
#include "../../QPropertyUndoCommand/qpropertyundocommand.h" #include "../../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../../qetapp.h"
#include "../../QetGraphicsItemModeler/qetgraphicshandleritem.h" #include "../../QetGraphicsItemModeler/qetgraphicshandleritem.h"
#include "../../QetGraphicsItemModeler/qetgraphicshandlerutility.h" #include "../../QetGraphicsItemModeler/qetgraphicshandlerutility.h"
#include "../../properties/elementdata.h" #include "../../properties/elementdata.h"
@@ -302,7 +303,7 @@ void PartPlcTable::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti
// Draw column headers // Draw column headers
QFont header_font = plc_data.headerFont.family().isEmpty() QFont header_font = plc_data.headerFont.family().isEmpty()
? painter->font() : plc_data.headerFont; ? QETApp::diagramTextsFont() : plc_data.headerFont;
header_font.setBold(true); header_font.setBold(true);
painter->setFont(header_font); painter->setFont(header_font);
@@ -318,7 +319,7 @@ void PartPlcTable::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti
// Draw IO rows // Draw IO rows
QFont cell_font = plc_data.cellFont.family().isEmpty() QFont cell_font = plc_data.cellFont.family().isEmpty()
? painter->font() : plc_data.cellFont; ? QETApp::diagramTextsFont() : plc_data.cellFont;
painter->setFont(cell_font); painter->setFont(cell_font);
int start_idx = block_starts.at(block); int start_idx = block_starts.at(block);
int end_idx = (block + 1 < block_starts.size()) int end_idx = (block + 1 < block_starts.size())
@@ -28,6 +28,8 @@
#include <QSignalBlocker> #include <QSignalBlocker>
#include <QTableWidgetItem> #include <QTableWidgetItem>
#include <QHeaderView> #include <QHeaderView>
#include <QScrollBar>
#include <QWheelEvent>
#include <QTableWidget> #include <QTableWidget>
#include <QCheckBox> #include <QCheckBox>
#include <QGroupBox> #include <QGroupBox>
@@ -41,6 +43,8 @@
#include <QFont> #include <QFont>
#include <QLineEdit> #include <QLineEdit>
#include <QSplitter> #include <QSplitter>
#include <QShortcut>
#include <QMenu>
/** /**
@brief The EditorDelegate class @brief The EditorDelegate class
@@ -666,10 +670,15 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
plc_layout->addLayout(toolbar); plc_layout->addLayout(toolbar);
// Tables side by side: IO table (left) + Terminal table (right) // 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 // IO Table
m_plc_table = new QTableWidget(tables_splitter); m_plc_table = new QTableWidget(splitter);
m_plc_table->setColumnCount(5); m_plc_table->setColumnCount(5);
m_plc_table->setHorizontalHeaderLabels({ m_plc_table->setHorizontalHeaderLabels({
tr("Type"), tr("Adresse"), tr("Fonction"), tr("Type"), tr("Adresse"), tr("Fonction"),
@@ -686,10 +695,10 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems); m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems);
m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection); m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_plc_table->setMinimumHeight(200); m_plc_table->setMinimumHeight(200);
tables_splitter->addWidget(m_plc_table); m_plc_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
// Terminal table (per IO: Nb + T1-T4) // 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->setColumnCount(2);
m_plc_terminal_table->setHorizontalHeaderLabels({ m_plc_terminal_table->setHorizontalHeaderLabels({
tr("Nb."), tr("T1") tr("Nb."), tr("T1")
@@ -700,13 +709,61 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
m_plc_terminal_table->setSelectionBehavior(QAbstractItemView::SelectItems); m_plc_terminal_table->setSelectionBehavior(QAbstractItemView::SelectItems);
m_plc_terminal_table->setSelectionMode(QAbstractItemView::ExtendedSelection); m_plc_terminal_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_plc_terminal_table->setMinimumHeight(200); 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); // Shared vertical scrollbar
tables_splitter->setStretchFactor(1, 1); m_plc_shared_scrollbar = new QScrollBar(Qt::Vertical, tables_container);
tables_splitter->setSizes({500, 150}); 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 // Font settings
auto *font_layout = new QHBoxLayout(); auto *font_layout = new QHBoxLayout();
@@ -790,12 +847,25 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
} }
plc_layout->addLayout(col_layout); plc_layout->addLayout(col_layout);
// Add to master group box // Add to master group box - row 4, full width
ui->m_master_gb->layout()->addWidget(m_plc_gb); auto *gl = qobject_cast<QGridLayout*>(ui->m_master_gb->layout());
gl->addWidget(m_plc_gb, 4, 0, 1, 2);
// Connect signals // Connect signals
connect(add_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcAddRow); connect(add_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcAddRow);
connect(remove_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcRemoveRow); 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_header_font = plc_data.headerFont;
m_plc_cell_font = plc_data.cellFont; m_plc_cell_font = plc_data.cellFont;
if (m_plc_header_font.family().isEmpty()) { 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.setBold(true);
m_plc_header_font.setPointSize(8); m_plc_header_font.setPointSize(8);
} }
if (m_plc_cell_font.family().isEmpty()) { 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_cell_font.setPointSize(8);
} }
m_plc_header_font_btn->setText(tr("Police des en-têtes: %1 %2pt") 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); 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; return;
QStringList lines = clipboard_text.split('\n', Qt::SkipEmptyParts); QStringList lines = clipboard_text.split('\n', Qt::SkipEmptyParts);
if (lines.isEmpty())
return;
int start_row = m_plc_table->rowCount(); bool has_tabs = false;
m_plc_table->setRowCount(start_row + lines.size()); for (const QString &line : lines) {
if (line.contains('\t')) {
for (int i = 0; i < lines.size(); ++i) { has_tabs = true;
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);
}
// Try to match type from clipboard
if (!cells.isEmpty()) {
QString type_str = cells.at(0).trimmed();
int type_idx = -1;
for (int t = 0; t < plc_types.size(); ++t) {
if (plc_types.at(t).compare(type_str, Qt::CaseInsensitive) == 0) {
type_idx = t;
break; break;
} }
} }
if (type_idx >= 0)
type_cb->setCurrentIndex(type_idx); 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());
} }
m_plc_table->setCellWidget(row, 0, type_cb); } 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();
// Address for (int i = 0; i < lines.size(); ++i) {
m_plc_table->setItem(row, 1, new QTableWidgetItem( int row = target_row + i;
cells.size() > 1 ? cells.at(1).trimmed() : QString())); 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());
}
}
}
}
// Function text /**
m_plc_table->setItem(row, 2, new QTableWidgetItem( * @brief ElementPropertiesEditorWidget::plcSetCellFromValue
cells.size() > 2 ? cells.at(2).trimmed() : QString())); * 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;
// Comment if (col == 0) {
m_plc_table->setItem(row, 3, new QTableWidgetItem( auto *type_cb = qobject_cast<QComboBox*>(m_plc_table->cellWidget(row, col));
cells.size() > 3 ? cells.at(3).trimmed() : QString())); if (!type_cb)
return;
// CrossRef (read-only) if (!val.isEmpty()) {
auto *crossref_item = new QTableWidgetItem( QStringList plc_types = ElementData::plcIOTypeList();
cells.size() > 4 ? cells.at(4).trimmed() : QString()); for (int t = 0; t < plc_types.size(); ++t) {
crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable); if (plc_types.at(t).compare(val, Qt::CaseInsensitive) == 0) {
m_plc_table->setItem(row, 4, crossref_item); type_cb->setCurrentIndex(t);
return;
}
}
}
}
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 QGroupBox;
class QPushButton; class QPushButton;
class QLineEdit; class QLineEdit;
class QScrollBar;
namespace Ui { namespace Ui {
class ElementPropertiesEditorWidget; class ElementPropertiesEditorWidget;
@@ -74,6 +75,7 @@ class ElementPropertiesEditorWidget : public QDialog
void plcTerminalCountChanged(int row, int count); void plcTerminalCountChanged(int row, int count);
void plcSelectHeaderFont(); void plcSelectHeaderFont();
void plcSelectCellFont(); void plcSelectCellFont();
void plcSetCellFromValue(int row, int col, const QString &val);
//ATTRIBUTES //ATTRIBUTES
private: private:
@@ -95,6 +97,8 @@ class ElementPropertiesEditorWidget : public QDialog
QList<QCheckBox *> m_plc_col_visibility_checkboxes; QList<QCheckBox *> m_plc_col_visibility_checkboxes;
QList<QSpinBox *> m_plc_col_width_spinboxes; QList<QSpinBox *> m_plc_col_width_spinboxes;
QList<QLineEdit *> m_plc_col_name_edits; QList<QLineEdit *> m_plc_col_name_edits;
QScrollBar *m_plc_shared_scrollbar = nullptr;
bool m_plc_scroll_sync = false;
}; };
#endif // ELEMENTPROPERTIESEDITORWIDGET_H #endif // ELEMENTPROPERTIESEDITORWIDGET_H
@@ -39,6 +39,19 @@
<item> <item>
<widget class="QComboBox" name="m_base_type_cb"/> <widget class="QComboBox" name="m_base_type_cb"/>
</item> </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>
</item> </item>
<item> <item>
@@ -94,16 +107,33 @@
<string>Élément maître</string> <string>Élément maître</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout_3"> <layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0"> <item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="type_concret_layout">
<item>
<widget class="QLabel" name="label_5"> <widget class="QLabel" name="label_5">
<property name="text"> <property name="text">
<string>Type concret</string> <string>Type concret</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="1"> <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"/> <widget class="QComboBox" name="m_master_type_cb"/>
</item> </item>
</layout>
</item>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QCheckBox" name="max_slaves_checkbox"> <widget class="QCheckBox" name="max_slaves_checkbox">
<property name="text"> <property name="text">
@@ -202,19 +232,6 @@
</layout> </layout>
</widget> </widget>
</item> </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>
<widget class="QWidget" name="Informations"> <widget class="QWidget" name="Informations">
+90
View File
@@ -53,6 +53,8 @@
#include <QSettings> #include <QSettings>
#include <QActionGroup> #include <QActionGroup>
#include <QFileDialog>
#include <QSvgGenerator>
/** /**
* @brief QETElementEditor::QETElementEditor * @brief QETElementEditor::QETElementEditor
@@ -1377,6 +1379,94 @@ bool QETElementEditor::on_m_save_as_file_action_triggered()
return false; 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() void QETElementEditor::on_m_reload_action_triggered()
{ {
//If user already edit the element, ask confirmation to reload //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_action_triggered();
void on_m_open_from_file_action_triggered(); void on_m_open_from_file_action_triggered();
bool on_m_save_as_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_reload_action_triggered();
void on_m_quit_action_triggered(); void on_m_quit_action_triggered();
void on_m_deselect_all_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_action"/>
<addaction name="m_save_as_action"/> <addaction name="m_save_as_action"/>
<addaction name="m_save_as_file_action"/> <addaction name="m_save_as_file_action"/>
<addaction name="m_export_svg_action"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="m_reload_action"/> <addaction name="m_reload_action"/>
<addaction name="separator"/> <addaction name="separator"/>
@@ -262,6 +263,15 @@
<string>Enregistrer dans un fichier</string> <string>Enregistrer dans un fichier</string>
</property> </property>
</action> </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"> <action name="m_reload_action">
<property name="icon"> <property name="icon">
<iconset resource="../../../qelectrotech.qrc"> <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_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_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_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_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_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); 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_edit_prop, SIGNAL(triggered()), this, SLOT(editProjectProperties()));
connect(prj_prop_diagram, SIGNAL(triggered()), this, SLOT(editDiagramProperties())); connect(prj_prop_diagram, SIGNAL(triggered()), this, SLOT(editDiagramProperties()));
connect(prj_add_diagram, SIGNAL(triggered()), this, SLOT(newDiagram())); 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_del_diagram, SIGNAL(triggered()), this, SLOT(deleteDiagram()));
connect(prj_duplicate_diagram, SIGNAL(triggered()), this, SLOT(duplicateDiagram())); connect(prj_duplicate_diagram, SIGNAL(triggered()), this, SLOT(duplicateDiagram()));
connect(prj_move_diagram_up, SIGNAL(triggered()), this, SLOT(moveDiagramUp())); 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 * Emet le signal requestForDiagramsDeletion avec les schemas selectionnes
*/ */
@@ -451,6 +481,8 @@ void ElementsPanelWidget::updateButtons()
prj_del_diagram -> setEnabled(is_writable); prj_del_diagram -> setEnabled(is_writable);
prj_duplicate_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_up -> setEnabled(is_writable && min_position > 0);
prj_move_diagram_down -> setEnabled(is_writable && max_position < project_diagrams_count - 1); prj_move_diagram_down -> setEnabled(is_writable && max_position < project_diagrams_count - 1);
prj_move_diagram_top -> setEnabled(is_writable && min_position > 0); prj_move_diagram_top -> setEnabled(is_writable && min_position > 0);
@@ -504,6 +536,8 @@ void ElementsPanelWidget::handleContextMenu(const QPoint &pos) {
break; break;
case QET::Diagram: case QET::Diagram:
context_menu -> addAction(prj_prop_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_del_diagram);
context_menu -> addAction(prj_duplicate_diagram); context_menu -> addAction(prj_duplicate_diagram);
context_menu -> addAction(prj_move_diagram_top); context_menu -> addAction(prj_move_diagram_top);
+5
View File
@@ -46,6 +46,8 @@ class ElementsPanelWidget : public QWidget {
*prj_edit_prop, *prj_edit_prop,
*prj_prop_diagram, *prj_prop_diagram,
*prj_add_diagram, *prj_add_diagram,
*prj_insert_diagram_above,
*prj_insert_diagram_below,
*prj_del_diagram, *prj_del_diagram,
*prj_duplicate_diagram, *prj_duplicate_diagram,
*prj_move_diagram_up, *prj_move_diagram_up,
@@ -66,6 +68,7 @@ class ElementsPanelWidget : public QWidget {
signals: signals:
void requestForProject(QETProject *); void requestForProject(QETProject *);
void requestForNewDiagram(QETProject *); void requestForNewDiagram(QETProject *);
void requestForNewDiagramAt(QETProject *, int);
void requestForProjectClosing(QETProject *); void requestForProjectClosing(QETProject *);
void requestForProjectPropertiesEdition(QETProject *); void requestForProjectPropertiesEdition(QETProject *);
void requestForDiagramPropertiesEdition(Diagram *); void requestForDiagramPropertiesEdition(Diagram *);
@@ -88,6 +91,8 @@ class ElementsPanelWidget : public QWidget {
void editProjectProperties(); void editProjectProperties();
void editDiagramProperties(); void editDiagramProperties();
void newDiagram(); void newDiagram();
void insertDiagramAbove();
void insertDiagramBelow();
void deleteDiagram(); void deleteDiagram();
void duplicateDiagram(); void duplicateDiagram();
void moveDiagramUp(); 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/>. along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "cli_export.h" #include "cli_export.h"
#include "logging/eventloopwatchdog.h"
#include "logging/qetlogger.h"
#include "machine_info.h" #include "machine_info.h"
#include "qet.h" #include "qet.h"
#include "qetapp.h" #include "qetapp.h"
@@ -62,131 +64,16 @@ class EarlyFileOpenCatcher : public QObject
#endif #endif
/** /**
@brief myMessageOutput @brief qetLogMessageHandler
for debugging Installed via qInstallMessageHandler(); forwards to QetLogger, which
@param type : the messages that can be sent to a message handler holds all the actual formatting/ring/rotation state. See
@param context : were? wat? logging/qetlogger.h for the rationale (discussion #644).
@param msg : Message
*/ */
void myMessageOutput(QtMsgType type, void qetLogMessageHandler(QtMsgType type,
const QMessageLogContext &context, const QMessageLogContext &context,
const QString &msg) const QString &msg)
{ {
QetLogger::instance().handleMessage(type, context, 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!";
}
}
} }
/** /**
@@ -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: // Install the log-file message handler BEFORE the application starts:
// QETApp's constructor does the whole startup (collections, editor, // QETApp's constructor does the whole startup (collections, editor,
// opening the projects given on the command line), so installing the // opening the projects given on the command line), so installing the
// handler afterwards - as was done in the startup worker below - meant // handler afterwards - as was done in the startup worker below - meant
// exactly the interesting lines (collection and project load timers) // exactly the interesting lines (collection and project load timers)
// went to stderr, which is invisible in a Windows GUI session. // 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); SingleApplication app(argc, argv, true);
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
@@ -308,9 +207,16 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
{ {
qInfo("Start-up"); qInfo("Start-up");
// delete old log files of max 7 days old. // delete old log files of max 7 days old.
delete_old_log_files(7); QetLogger::instance().pruneOldLogFiles(7);
MachineInfo::instance()->send_info_to_debug(); 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(); return app.exec();
} }
+1 -1
View File
@@ -842,7 +842,7 @@ void ProjectView::initWidgets()
QHBoxLayout *TopRightCorner_Layout = new QHBoxLayout(); QHBoxLayout *TopRightCorner_Layout = new QHBoxLayout();
TopRightCorner_Layout->setContentsMargins(0,0,0,0); TopRightCorner_Layout->setContentsMargins(0,0,0,0);
// some place left to the 'next_right_view_button' button // some place left to the 'next_right_view_button' button
TopRightCorner_Layout->insertSpacing(1,10); TopRightCorner_Layout->addSpacing(10);
QHBoxLayout *TopLeftCorner_Layout = new QHBoxLayout(); QHBoxLayout *TopLeftCorner_Layout = new QHBoxLayout();
TopLeftCorner_Layout->setContentsMargins(0,0,0,0); TopLeftCorner_Layout->setContentsMargins(0,0,0,0);
+205
View File
@@ -267,6 +267,211 @@ QDomElement ElementData::kindInfoToXml(QDomDocument &document)
return returned_elmt; 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 * @brief ElementData::setTerminalType
* Override the terminal type by \p t_type * 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) QList<int> columnOrder; ///< Column display order (logical indices)
bool showHeaders = true; ///< Show column headers on sheet bool showHeaders = true; ///< Show column headers on sheet
PlcMasterData() {
headerFont.setFamily(QString());
cellFont.setFamily(QString());
}
bool operator==(const PlcMasterData &other) const { bool operator==(const PlcMasterData &other) const {
return ios == other.ios return ios == other.ios
&& breakPositions == other.breakPositions && breakPositions == other.breakPositions
@@ -201,6 +206,8 @@ class ElementData : public PropertiesInterface
QDomElement toXml(QDomDocument &xml_element) const override; QDomElement toXml(QDomDocument &xml_element) const override;
bool fromXml(const QDomElement &xml_element) override; bool fromXml(const QDomElement &xml_element) override;
QDomElement kindInfoToXml(QDomDocument &document); QDomElement kindInfoToXml(QDomDocument &document);
QDomElement plcMasterDataToXml(QDomDocument &document) const;
void plcMasterDataFromXml(const QDomElement &xml_plc);
void setTerminalType(ElementData::TerminalType t_type); void setTerminalType(ElementData::TerminalType t_type);
ElementData::TerminalType terminalType() const; ElementData::TerminalType terminalType() const;
+2
View File
@@ -17,6 +17,7 @@
*/ */
#include "terminaldata.h" #include "terminaldata.h"
#include "../qetapp.h"
#include "../utils/qetutils.h" #include "../utils/qetutils.h"
#include <QGraphicsObject> #include <QGraphicsObject>
@@ -38,6 +39,7 @@ TerminalData::TerminalData(QGraphicsObject *parent):
void TerminalData::init() void TerminalData::init()
{ {
m_label_font = QETApp::diagramTextsFont();
} }
TerminalData::~TerminalData() TerminalData::~TerminalData()
+53
View File
@@ -40,6 +40,8 @@
#include "machine_info.h" #include "machine_info.h"
#include "TerminalStrip/ui/terminalstripeditorwindow.h" #include "TerminalStrip/ui/terminalstripeditorwindow.h"
#include "qetversion.h" #include "qetversion.h"
#include "logging/qetlogger.h"
#include "logging/ui/diagnosticsreportdialog.h"
#include <cstdlib> #include <cstdlib>
#include <iostream> #include <iostream>
@@ -2575,6 +2577,10 @@ void QETApp::checkBackupFiles()
} }
if (stale_files.isEmpty()) { 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; 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 @brief QETApp::fetchWindowStats
Updates the booleans concerning the state of the windows 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 openTitleBlockTemplateFiles(const QStringList &);
void configureQET(); void configureQET();
void aboutQET(); void aboutQET();
void showDiagnosticsReport();
void receiveMessage(int instanceId, QByteArray message); void receiveMessage(int instanceId, QByteArray message);
private: private:
@@ -287,6 +288,7 @@ class QETApp : public QObject
void initSystemTray(); void initSystemTray();
void buildSystemTrayMenu(); void buildSystemTrayMenu();
void checkBackupFiles(); void checkBackupFiles();
void checkCrashDump();
void fetchWindowStats( void fetchWindowStats(
const QList<QETDiagramEditor *> &, const QList<QETDiagramEditor *> &,
const QList<QETElementEditor *> &, 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(requestForProjectClosing (QETProject *)), this, SLOT(closeProject(QETProject *)));
connect(pa, SIGNAL(requestForProjectPropertiesEdition (QETProject *)), this, SLOT(editProjectProperties(QETProject *))); connect(pa, SIGNAL(requestForProjectPropertiesEdition (QETProject *)), this, SLOT(editProjectProperties(QETProject *)));
connect(pa, SIGNAL(requestForNewDiagram (QETProject *)), this, SLOT(addDiagramToProject(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(requestForDiagramPropertiesEdition (Diagram *)), this, SLOT(editDiagramProperties(Diagram *)));
connect(pa, SIGNAL(requestForDiagramsDeletion (const QList<Diagram *> &)), this, SLOT(removeDiagrams(const QList<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 *>&))); connect(pa, SIGNAL(requestForDiagramMoveUp (const QList<Diagram *> &)), this, SLOT(moveDiagramUp(const QList<Diagram *>&)));
@@ -897,6 +898,8 @@ void QETDiagramEditor::setUpMenu()
// menu Projet // menu Projet
menu_project -> addAction(m_project_edit_properties); 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_project_add_diagram);
menu_project -> addAction(m_remove_diagram_from_project); menu_project -> addAction(m_remove_diagram_from_project);
menu_project -> addAction(m_clean_project); menu_project -> addAction(m_clean_project);
@@ -932,6 +935,7 @@ void QETDiagramEditor::setUpMenu()
menu_affichage -> addAction(m_mode_visualise); menu_affichage -> addAction(m_mode_visualise);
menu_affichage -> addSeparator(); menu_affichage -> addSeparator();
menu_affichage -> addAction(m_draw_grid); menu_affichage -> addAction(m_draw_grid);
menu_affichage -> addAction(m_draw_guides);
menu_affichage -> addAction(m_grey_background); menu_affichage -> addAction(m_grey_background);
menu_affichage -> addSeparator(); menu_affichage -> addSeparator();
menu_affichage -> addActions(m_zoom_actions_group.actions()); menu_affichage -> addActions(m_zoom_actions_group.actions());
@@ -2295,6 +2299,25 @@ void QETDiagramEditor::addDiagramToProject(QETProject *project)
project_view->project()->addNewDiagram(); 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 * @brief QETDiagramEditor::removeDiagram
* Wrapper für einzelne Diagramme, um Abwärtskompatibilität zu erhalten. * 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(DiagramView *);
void editDiagramProperties(Diagram *); void editDiagramProperties(Diagram *);
void addDiagramToProject(QETProject *); void addDiagramToProject(QETProject *);
void addDiagramToProjectAt(QETProject *, int);
void removeDiagram(Diagram *); void removeDiagram(Diagram *);
void removeDiagrams(const QList<Diagram *> &diagrams); void removeDiagrams(const QList<Diagram *> &diagrams);
void removeDiagramFromProject(); void removeDiagramFromProject();
@@ -200,7 +201,6 @@ class QETDiagramEditor : public QETMainWindow
*m_project_add_diagram, ///< Add a diagram to the current project. *m_project_add_diagram, ///< Add a diagram to the current project.
*m_remove_diagram_from_project, ///< Delete a diagram from 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_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_csv_export, ///< generate nomenclature
*m_add_nomenclature, ///< Add nomenclature graphics item; *m_add_nomenclature, ///< Add nomenclature graphics item;
*m_add_summary, ///<Add summary 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/>. along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "element.h" #include "element.h"
#include "../qetapp.h"
#include "../qetproject.h" #include "../qetproject.h"
#include "../PropertiesEditor/propertieseditordialog.h" #include "../PropertiesEditor/propertieseditordialog.h"
#include "../autoNum/assignvariables.h" #include "../autoNum/assignvariables.h"
@@ -33,6 +34,7 @@
#include "../qetgraphicsitem/terminal.h" #include "../qetgraphicsitem/terminal.h"
#include "../ui/elementpropertieswidget.h" #include "../ui/elementpropertieswidget.h"
#include "../undocommand/changeelementinformationcommand.h" #include "../undocommand/changeelementinformationcommand.h"
#include "../undocommand/setautonumcontextcommand.h"
#include "dynamicelementtextitem.h" #include "dynamicelementtextitem.h"
#include "elementtextitemgroup.h" #include "elementtextitemgroup.h"
#include "iostream" #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 //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. //otherwise the pos of the text will not be the same as it was at save time.
for(DynamicElementTextItem *deti : m_dynamic_text_list) for(DynamicElementTextItem *deti : m_dynamic_text_list)
@@ -993,6 +1004,15 @@ QDomElement Element::toXml(
element.appendChild(properties); 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 //Dynamic texts
QDomElement dyn_text = document.createElement(QStringLiteral("dynamic_texts")); QDomElement dyn_text = document.createElement(QStringLiteral("dynamic_texts"));
for (DynamicElementTextItem *deti : m_dynamic_text_list) for (DynamicElementTextItem *deti : m_dynamic_text_list)
@@ -1626,7 +1646,7 @@ void Element::hoverLeaveEvent(QGraphicsSceneHoverEvent *e)
(ex K for coil) with condition : (ex K for coil) with condition :
formula is empty, text tagged "label" is emptty or "_"; 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) Q_UNUSED(code_letter)
@@ -1655,8 +1675,21 @@ void Element::setUpFormula(bool code_letter)
nc, nc,
diagram(), diagram(),
element_currentAutoNum); 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()) if(!m_freeze_label && !formula.isEmpty())
{ {
@@ -1876,12 +1909,12 @@ void Element::drawPlcTable(QPainter *painter)
// Fonts // Fonts
QFont header_font = plc_data.headerFont; QFont header_font = plc_data.headerFont;
if (header_font.family().isEmpty()) { if (header_font.family().isEmpty()) {
header_font = painter->font(); header_font = QETApp::diagramTextsFont();
header_font.setBold(true); header_font.setBold(true);
} }
QFont cell_font = plc_data.cellFont; QFont cell_font = plc_data.cellFont;
if (cell_font.family().isEmpty()) { if (cell_font.family().isEmpty()) {
cell_font = painter->font(); cell_font = QETApp::diagramTextsFont();
} }
for (const QPointF &pos : positions) { for (const QPointF &pos : positions) {
+2 -1
View File
@@ -35,6 +35,7 @@ class Terminal;
class Conductor; class Conductor;
class DynamicElementTextItem; class DynamicElementTextItem;
class ElementTextItemGroup; class ElementTextItemGroup;
class QUndoCommand;
/** /**
This is the base class for electrical elements. This is the base class for electrical elements.
@@ -142,7 +143,7 @@ class Element : public QetGraphicsItem
{return m_autoNum_seq;} {return m_autoNum_seq;}
autonum::sequentialNumbers& rSequenceStruct() autonum::sequentialNumbers& rSequenceStruct()
{return m_autoNum_seq;} {return m_autoNum_seq;}
void setUpFormula(bool code_letter = true); void setUpFormula(bool code_letter = true, QUndoCommand *parent_undo = nullptr);
void setPrefix(QString); void setPrefix(QString);
QString getPrefix() const; QString getPrefix() const;
void freezeLabel(bool freeze); 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_ = 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")); about_qt_ -> setStatusTip(tr("Affiche des informations sur la bibliothèque Qt", "status bar tip"));
connect(about_qt_, SIGNAL(triggered()), qApp, SLOT(aboutQt())); 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(donate_);
help_menu_ -> addAction(about_qt_); help_menu_ -> addAction(about_qt_);
help_menu_ -> addAction(about_qet_); help_menu_ -> addAction(about_qet_);
help_menu_ -> addSeparator();
help_menu_ -> addAction(diagnostics_action_);
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
upgrade_ -> setVisible(true); upgrade_ -> setVisible(true);
+1
View File
@@ -62,6 +62,7 @@ class QETMainWindow : public QMainWindow {
QAction *upgrade_M; ///< Launch browser on QElectroTech MAC_OS_X 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 *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 *settings_menu_; ///< Settings menu
QMenu *help_menu_; ///< Help menu QMenu *help_menu_; ///< Help menu
QMenu *display_toolbars_; ///< Show/hide toolbars/docks 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) { void QETProject::addConductorAutoNum(const QString& key, const NumerotationContext& context) {
m_conductor_autonum.insert(key, 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); m_element_autonum.insert(key, context);
emit elementAutoNumAdded(key); 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) { void QETProject::addFolioAutoNum(const QString& key, const NumerotationContext& context) {
m_folio_autonum.insert(key, context); m_folio_autonum.insert(key, context);
emit autoNumContextUpdated();
} }
/** /**
+6
View File
@@ -237,6 +237,12 @@ class QETProject : public QObject
void conductorAutoNumAdded(); void conductorAutoNumAdded();
void conductorAutoNumRemoved(); void conductorAutoNumRemoved();
void folioAutoNumAdded(); 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 folioAutoNumRemoved();
void folioAutoNumChanged(QString); void folioAutoNumChanged(QString);
void defaultTitleBlockPropertiesChanged(); 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"); rpw->toSettings(settings, "diagrameditor/defaultreport");
// default xref properties // 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 // Global in QSettings speichern
QList<Diagram::Guide> current_guides = m_gpw->guides(); 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 "elementinfowidget.h"
#include <QCheckBox> #include <QCheckBox>
#include <QPushButton>
#include "../diagram.h" #include "../diagram.h"
#include "../qetapp.h" #include "../qetapp.h"
#include "../qetgraphicsitem/element.h" #include "../qetgraphicsitem/element.h"
#include "../qetinformation.h" #include "../qetinformation.h"
#include "../ui_elementinfowidget.h" #include "../ui_elementinfowidget.h"
#include "../undocommand/changeelementinformationcommand.h" #include "../undocommand/changeelementinformationcommand.h"
#include "customelementinfopartwidget.h"
#include "elementinfopartwidget.h" #include "elementinfopartwidget.h"
/** /**
@@ -47,6 +49,7 @@ ElementInfoWidget::ElementInfoWidget(Element *elmt, QWidget *parent) :
ElementInfoWidget::~ElementInfoWidget() ElementInfoWidget::~ElementInfoWidget()
{ {
qDeleteAll(m_eipw_list); qDeleteAll(m_eipw_list);
qDeleteAll(m_custom_eipw_list);
delete ui; delete ui;
} }
@@ -207,6 +210,11 @@ void ElementInfoWidget::buildInterface()
ui->scroll_vlayout->addWidget(eipw); ui->scroll_vlayout->addWidget(eipw);
m_eipw_list << 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(); ui->scroll_vlayout->addStretch();
// Existing potential isolating checkbox // Existing potential isolating checkbox
@@ -235,6 +243,67 @@ void ElementInfoWidget::buildInterface()
m_potential_isolating_cb->setVisible(false); 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 @brief ElementInfoWidget::infoPartWidgetForKey
@param key @param key
@@ -271,6 +340,21 @@ void ElementInfoWidget::updateUi()
for (ElementInfoPartWidget *eipw : m_eipw_list) { for (ElementInfoPartWidget *eipw : m_eipw_list) {
eipw -> setText (element_info[eipw->key()].toString()); 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 // Load the lock status for auto numbering
if (m_element->elementData().m_type == ElementData::Terminal) { if (m_element->elementData().m_type == ElementData::Terminal) {
QString lock_value = element_info.value(QStringLiteral("auto_num_locked")).toString(); 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 // Save the auto numbering lock status
if (m_element->elementData().m_type == ElementData::Terminal) { 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")); 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 Element;
class QUndoCommand; class QUndoCommand;
class ElementInfoPartWidget; class ElementInfoPartWidget;
class CustomElementInfoPartWidget;
class ChangeElementInformationCommand; class ChangeElementInformationCommand;
class QCheckBox; class QCheckBox;
class QPushButton;
namespace Ui { namespace Ui {
class ElementInfoWidget; class ElementInfoWidget;
@@ -63,15 +65,20 @@ class ElementInfoWidget : public AbstractElementPropertiesEditorWidget
private: private:
void buildInterface(); void buildInterface();
ElementInfoPartWidget *infoPartWidgetForKey(const QString &key) const; ElementInfoPartWidget *infoPartWidgetForKey(const QString &key) const;
QStringList predefinedKeys() const;
private slots: private slots:
void firstActivated(); void firstActivated();
void elementInfoChange(); void elementInfoChange();
void addCustomProperty(const QString &key = QString(), const QString &value = QString());
void removeCustomProperty(CustomElementInfoPartWidget *widget);
//ATTRIBUTES //ATTRIBUTES
private: private:
Ui::ElementInfoWidget *ui; Ui::ElementInfoWidget *ui;
QList <ElementInfoPartWidget *> m_eipw_list; 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_potential_isolating_cb = nullptr;
QCheckBox *m_exclude_from_bom_cb = nullptr; QCheckBox *m_exclude_from_bom_cb = nullptr;
bool m_first_activation; bool m_first_activation;
+1 -1
View File
@@ -535,7 +535,7 @@ void MasterPropertiesWidget::updateUi()
tr("Commentaire"), tr("Réf. croisée") tr("Commentaire"), tr("Réf. croisée")
}); });
m_plc_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); 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->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_plc_table->setMinimumHeight(200); 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);
}

Some files were not shown because too many files have changed in this diff Show More