mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-08-13 10:04:13 +02:00
Merge branch 'master' into Replace-automatic-conductors
This commit is contained in:
@@ -11,7 +11,6 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/master'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
@@ -145,6 +145,34 @@ else()
|
||||
)
|
||||
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(
|
||||
${PROJECT_NAME}
|
||||
PUBLIC
|
||||
|
||||
@@ -33,3 +33,17 @@ add_definitions(-DQT_MESSAGELOGCONTEXT)
|
||||
|
||||
# Build with KF5
|
||||
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)
|
||||
|
||||
@@ -116,6 +116,16 @@ set(QET_RES_FILES
|
||||
set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/cli_export.cpp
|
||||
${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.h
|
||||
${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_p.h
|
||||
${QET_DIR}/sources/richtext/ui_addlinkdialog.h
|
||||
|
||||
${QET_DIR}/sources/SearchAndReplace/searchandreplaceworker.cpp
|
||||
${QET_DIR}/sources/SearchAndReplace/searchandreplaceworker.h
|
||||
@@ -683,6 +692,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/ui/dynamicelementtextitemeditor.h
|
||||
${QET_DIR}/sources/ui/dynamicelementtextmodel.cpp
|
||||
${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.h
|
||||
${QET_DIR}/sources/ui/elementinfowidget.cpp
|
||||
@@ -761,6 +772,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/undocommand/movediagramcommand.h
|
||||
${QET_DIR}/sources/undocommand/removediagramcommand.cpp
|
||||
${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.h
|
||||
${QET_DIR}/sources/undocommand/rotatetextscommand.cpp
|
||||
|
||||
+923
-681
File diff suppressed because it is too large
Load Diff
+915
-681
File diff suppressed because it is too large
Load Diff
+917
-681
File diff suppressed because it is too large
Load Diff
+915
-681
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+916
-681
File diff suppressed because it is too large
Load Diff
+915
-681
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+917
-681
File diff suppressed because it is too large
Load Diff
+915
-681
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+940
-706
File diff suppressed because it is too large
Load Diff
+922
-686
File diff suppressed because it is too large
Load Diff
+913
-681
File diff suppressed because it is too large
Load Diff
+915
-681
File diff suppressed because it is too large
Load Diff
+913
-681
File diff suppressed because it is too large
Load Diff
+913
-681
File diff suppressed because it is too large
Load Diff
+915
-681
File diff suppressed because it is too large
Load Diff
+915
-681
File diff suppressed because it is too large
Load Diff
+915
-681
File diff suppressed because it is too large
Load Diff
+943
-709
File diff suppressed because it is too large
Load Diff
+917
-681
File diff suppressed because it is too large
Load Diff
+943
-709
File diff suppressed because it is too large
Load Diff
+915
-681
File diff suppressed because it is too large
Load Diff
+946
-710
File diff suppressed because it is too large
Load Diff
+945
-709
File diff suppressed because it is too large
Load Diff
+917
-681
File diff suppressed because it is too large
Load Diff
+945
-709
File diff suppressed because it is too large
Load Diff
+947
-709
File diff suppressed because it is too large
Load Diff
+945
-709
File diff suppressed because it is too large
Load Diff
+915
-681
File diff suppressed because it is too large
Load Diff
+913
-681
File diff suppressed because it is too large
Load Diff
+917
-681
File diff suppressed because it is too large
Load Diff
+913
-681
File diff suppressed because it is too large
Load Diff
+7
-2
@@ -173,7 +173,9 @@ HEADERS += $$files(sources/*.h) \
|
||||
$$files(sources/qet_elementscaler/*.h) \
|
||||
$$files(sources/svg/*.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) \
|
||||
$$files(sources/editor/*.cpp) \
|
||||
@@ -219,7 +221,10 @@ SOURCES += $$files(sources/*.cpp) \
|
||||
$$files(sources/qet_elementscaler/*.cpp) \
|
||||
$$files(sources/svg/*.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
|
||||
INCLUDEPATH += sources/TerminalStrip/ui
|
||||
|
||||
@@ -22,7 +22,39 @@
|
||||
#include "../qeticons.h"
|
||||
#include "elementslocation.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
#include <QStyle>
|
||||
|
||||
namespace {
|
||||
/**
|
||||
@return the folder icon overlaid with a small warning badge in the
|
||||
bottom-right corner. Used for a directory whose qet_directory could
|
||||
not be read (@see FileElementCollectionItem::m_qet_directory_unreadable),
|
||||
so the problem is visible in the tree itself and not only on hover
|
||||
via the tooltip. Built once: same folder icon, same badge, every time.
|
||||
*/
|
||||
const QIcon &unreadableFolderIcon()
|
||||
{
|
||||
static const QIcon icon = []() {
|
||||
QPixmap pixmap = QET::Icons::Folder.pixmap(16, 16);
|
||||
const QPixmap badge = QApplication::style()
|
||||
->standardIcon(QStyle::SP_MessageBoxWarning)
|
||||
.pixmap(9, 9);
|
||||
|
||||
QPainter painter(&pixmap);
|
||||
painter.drawPixmap(pixmap.width() - badge.width(),
|
||||
pixmap.height() - badge.height(),
|
||||
badge);
|
||||
painter.end();
|
||||
|
||||
return QIcon(pixmap);
|
||||
}();
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief FileElementCollectionItem::FileElementCollectionItem
|
||||
@@ -136,18 +168,41 @@ QString FileElementCollectionItem::localName()
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fall back to the raw directory name (m_path) whenever the
|
||||
// translated name can't be obtained -- qet_directory missing,
|
||||
// unreadable (e.g. a Windows path-encoding issue with special
|
||||
// characters, see bugtracker #332), malformed, or present but
|
||||
// without a usable name entry -- rather than leaving the item
|
||||
// blank.
|
||||
QString display_name;
|
||||
bool readable = false;
|
||||
QString str(fileSystemPath() % "/qet_directory");
|
||||
pugi::xml_document docu;
|
||||
if(docu.load_file(str.toStdWString().c_str()))
|
||||
if (docu.load_file(str.toStdWString().c_str()))
|
||||
{
|
||||
if (QString(docu.document_element().name())
|
||||
== "qet-directory")
|
||||
{
|
||||
readable = true;
|
||||
NamesList nl;
|
||||
nl.fromXml(docu.document_element());
|
||||
setText(nl.name());
|
||||
// Deliberately no fallback argument: a non-empty one
|
||||
// is returned *before* NamesList::name() reaches its
|
||||
// "first available translation" step, so passing
|
||||
// m_path here would replace a perfectly good name in
|
||||
// some other language with the raw directory name.
|
||||
// The fallback belongs after the chain, not inside it.
|
||||
display_name = nl.name();
|
||||
}
|
||||
}
|
||||
setText(display_name.isEmpty() ? m_path : display_name);
|
||||
|
||||
// Only a file-level failure counts: a readable qet-directory
|
||||
// with no entry for the current language is not an error,
|
||||
// NamesList::name() resolves that on its own. Recorded here
|
||||
// and reported by setUpData(), which sets the tooltip after
|
||||
// this runs.
|
||||
m_qet_directory_unreadable = !readable;
|
||||
}
|
||||
}
|
||||
else if (isElement()) {
|
||||
@@ -350,7 +405,21 @@ void FileElementCollectionItem::setUpData()
|
||||
}
|
||||
}
|
||||
|
||||
setToolTip(collectionPath());
|
||||
// Falling back to the raw directory name keeps the folder usable, but
|
||||
// on its own it hides the fact that a file is broken: the user sees a
|
||||
// plausible name and never learns there is anything to repair. Say so
|
||||
// above the collection path, which stays as the last line the way the
|
||||
// element tooltip above builds it.
|
||||
QStringList tip;
|
||||
if (isDir() && m_qet_directory_unreadable)
|
||||
{
|
||||
tip << QObject::tr("Le fichier « %1 » est absent ou illisible : "
|
||||
"le nom traduit de ce dossier n'a pas pu être lu, "
|
||||
"son nom de dossier est affiché à la place.")
|
||||
.arg(fileSystemPath() % "/qet_directory");
|
||||
}
|
||||
tip << collectionPath();
|
||||
setToolTip(tip.join(QLatin1Char('\n')));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -361,6 +430,19 @@ void FileElementCollectionItem::setUpData()
|
||||
*/
|
||||
void FileElementCollectionItem::setUpIcon()
|
||||
{
|
||||
// Must return unconditionally once an icon is set: setIcon() calls
|
||||
// setData(), which emits dataChanged() regardless of whether the new
|
||||
// icon differs from the old one (QIcon has no meaningful equality).
|
||||
// QTreeView responds to dataChanged() by recomputing the row's size
|
||||
// hint, which re-enters data() for this same index -- so without this
|
||||
// guard, any repeated setIcon() here recurses until the stack
|
||||
// overflows. Confirmed by crash report on PR #633.
|
||||
//
|
||||
// This item's m_qet_directory_unreadable is already final by the time
|
||||
// this can run at all: ElementsCollectionModel only attaches itself
|
||||
// to the tree view (making data() reachable) from loadingFinished(),
|
||||
// which fires after the QtConcurrent::map over every item -- this one
|
||||
// included -- has completed. So there is no race to work around here.
|
||||
if (!icon().isNull())
|
||||
return;
|
||||
|
||||
@@ -380,7 +462,8 @@ void FileElementCollectionItem::setUpIcon()
|
||||
else
|
||||
{
|
||||
if (isDir()) {
|
||||
setIcon(QET::Icons::Folder);
|
||||
setIcon(m_qet_directory_unreadable ? unreadableFolderIcon()
|
||||
: QET::Icons::Folder);
|
||||
} else {
|
||||
if (m_path.endsWith(".qetmak")) {
|
||||
setIcon(QIcon());
|
||||
|
||||
@@ -64,6 +64,11 @@ class FileElementCollectionItem : public ElementCollectionItem
|
||||
|
||||
private:
|
||||
QString m_path;
|
||||
/// True when this directory's qet_directory file is missing or
|
||||
/// unreadable, so setUpData() can say so in the tooltip. Recorded
|
||||
/// rather than acted on in localName(), because setUpData() resets
|
||||
/// the tooltip afterwards and would otherwise discard it.
|
||||
bool m_qet_directory_unreadable = false;
|
||||
};
|
||||
|
||||
#endif // FILEELEMENTCOLLECTIONITEM2_H
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace autonum
|
||||
sequentialNumbers::sequentialNumbers(const sequentialNumbers &other)
|
||||
{
|
||||
unit = other.unit;
|
||||
wrap = other.wrap;
|
||||
unit_folio = other.unit_folio;
|
||||
ten = other.ten;
|
||||
ten_folio = other.ten_folio;
|
||||
@@ -57,6 +58,7 @@ namespace autonum
|
||||
return (*this);
|
||||
|
||||
unit = other.unit;
|
||||
wrap = other.wrap;
|
||||
unit_folio = other.unit_folio;
|
||||
ten = other.ten;
|
||||
ten_folio = other.ten_folio;
|
||||
@@ -70,6 +72,7 @@ namespace autonum
|
||||
bool sequentialNumbers::operator==(const sequentialNumbers &other) const
|
||||
{
|
||||
if (unit == other.unit && \
|
||||
wrap == other.wrap && \
|
||||
unit_folio == other.unit_folio && \
|
||||
ten == other.ten && \
|
||||
ten_folio == other.ten_folio && \
|
||||
@@ -107,6 +110,11 @@ namespace autonum
|
||||
document,
|
||||
"unit",
|
||||
unit.join(";")));
|
||||
if (!wrap.isEmpty())
|
||||
element.appendChild(QETXML::textToDomElement(
|
||||
document,
|
||||
"wrap",
|
||||
wrap.join(";")));
|
||||
if (!unit_folio.isEmpty())
|
||||
element.appendChild(QETXML::textToDomElement(
|
||||
document,
|
||||
@@ -156,6 +164,11 @@ namespace autonum
|
||||
from = element.firstChildElement("unit");
|
||||
unit = from.text().split(";");
|
||||
|
||||
//Absent from files written before cyclic parts could be
|
||||
//rendered; an empty list is the correct reading of that.
|
||||
from = element.firstChildElement("wrap");
|
||||
wrap = from.text().split(";");
|
||||
|
||||
from = element.firstChildElement("unitFolio");
|
||||
unit_folio = from.text().split(";");
|
||||
|
||||
@@ -179,6 +192,7 @@ namespace autonum
|
||||
void sequentialNumbers::clear()
|
||||
{
|
||||
unit.clear();
|
||||
wrap.clear();
|
||||
unit_folio.clear();
|
||||
ten.clear();
|
||||
ten_folio.clear();
|
||||
@@ -434,7 +448,8 @@ namespace autonum
|
||||
qMax(
|
||||
qMax(m_seq_struct.hundred.size(),
|
||||
m_seq_struct.ten.size()),
|
||||
m_seq_struct.alpha.size())
|
||||
qMax(m_seq_struct.alpha.size(),
|
||||
m_seq_struct.wrap.size()))
|
||||
);
|
||||
|
||||
for (int i=1; i<=max ; i++)
|
||||
@@ -442,6 +457,9 @@ namespace autonum
|
||||
if (m_assigned_label.contains("%sequ_" + QString::number(i)) && m_seq_struct.unit.size() >= i) {
|
||||
m_assigned_label.replace("%sequ_" + QString::number(i),m_seq_struct.unit.at(i-1));
|
||||
}
|
||||
if (m_assigned_label.contains("%seqw_" + QString::number(i)) && m_seq_struct.wrap.size() >= i) {
|
||||
m_assigned_label.replace("%seqw_" + QString::number(i),m_seq_struct.wrap.at(i-1));
|
||||
}
|
||||
if (m_assigned_label.contains("%seqt_" + QString::number(i)) && m_seq_struct.ten.size() >= i) {
|
||||
m_assigned_label.replace("%seqt_" + QString::number(i),m_seq_struct.ten.at(i-1));
|
||||
}
|
||||
@@ -479,15 +497,26 @@ namespace autonum
|
||||
{
|
||||
if (context.itemAt(i).at(0) == type)
|
||||
{
|
||||
const QStringList item = context.itemAt(i);
|
||||
//A zero-padding mask, spreadsheet style: its length is the
|
||||
//minimum number of digits. It overrides the width implied
|
||||
//by the part type, so "Chiffre 01" with a mask of "0000"
|
||||
//pads to four. An absent mask -- which is every context
|
||||
//written before the field existed -- falls through to the
|
||||
//type's own width, so nothing about existing projects
|
||||
//changes.
|
||||
const QString mask = NumerotationContext::formatOf(item);
|
||||
QString number;
|
||||
if (type == "ten" || type == "tenfolio")
|
||||
number = QString("%1").arg(context.itemAt(i).at(1).toInt(), 2, 10, QChar('0'));
|
||||
else if (type == "hundred" || type == "hundredfolio")
|
||||
number = QString("%1").arg(context.itemAt(i).at(1).toInt(), 3, 10, QChar('0'));
|
||||
else if (type == "alpha")
|
||||
if (type == "alpha")
|
||||
//Alphabetic value, not an integer -- used as-is.
|
||||
number = context.itemAt(i).at(1);
|
||||
else number = QString::number(context.itemAt(i).at(1).toInt());
|
||||
number = item.at(1);
|
||||
else if (!mask.isEmpty())
|
||||
number = QString("%1").arg(item.at(1).toInt(), mask.length(), 10, QChar('0'));
|
||||
else if (type == "ten" || type == "tenfolio")
|
||||
number = QString("%1").arg(item.at(1).toInt(), 2, 10, QChar('0'));
|
||||
else if (type == "hundred" || type == "hundredfolio")
|
||||
number = QString("%1").arg(item.at(1).toInt(), 3, 10, QChar('0'));
|
||||
else number = QString::number(item.at(1).toInt());
|
||||
list.append(number);
|
||||
}
|
||||
}
|
||||
@@ -553,6 +582,10 @@ namespace autonum
|
||||
{
|
||||
autonum::setSequentialToList(seqStruct.unit, context,"unit");
|
||||
}
|
||||
if (label.contains("%seqw_"))
|
||||
{
|
||||
autonum::setSequentialToList(seqStruct.wrap, context,"wrap");
|
||||
}
|
||||
if (label.contains("%sequf_"))
|
||||
{
|
||||
autonum::setSequentialToList(seqStruct.unit_folio, context,"unitfolio");
|
||||
@@ -594,6 +627,7 @@ namespace autonum
|
||||
QString value;
|
||||
QString formula;
|
||||
int count_unit = 0;
|
||||
int count_wrap = 0;
|
||||
int count_unitf = 0;
|
||||
int count_ten = 0;
|
||||
int count_tenf = 0;
|
||||
@@ -636,6 +670,10 @@ namespace autonum
|
||||
count_unit++;
|
||||
formula.append("%sequ_" + QString::number(count_unit));
|
||||
}
|
||||
else if (type == "wrap") {
|
||||
count_wrap++;
|
||||
formula.append("%seqw_" + QString::number(count_wrap));
|
||||
}
|
||||
else if (type == "unitfolio") {
|
||||
count_unitf++;
|
||||
formula.append("%sequf_" + QString::number(count_unitf));
|
||||
|
||||
@@ -47,6 +47,8 @@ namespace autonum
|
||||
void clear();
|
||||
|
||||
QStringList unit;
|
||||
/// Values of the cyclic (modulo) parts, referenced by %seqw_N.
|
||||
QStringList wrap;
|
||||
QStringList unit_folio;
|
||||
QStringList ten;
|
||||
QStringList ten_folio;
|
||||
|
||||
@@ -52,13 +52,18 @@ void NumerotationContext::clear ()
|
||||
@param increase the increase number of value
|
||||
@param initialvalue
|
||||
@param modulus wrap-and-carry modulus (0 means "not a wrapping part")
|
||||
@param format zero-padding mask, spreadsheet style: "00" pads to two
|
||||
digits, "000" to three. Empty keeps the part type's natural width, so
|
||||
an absent format reproduces exactly the behaviour of every context
|
||||
written before this field existed.
|
||||
@return true if value is append
|
||||
*/
|
||||
bool NumerotationContext::addValue(const QString &type,
|
||||
const QVariant &value,
|
||||
const int increase,
|
||||
const int initialvalue,
|
||||
const int modulus) {
|
||||
const int modulus,
|
||||
const QString &format) {
|
||||
if (!keyIsAcceptable(type) && !value.canConvert<QString>())
|
||||
return false;
|
||||
if (keyIsNumber(type) && !value.canConvert<int>())
|
||||
@@ -74,7 +79,9 @@ bool NumerotationContext::addValue(const QString &type,
|
||||
+ "|"
|
||||
+ QString::number(initialvalue)
|
||||
+ "|"
|
||||
+ QString::number(modulus);
|
||||
+ QString::number(modulus)
|
||||
+ "|"
|
||||
+ QString(format).remove("|");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -179,6 +186,9 @@ QDomElement NumerotationContext::toXml(QDomDocument &d, const QString& str) {
|
||||
if (strl.at(0) == ("wrap") && strl.size() > 4) {
|
||||
part.setAttribute("modulus", strl.at(4));
|
||||
}
|
||||
if (strl.size() > 5 && !strl.at(5).isEmpty()) {
|
||||
part.setAttribute("format", strl.at(5));
|
||||
}
|
||||
num_auto.appendChild(part);
|
||||
}
|
||||
return num_auto;
|
||||
@@ -190,7 +200,7 @@ QDomElement NumerotationContext::toXml(QDomDocument &d, const QString& str) {
|
||||
*/
|
||||
void NumerotationContext::fromXml(QDomElement &e) {
|
||||
clear();
|
||||
foreach(QDomElement qde, QET::findInDomElement(e, "part")) addValue(qde.attribute("type"), qde.attribute("value"), qde.attribute("increase").toInt(), qde.attribute("initialvalue").toInt(), qde.attribute("modulus").toInt());
|
||||
foreach(QDomElement qde, QET::findInDomElement(e, "part")) addValue(qde.attribute("type"), qde.attribute("value"), qde.attribute("increase").toInt(), qde.attribute("initialvalue").toInt(), qde.attribute("modulus").toInt(), qde.attribute("format"));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,5 +216,17 @@ void NumerotationContext::replaceValue(int index, QString content) {
|
||||
QString increase = strl.at(2);
|
||||
QString initvalue = strl.at(3);
|
||||
QString modulus = strl.size() > 4 ? strl.at(4) : QStringLiteral("0");
|
||||
content_[index] = type + "|" + value + "|" + increase + "|" + initvalue + "|" + modulus;
|
||||
QString format = strl.size() > 5 ? strl.at(5) : QString();
|
||||
content_[index] = type + "|" + value + "|" + increase + "|" + initvalue + "|" + modulus + "|" + format;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NumerotationContext::formatOf
|
||||
@param item : a context item as returned by itemAt()
|
||||
@return the part's zero-padding mask, or an empty string when it has
|
||||
none -- which every context written before the field existed will be.
|
||||
*/
|
||||
QString NumerotationContext::formatOf(const QStringList &item)
|
||||
{
|
||||
return item.size() > 5 ? item.at(5) : QString();
|
||||
}
|
||||
|
||||
@@ -37,7 +37,11 @@ class NumerotationContext
|
||||
const QVariant & = QVariant(1),
|
||||
const int = 1,
|
||||
const int = 0,
|
||||
const int = 0);
|
||||
const int = 0,
|
||||
const QString & = QString());
|
||||
/// Zero-padding mask of a part, e.g. "00"; empty means the type's
|
||||
/// own natural width. See addValue().
|
||||
static QString formatOf(const QStringList &item);
|
||||
QString operator[] (const int &) const;
|
||||
void operator << (const NumerotationContext &);
|
||||
int size() const;
|
||||
|
||||
@@ -259,7 +259,7 @@ NumerotationContext NumStrategy::nextString (const NumerotationContext &nc,
|
||||
{
|
||||
QStringList strl = nc.itemAt(i);
|
||||
NumerotationContext newnc;
|
||||
newnc.addValue(strl.at(0), strl.at(1), strl.at(2).toInt());
|
||||
newnc.addValue(strl.at(0), strl.at(1), strl.at(2).toInt(), 0, 0, NumerotationContext::formatOf(strl));
|
||||
return (newnc);
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ NumerotationContext NumStrategy::nextNumber (const NumerotationContext &nc,
|
||||
QStringList strl = nc.itemAt(i);
|
||||
NumerotationContext newnc;
|
||||
QString value = QString::number( (strl.at(1).toInt()) + (strl.at(2).toInt()) );
|
||||
newnc.addValue(strl.at(0), value, strl.at(2).toInt(), strl.at(3).toInt());
|
||||
newnc.addValue(strl.at(0), value, strl.at(2).toInt(), strl.at(3).toInt(), 0, NumerotationContext::formatOf(strl));
|
||||
return (newnc);
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ NumerotationContext NumStrategy::previousNumber(const NumerotationContext &nc,
|
||||
QStringList strl = nc.itemAt(i);
|
||||
NumerotationContext newnc;
|
||||
QString value = QString::number( (strl.at(1).toInt()) - (strl.at(2).toInt()) );
|
||||
newnc.addValue(strl.at(0), value, strl.at(2).toInt(), strl.at(3).toInt());
|
||||
newnc.addValue(strl.at(0), value, strl.at(2).toInt(), strl.at(3).toInt(), 0, NumerotationContext::formatOf(strl));
|
||||
return (newnc);
|
||||
}
|
||||
|
||||
@@ -550,7 +550,7 @@ NumerotationContext WrapNum::next (const NumerotationContext &nc, const int i) c
|
||||
int new_value = strl.at(1).toInt() + increase;
|
||||
if (modulus > 0)
|
||||
new_value %= modulus;
|
||||
newnc.addValue(strl.at(0), QString::number(new_value), increase, strl.at(3).toInt(), modulus);
|
||||
newnc.addValue(strl.at(0), QString::number(new_value), increase, strl.at(3).toInt(), modulus, NumerotationContext::formatOf(strl));
|
||||
return (newnc);
|
||||
}
|
||||
|
||||
@@ -570,7 +570,7 @@ NumerotationContext WrapNum::previous(const NumerotationContext &nc, const int i
|
||||
if (new_value < 0)
|
||||
new_value += modulus;
|
||||
}
|
||||
newnc.addValue(strl.at(0), QString::number(new_value), increase, strl.at(3).toInt(), modulus);
|
||||
newnc.addValue(strl.at(0), QString::number(new_value), increase, strl.at(3).toInt(), modulus, NumerotationContext::formatOf(strl));
|
||||
return (newnc);
|
||||
}
|
||||
|
||||
@@ -706,7 +706,7 @@ NumerotationContext AlphaNum::next (const NumerotationContext &nc, const int i)
|
||||
{
|
||||
QStringList strl = nc.itemAt(i);
|
||||
NumerotationContext newnc;
|
||||
newnc.addValue(strl.at(0), incrementAlpha(strl.at(1)), strl.at(2).toInt());
|
||||
newnc.addValue(strl.at(0), incrementAlpha(strl.at(1)), strl.at(2).toInt(), 0, 0, NumerotationContext::formatOf(strl));
|
||||
return (newnc);
|
||||
}
|
||||
|
||||
@@ -718,7 +718,7 @@ NumerotationContext AlphaNum::previous(const NumerotationContext &nc, const int
|
||||
{
|
||||
QStringList strl = nc.itemAt(i);
|
||||
NumerotationContext newnc;
|
||||
newnc.addValue(strl.at(0), decrementAlpha(strl.at(1)), strl.at(2).toInt());
|
||||
newnc.addValue(strl.at(0), decrementAlpha(strl.at(1)), strl.at(2).toInt(), 0, 0, NumerotationContext::formatOf(strl));
|
||||
return (newnc);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
#include "../numerotationcontext.h"
|
||||
#include "ui_autonumberingdockwidget.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QLineEdit>
|
||||
|
||||
/**
|
||||
@brief AutoNumberingDockWidget::AutoNumberingDockWidget
|
||||
Constructor
|
||||
@@ -58,6 +61,9 @@ void AutoNumberingDockWidget::clear()
|
||||
ui->m_conductor_cb->clear();
|
||||
ui->m_element_cb->clear();
|
||||
ui->m_folio_cb->clear();
|
||||
ui->m_conductor_value_le->clear();
|
||||
ui->m_element_value_le->clear();
|
||||
ui->m_folio_value_le->clear();
|
||||
}
|
||||
|
||||
void AutoNumberingDockWidget::projectClosed()
|
||||
@@ -109,6 +115,8 @@ void AutoNumberingDockWidget::setProject(QETProject *project,
|
||||
this,SLOT(setActive()));
|
||||
|
||||
//Conductor, Element and Folio Signals
|
||||
disconnect(m_project, &QETProject::autoNumContextUpdated,
|
||||
this, &AutoNumberingDockWidget::refreshValueFields);
|
||||
disconnect(m_project, &QETProject::destroyed,
|
||||
this, &AutoNumberingDockWidget::projectClosed);
|
||||
}
|
||||
@@ -146,6 +154,8 @@ void AutoNumberingDockWidget::setProject(QETProject *project,
|
||||
this,SLOT(setActive()));
|
||||
|
||||
//Conductor, Element and Folio Signals
|
||||
connect(m_project, &QETProject::autoNumContextUpdated,
|
||||
this, &AutoNumberingDockWidget::refreshValueFields);
|
||||
connect(m_project, &QETProject::destroyed,
|
||||
this, &AutoNumberingDockWidget::projectClosed);
|
||||
|
||||
@@ -189,6 +199,12 @@ void AutoNumberingDockWidget::setContext()
|
||||
{ ui->m_folio_cb -> addItem(str);}
|
||||
}
|
||||
|
||||
//The combo boxes have just been repopulated, so the value fields next
|
||||
//to them are showing whatever the previous project left there.
|
||||
refreshValueField(ui->m_conductor_cb, ui->m_conductor_value_le, AutoNumCategory::Conductor);
|
||||
refreshValueField(ui->m_element_cb, ui->m_element_value_le, AutoNumCategory::Element);
|
||||
refreshValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
|
||||
|
||||
this->setActive();
|
||||
}
|
||||
|
||||
@@ -263,6 +279,7 @@ void AutoNumberingDockWidget::on_m_conductor_cb_activated(int)
|
||||
m_project->setCurrentConductorAutoNum(current_autonum);
|
||||
m_project_view->currentDiagram()->diagram()->setConductorsAutonumName(current_autonum);
|
||||
m_project_view->currentDiagram()->diagram()->loadCndFolioSeq();
|
||||
refreshValueField(ui->m_conductor_cb, ui->m_conductor_value_le, AutoNumCategory::Conductor);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -291,6 +308,7 @@ void AutoNumberingDockWidget::on_m_element_cb_activated(int)
|
||||
{
|
||||
m_project->setCurrrentElementAutonum(ui->m_element_cb->currentText());
|
||||
m_project_view->currentDiagram()->diagram()->loadElmtFolioSeq();
|
||||
refreshValueField(ui->m_element_cb, ui->m_element_value_le, AutoNumCategory::Element);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -328,6 +346,7 @@ void AutoNumberingDockWidget::on_m_folio_cb_activated(int) {
|
||||
m_project->setDefaultTitleBlockProperties(ip);
|
||||
}
|
||||
emit(folioAutoNumChanged(current_autonum));
|
||||
refreshValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
|
||||
}
|
||||
|
||||
void AutoNumberingDockWidget::on_m_configure_pb_clicked()
|
||||
@@ -339,3 +358,209 @@ void AutoNumberingDockWidget::on_m_configure_pb_clicked()
|
||||
ppd.exec();
|
||||
}
|
||||
}
|
||||
|
||||
void AutoNumberingDockWidget::on_m_conductor_reset_start_pb_clicked()
|
||||
{
|
||||
resetAutoNum(ui->m_conductor_cb, AutoNumCategory::Conductor);
|
||||
}
|
||||
|
||||
void AutoNumberingDockWidget::on_m_element_reset_start_pb_clicked()
|
||||
{
|
||||
resetAutoNum(ui->m_element_cb, AutoNumCategory::Element);
|
||||
}
|
||||
|
||||
void AutoNumberingDockWidget::on_m_folio_reset_start_pb_clicked()
|
||||
{
|
||||
resetAutoNum(ui->m_folio_cb, AutoNumCategory::Folio);
|
||||
}
|
||||
|
||||
void AutoNumberingDockWidget::on_m_conductor_value_le_editingFinished()
|
||||
{
|
||||
applyValueField(ui->m_conductor_cb, ui->m_conductor_value_le, AutoNumCategory::Conductor);
|
||||
}
|
||||
|
||||
void AutoNumberingDockWidget::on_m_element_value_le_editingFinished()
|
||||
{
|
||||
applyValueField(ui->m_element_cb, ui->m_element_value_le, AutoNumCategory::Element);
|
||||
}
|
||||
|
||||
void AutoNumberingDockWidget::on_m_folio_value_le_editingFinished()
|
||||
{
|
||||
applyValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AutoNumberingDockWidget::contextFor
|
||||
@return the numerotation context named by combo_box, for category
|
||||
*/
|
||||
NumerotationContext AutoNumberingDockWidget::contextFor(QComboBox *combo_box, AutoNumCategory category) const
|
||||
{
|
||||
const QString key = combo_box->currentText();
|
||||
switch (category) {
|
||||
case AutoNumCategory::Conductor: return m_project->conductorAutoNum(key);
|
||||
case AutoNumCategory::Element: return m_project->elementAutoNum(key);
|
||||
case AutoNumCategory::Folio: return m_project->folioAutoNum(key);
|
||||
}
|
||||
return NumerotationContext();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AutoNumberingDockWidget::storeContext
|
||||
Write context back under the name selected in combo_box and flag the
|
||||
project as modified -- without that last step the change is not saved
|
||||
and the user is never asked to save it on close.
|
||||
*/
|
||||
void AutoNumberingDockWidget::storeContext(QComboBox *combo_box, AutoNumCategory category, const NumerotationContext &context)
|
||||
{
|
||||
const QString key = combo_box->currentText();
|
||||
switch (category) {
|
||||
case AutoNumCategory::Conductor: m_project->addConductorAutoNum(key, context); break;
|
||||
case AutoNumCategory::Element: m_project->addElementAutoNum(key, context); break;
|
||||
case AutoNumCategory::Folio: m_project->addFolioAutoNum(key, context); break;
|
||||
}
|
||||
m_project->setModified(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AutoNumberingDockWidget::counterIndex
|
||||
@return the index of the part the value field shows: the last one that
|
||||
actually progresses, i.e. the least significant digit of the number.
|
||||
-1 when the context has no progressing part at all.
|
||||
*/
|
||||
int AutoNumberingDockWidget::counterIndex(const NumerotationContext &context)
|
||||
{
|
||||
for (int i = context.size() - 1 ; i >= 0 ; --i)
|
||||
{
|
||||
const QString type = context.itemAt(i).at(0);
|
||||
if (type == QLatin1String("unit")
|
||||
|| type == QLatin1String("ten")
|
||||
|| type == QLatin1String("hundred")
|
||||
|| type == QLatin1String("unitfolio")
|
||||
|| type == QLatin1String("tenfolio")
|
||||
|| type == QLatin1String("hundredfolio")
|
||||
|| type == QLatin1String("wrap")
|
||||
|| type == QLatin1String("alpha"))
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AutoNumberingDockWidget::refreshValueFields
|
||||
Re-read all three value fields from the project. Called whenever a
|
||||
numerotation context's values change, which includes every element or
|
||||
conductor that consumes the next number -- without this the field only
|
||||
caught up when the user re-picked a rule from the combo box, because
|
||||
the combo's activated() signal fires on user interaction alone.
|
||||
*/
|
||||
void AutoNumberingDockWidget::refreshValueFields()
|
||||
{
|
||||
//Leave alone a field the user is typing in: numbering an element
|
||||
//refreshes all three, and overwriting a half-typed value under the
|
||||
//cursor is worse than showing it a moment out of date. Only this
|
||||
//automatic path skips; an explicit refresh after a reset or an edit
|
||||
//still writes, so the field always ends up canonical.
|
||||
if (!ui->m_conductor_value_le->hasFocus())
|
||||
refreshValueField(ui->m_conductor_cb, ui->m_conductor_value_le, AutoNumCategory::Conductor);
|
||||
if (!ui->m_element_value_le->hasFocus())
|
||||
refreshValueField(ui->m_element_cb, ui->m_element_value_le, AutoNumCategory::Element);
|
||||
if (!ui->m_folio_value_le->hasFocus())
|
||||
refreshValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AutoNumberingDockWidget::refreshValueField
|
||||
Show the current value of the selected context's counter, so the field
|
||||
always reflects where the numbering has actually got to.
|
||||
*/
|
||||
void AutoNumberingDockWidget::refreshValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category)
|
||||
{
|
||||
if (!m_project || combo_box->currentText().isEmpty())
|
||||
{
|
||||
line_edit->clear();
|
||||
line_edit->setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const NumerotationContext context = contextFor(combo_box, category);
|
||||
const int index = counterIndex(context);
|
||||
line_edit->setEnabled(index >= 0);
|
||||
line_edit->setText(index >= 0 ? context.itemAt(index).at(1) : QString());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AutoNumberingDockWidget::applyValueField
|
||||
Write the value typed in line_edit to the counter it displays. An empty
|
||||
field is treated as "no change" rather than as an empty value, so
|
||||
clearing the box by accident cannot wipe the counter.
|
||||
*/
|
||||
void AutoNumberingDockWidget::applyValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category)
|
||||
{
|
||||
if (!m_project || combo_box->currentText().isEmpty())
|
||||
return;
|
||||
|
||||
NumerotationContext context = contextFor(combo_box, category);
|
||||
const int index = counterIndex(context);
|
||||
if (index < 0)
|
||||
return;
|
||||
|
||||
const QString typed = line_edit->text();
|
||||
if (typed.isEmpty() || typed == context.itemAt(index).at(1))
|
||||
{
|
||||
refreshValueField(combo_box, line_edit, category);
|
||||
return;
|
||||
}
|
||||
|
||||
context.replaceValue(index, typed);
|
||||
storeContext(combo_box, category, context);
|
||||
refreshValueField(combo_box, line_edit, category);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief AutoNumberingDockWidget::resetAutoNum
|
||||
Reset the numerotation context currently selected in combo_box back to
|
||||
a per-type starting value, then write it back. Does nothing if no
|
||||
context is selected.
|
||||
|
||||
Only parts that actually progress are touched: folio-anchored numeric
|
||||
types go back to their own stored initialvalue, plain numeric types go
|
||||
back to "1", a wrap part goes back to "0" because a modulo counter
|
||||
cycles over [0, modulus) -- a PLC card addressed %IX0.0..%IX0.31 starts
|
||||
at 0, not 1 -- and alpha goes back to "a". Non-incrementing types
|
||||
(string, plant, locmach, idfolio, folio, elementline, elementcolumn,
|
||||
elementprefix) are left alone: there is no "start" for them distinct
|
||||
from the fixed or contextual value the user configured.
|
||||
*/
|
||||
void AutoNumberingDockWidget::resetAutoNum(QComboBox *combo_box, AutoNumCategory category)
|
||||
{
|
||||
if (!m_project || combo_box->currentText().isEmpty())
|
||||
return;
|
||||
|
||||
NumerotationContext context = contextFor(combo_box, category);
|
||||
|
||||
for (int i = 0 ; i < context.size() ; ++i)
|
||||
{
|
||||
const QStringList item = context.itemAt(i);
|
||||
const QString &type = item.at(0);
|
||||
if (type == QLatin1String("unitfolio")
|
||||
|| type == QLatin1String("tenfolio")
|
||||
|| type == QLatin1String("hundredfolio"))
|
||||
context.replaceValue(i, item.size() > 3 ? item.at(3) : QStringLiteral("1"));
|
||||
else if (type == QLatin1String("unit")
|
||||
|| type == QLatin1String("ten")
|
||||
|| type == QLatin1String("hundred"))
|
||||
context.replaceValue(i, QStringLiteral("1"));
|
||||
else if (type == QLatin1String("wrap"))
|
||||
context.replaceValue(i, QStringLiteral("0"));
|
||||
else if (type == QLatin1String("alpha"))
|
||||
context.replaceValue(i, QStringLiteral("a"));
|
||||
}
|
||||
|
||||
storeContext(combo_box, category, context);
|
||||
|
||||
switch (category) {
|
||||
case AutoNumCategory::Conductor: refreshValueField(combo_box, ui->m_conductor_value_le, category); break;
|
||||
case AutoNumCategory::Element: refreshValueField(combo_box, ui->m_element_value_le, category); break;
|
||||
case AutoNumCategory::Folio: refreshValueField(combo_box, ui->m_folio_value_le, category); break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
|
||||
#include <QDockWidget>
|
||||
|
||||
class QComboBox;
|
||||
class QLineEdit;
|
||||
|
||||
namespace Ui {
|
||||
class AutoNumberingDockWidget;
|
||||
}
|
||||
@@ -51,13 +54,45 @@ class AutoNumberingDockWidget : public QDockWidget
|
||||
void folioAutoNumChanged();
|
||||
void clear();
|
||||
void projectClosed();
|
||||
void refreshValueFields();
|
||||
|
||||
void on_m_configure_pb_clicked();
|
||||
|
||||
void on_m_conductor_reset_start_pb_clicked();
|
||||
void on_m_element_reset_start_pb_clicked();
|
||||
void on_m_folio_reset_start_pb_clicked();
|
||||
|
||||
void on_m_conductor_value_le_editingFinished();
|
||||
void on_m_element_value_le_editingFinished();
|
||||
void on_m_folio_value_le_editingFinished();
|
||||
|
||||
signals:
|
||||
void folioAutoNumChanged(QString);
|
||||
|
||||
private:
|
||||
enum class AutoNumCategory { Conductor, Element, Folio };
|
||||
|
||||
/**
|
||||
@brief resetAutoNum
|
||||
Reset the numerotation context currently selected in combo_box
|
||||
(for the given category) to a per-type starting value. Does
|
||||
nothing if no context is selected.
|
||||
*/
|
||||
void resetAutoNum(QComboBox *combo_box, AutoNumCategory category);
|
||||
|
||||
/// Read/write the numerotation context named in combo_box.
|
||||
NumerotationContext contextFor(QComboBox *combo_box, AutoNumCategory category) const;
|
||||
void storeContext(QComboBox *combo_box, AutoNumCategory category, const NumerotationContext &context);
|
||||
|
||||
/// Index of the counter the value field shows and edits: the last
|
||||
/// part that actually progresses. Returns -1 when the context has
|
||||
/// no such part (e.g. it is only fixed text).
|
||||
static int counterIndex(const NumerotationContext &context);
|
||||
|
||||
/// Refresh a value field from its context, and apply a typed value.
|
||||
void refreshValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category);
|
||||
void applyValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category);
|
||||
|
||||
Ui::AutoNumberingDockWidget *ui;
|
||||
QETProject* m_project = nullptr;
|
||||
ProjectView* m_project_view = nullptr;
|
||||
|
||||
@@ -28,6 +28,39 @@
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="m_conductor_cb"/>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QPushButton" name="m_conductor_reset_start_pb">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>24</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Réinitialiser à la valeur de départ</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../qelectrotech.qrc">
|
||||
<normaloff>:/ico/16x16/view-refresh.png</normaloff>:/ico/16x16/view-refresh.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="3">
|
||||
<widget class="QLineEdit" name="m_conductor_value_le">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>70</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Valeur actuelle du compteur. Saisir une nouvelle valeur et valider pour la modifier.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
@@ -42,9 +75,75 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QPushButton" name="m_element_reset_start_pb">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>24</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Réinitialiser à la valeur de départ</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../qelectrotech.qrc">
|
||||
<normaloff>:/ico/16x16/view-refresh.png</normaloff>:/ico/16x16/view-refresh.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="3">
|
||||
<widget class="QLineEdit" name="m_element_value_le">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>70</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Valeur actuelle du compteur. Saisir une nouvelle valeur et valider pour la modifier.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QComboBox" name="m_folio_cb"/>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<widget class="QPushButton" name="m_folio_reset_start_pb">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>24</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Réinitialiser à la valeur de départ</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../qelectrotech.qrc">
|
||||
<normaloff>:/ico/16x16/view-refresh.png</normaloff>:/ico/16x16/view-refresh.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="3">
|
||||
<widget class="QLineEdit" name="m_folio_value_le">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>70</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Valeur actuelle du compteur. Saisir une nouvelle valeur et valider pour la modifier.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#include "numparteditorw.h"
|
||||
#include "ui_numparteditorw.h"
|
||||
|
||||
#include "../numerotationcontext.h"
|
||||
|
||||
#include <QRegularExpressionValidator>
|
||||
|
||||
/**
|
||||
@@ -35,6 +37,9 @@ NumPartEditorW::NumPartEditorW(int type, QWidget *parent) :
|
||||
{
|
||||
ui -> setupUi(this);
|
||||
setVisibleItems();
|
||||
//The mask is a run of zeros and nothing else, so it cannot be typed
|
||||
//into a state the renderer would have to reject.
|
||||
ui -> format_le -> setValidator(new QRegularExpressionValidator(QRegularExpression("0*"), this));
|
||||
setType(NumPartEditorW::unit, true);
|
||||
}
|
||||
|
||||
@@ -99,6 +104,7 @@ NumPartEditorW::NumPartEditorW (NumerotationContext &context,
|
||||
ui -> increase_spinBox -> setValue(strl.at(2).toInt());
|
||||
if (strl.at(0)=="wrap" && strl.size() > 4)
|
||||
ui -> modulus_spinBox -> setValue(strl.at(4).toInt());
|
||||
ui -> format_le -> setText(NumerotationContext::formatOf(strl));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,23 +225,30 @@ NumerotationContext NumPartEditorW::toNumContext()
|
||||
type_str = "alpha";
|
||||
break;
|
||||
}
|
||||
const QString number_format = ui -> format_le -> text();
|
||||
if (type_str == "unitfolio"
|
||||
|| type_str == "tenfolio"
|
||||
|| type_str == "hundredfolio")
|
||||
nc.addValue(type_str,
|
||||
ui -> value_field -> displayText(),
|
||||
ui -> increase_spinBox -> value(),
|
||||
ui->value_field->displayText().toInt());
|
||||
ui->value_field->displayText().toInt(),
|
||||
0,
|
||||
number_format);
|
||||
else if (type_str == "wrap")
|
||||
nc.addValue(type_str,
|
||||
ui -> value_field -> displayText(),
|
||||
ui -> increase_spinBox -> value(),
|
||||
0,
|
||||
ui -> modulus_spinBox -> value());
|
||||
ui -> modulus_spinBox -> value(),
|
||||
number_format);
|
||||
else
|
||||
nc.addValue(type_str,
|
||||
ui -> value_field -> displayText(),
|
||||
ui -> increase_spinBox -> value());
|
||||
ui -> increase_spinBox -> value(),
|
||||
0,
|
||||
0,
|
||||
number_format);
|
||||
return nc;
|
||||
}
|
||||
|
||||
@@ -317,6 +330,14 @@ void NumPartEditorW::on_increase_spinBox_valueChanged(int) {
|
||||
@brief NumPartEditorW::on_modulus_spinBox_valueChanged
|
||||
emit changed when modulus_spinBox value changed
|
||||
*/
|
||||
/**
|
||||
@brief NumPartEditorW::on_format_le_textEdited
|
||||
emit changed when the display format is edited
|
||||
*/
|
||||
void NumPartEditorW::on_format_le_textEdited(const QString &) {
|
||||
emit changed();
|
||||
}
|
||||
|
||||
void NumPartEditorW::on_modulus_spinBox_valueChanged(int) {
|
||||
if (!ui -> value_field -> text().isEmpty()) emit changed();
|
||||
}
|
||||
@@ -359,8 +380,6 @@ void NumPartEditorW::setType(NumPartEditorW::type t, bool fnum) {
|
||||
ui -> value_field -> setValidator(intValidator);
|
||||
ui -> increase_spinBox -> setEnabled(true);
|
||||
ui -> increase_spinBox -> setValue(1);
|
||||
if (t == wrap)
|
||||
ui -> modulus_spinBox -> setValue(8);
|
||||
}
|
||||
//@t isn't a numeric type
|
||||
else if (t == string
|
||||
@@ -414,7 +433,24 @@ void NumPartEditorW::setType(NumPartEditorW::type t, bool fnum) {
|
||||
ui -> increase_spinBox -> setDisabled(true);
|
||||
}
|
||||
}
|
||||
//A modulus of 0 means "no cycle", which makes a Cyclique part behave
|
||||
//exactly like a plain digit. Defaulting it used to live in the numeric
|
||||
//behavior block above, which is skipped when the previous type was
|
||||
//itself numeric -- so the ordinary path of turning the default
|
||||
//"Chiffre 1" into a "Cyclique (modulo)" left the modulus at 0 and
|
||||
//produced a wrap part that never wrapped. Kept out of that block so it
|
||||
//applies whatever the part was before, and only when the current value
|
||||
//is unusable, so a modulus the user chose on purpose is not clobbered.
|
||||
if (t == wrap && ui -> modulus_spinBox -> value() <= 0)
|
||||
ui -> modulus_spinBox -> setValue(8);
|
||||
ui -> modulus_spinBox -> setEnabled(t == wrap);
|
||||
//A padding mask only means anything for a part rendered as a number.
|
||||
const bool numeric = (t == unit || t == unitfolio || t == ten
|
||||
|| t == tenfolio || t == hundred || t == hundredfolio
|
||||
|| t == wrap);
|
||||
ui -> format_le -> setEnabled(numeric);
|
||||
if (!numeric)
|
||||
ui -> format_le -> clear();
|
||||
type_= t;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ class NumPartEditorW : public QWidget
|
||||
void on_value_field_textEdited();
|
||||
void on_increase_spinBox_valueChanged(int);
|
||||
void on_modulus_spinBox_valueChanged(int);
|
||||
void on_format_le_textEdited(const QString &);
|
||||
void setType (NumPartEditorW::type t, bool=false);
|
||||
|
||||
signals:
|
||||
|
||||
@@ -126,6 +126,28 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="format_le">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>70</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Format d'affichage : une suite de zéros donne le nombre minimum de chiffres (00 = 07, 000 = 007). Vide = largeur naturelle du type.</string>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>0</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "qet.h"
|
||||
#include "qetdiagrameditor.h"
|
||||
#include "ui/potentialselectordialog.h"
|
||||
#include "undocommand/setautonumcontextcommand.h"
|
||||
|
||||
/**
|
||||
@brief ConductorAutoNumerotation::ConductorAutoNumerotation
|
||||
@@ -156,7 +157,16 @@ void ConductorAutoNumerotation::newProperties(
|
||||
autonum::setSequential(formula, seq, context, diagram, autoNum_name);
|
||||
|
||||
NumerotationContextCommands ncc (context, diagram);
|
||||
diagram->project()->addConductorAutoNum(autoNum_name, ncc.next());
|
||||
NumerotationContext new_context = ncc.next();
|
||||
|
||||
QETProject *project = diagram->project();
|
||||
auto *undo = new SetAutoNumContextCommand(
|
||||
[project](const QString &k, const NumerotationContext &c) {project->addConductorAutoNum(k, c);},
|
||||
autoNum_name,
|
||||
context,
|
||||
new_context);
|
||||
undo->setText(QObject::tr("Numéroter automatiquement un conducteur", "undo caption"));
|
||||
diagram->undoStack().push(undo);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,7 +255,21 @@ void ConductorAutoNumerotation::numerateNewConductor()
|
||||
autoNum_name);
|
||||
|
||||
NumerotationContextCommands ncc (context, m_diagram);
|
||||
m_diagram->project()->addConductorAutoNum(autoNum_name, ncc.next());
|
||||
NumerotationContext new_context = ncc.next();
|
||||
|
||||
QETProject *project = m_diagram->project();
|
||||
auto setter = [project](const QString &k, const NumerotationContext &c) {project->addConductorAutoNum(k, c);};
|
||||
|
||||
if (m_parent_undo)
|
||||
{
|
||||
new SetAutoNumContextCommand(setter, autoNum_name, context, new_context, m_parent_undo);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto *undo = new SetAutoNumContextCommand(setter, autoNum_name, context, new_context);
|
||||
undo->setText(QObject::tr("Numéroter automatiquement un conducteur", "undo caption"));
|
||||
m_diagram->undoStack().push(undo);
|
||||
}
|
||||
}
|
||||
|
||||
applyText(autonum::AssignVariables::formulaToLabel(
|
||||
|
||||
@@ -376,6 +376,7 @@ QString ElementQueryWidget::queryStr() const
|
||||
if (ui->m_plc_cb->isChecked()) {
|
||||
if (b) where +=" OR";
|
||||
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::PLC) += "'";
|
||||
b = true;
|
||||
}
|
||||
where.append(")");
|
||||
|
||||
|
||||
@@ -501,7 +501,12 @@ void DiagramEventAddElement::addElement()
|
||||
}
|
||||
m_diagram->addItem(m_element);
|
||||
|
||||
//Autonum the new element before pushing undo_object, so the counter
|
||||
//change it triggers is part of the same undo macro as the element's
|
||||
//own placement (one Ctrl+Z reverts both, instead of silently
|
||||
//leaving the counter advanced).
|
||||
element->setUpFormula(true, undo_object);
|
||||
|
||||
m_diagram -> undoStack().push(undo_object);
|
||||
element->setUpFormula();
|
||||
element->freezeNewAddedElement();
|
||||
}
|
||||
|
||||
@@ -317,6 +317,10 @@ void ElementScene::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
|
||||
*/
|
||||
void ElementScene::drawForeground(QPainter *p, const QRectF &)
|
||||
{
|
||||
if (!m_hotspot_visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
p -> save();
|
||||
|
||||
// desactive tout antialiasing, sauf pour le texte
|
||||
|
||||
@@ -91,6 +91,8 @@ class ElementScene : public QGraphicsScene
|
||||
|
||||
QPointer<CustomElementGraphicPart> m_single_selected_item;
|
||||
|
||||
bool m_hotspot_visible = true;
|
||||
|
||||
// methods
|
||||
public:
|
||||
ElementData elementData();
|
||||
@@ -133,6 +135,14 @@ class ElementScene : public QGraphicsScene
|
||||
void addItems(QVector<QGraphicsItem *> items);
|
||||
void removeItems(QVector<QGraphicsItem *> items);
|
||||
|
||||
/// Whether drawForeground() draws the red hotspot cross. On by
|
||||
/// default (the normal editing view); turned off around a
|
||||
/// render() call meant to capture the element's actual content
|
||||
/// only, e.g. exporting to SVG -- the cross is an editing aid,
|
||||
/// not part of the drawn symbol.
|
||||
void setHotspotVisible(bool visible) {m_hotspot_visible = visible;}
|
||||
bool hotspotVisible() const {return m_hotspot_visible;}
|
||||
|
||||
protected:
|
||||
void mouseMoveEvent (QGraphicsSceneMouseEvent *) override;
|
||||
void mousePressEvent (QGraphicsSceneMouseEvent *) override;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "partplctable.h"
|
||||
|
||||
#include "../../QPropertyUndoCommand/qpropertyundocommand.h"
|
||||
#include "../../qetapp.h"
|
||||
#include "../../QetGraphicsItemModeler/qetgraphicshandleritem.h"
|
||||
#include "../../QetGraphicsItemModeler/qetgraphicshandlerutility.h"
|
||||
#include "../../properties/elementdata.h"
|
||||
@@ -302,7 +303,7 @@ void PartPlcTable::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti
|
||||
|
||||
// Draw column headers
|
||||
QFont header_font = plc_data.headerFont.family().isEmpty()
|
||||
? painter->font() : plc_data.headerFont;
|
||||
? QETApp::diagramTextsFont() : plc_data.headerFont;
|
||||
header_font.setBold(true);
|
||||
painter->setFont(header_font);
|
||||
|
||||
@@ -318,7 +319,7 @@ void PartPlcTable::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti
|
||||
|
||||
// Draw IO rows
|
||||
QFont cell_font = plc_data.cellFont.family().isEmpty()
|
||||
? painter->font() : plc_data.cellFont;
|
||||
? QETApp::diagramTextsFont() : plc_data.cellFont;
|
||||
painter->setFont(cell_font);
|
||||
int start_idx = block_starts.at(block);
|
||||
int end_idx = (block + 1 < block_starts.size())
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
#include <QSignalBlocker>
|
||||
#include <QTableWidgetItem>
|
||||
#include <QHeaderView>
|
||||
#include <QScrollBar>
|
||||
#include <QWheelEvent>
|
||||
#include <QTableWidget>
|
||||
#include <QCheckBox>
|
||||
#include <QGroupBox>
|
||||
@@ -41,6 +43,8 @@
|
||||
#include <QFont>
|
||||
#include <QLineEdit>
|
||||
#include <QSplitter>
|
||||
#include <QShortcut>
|
||||
#include <QMenu>
|
||||
|
||||
/**
|
||||
@brief The EditorDelegate class
|
||||
@@ -666,10 +670,15 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
|
||||
plc_layout->addLayout(toolbar);
|
||||
|
||||
// Tables side by side: IO table (left) + Terminal table (right)
|
||||
auto *tables_splitter = new QSplitter(Qt::Horizontal, m_plc_gb);
|
||||
// Both share a single vertical scrollbar on the right
|
||||
auto *tables_container = new QWidget(m_plc_gb);
|
||||
auto *tables_layout = new QHBoxLayout(tables_container);
|
||||
tables_layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto *splitter = new QSplitter(Qt::Horizontal, tables_container);
|
||||
|
||||
// IO Table
|
||||
m_plc_table = new QTableWidget(tables_splitter);
|
||||
m_plc_table = new QTableWidget(splitter);
|
||||
m_plc_table->setColumnCount(5);
|
||||
m_plc_table->setHorizontalHeaderLabels({
|
||||
tr("Type"), tr("Adresse"), tr("Fonction"),
|
||||
@@ -686,10 +695,10 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
|
||||
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems);
|
||||
m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
m_plc_table->setMinimumHeight(200);
|
||||
tables_splitter->addWidget(m_plc_table);
|
||||
m_plc_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
|
||||
// Terminal table (per IO: Nb + T1-T4)
|
||||
m_plc_terminal_table = new QTableWidget(tables_splitter);
|
||||
m_plc_terminal_table = new QTableWidget(splitter);
|
||||
m_plc_terminal_table->setColumnCount(2);
|
||||
m_plc_terminal_table->setHorizontalHeaderLabels({
|
||||
tr("Nb."), tr("T1")
|
||||
@@ -700,13 +709,61 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
|
||||
m_plc_terminal_table->setSelectionBehavior(QAbstractItemView::SelectItems);
|
||||
m_plc_terminal_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
m_plc_terminal_table->setMinimumHeight(200);
|
||||
tables_splitter->addWidget(m_plc_terminal_table);
|
||||
m_plc_terminal_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
|
||||
tables_splitter->setStretchFactor(0, 3);
|
||||
tables_splitter->setStretchFactor(1, 1);
|
||||
tables_splitter->setSizes({500, 150});
|
||||
// Shared vertical scrollbar
|
||||
m_plc_shared_scrollbar = new QScrollBar(Qt::Vertical, tables_container);
|
||||
m_plc_shared_scrollbar->setMinimum(0);
|
||||
|
||||
plc_layout->addWidget(tables_splitter);
|
||||
splitter->addWidget(m_plc_table);
|
||||
splitter->addWidget(m_plc_terminal_table);
|
||||
splitter->setStretchFactor(0, 3);
|
||||
splitter->setStretchFactor(1, 1);
|
||||
|
||||
tables_layout->addWidget(splitter);
|
||||
tables_layout->addWidget(m_plc_shared_scrollbar);
|
||||
|
||||
// Bidirectional sync between tables and shared scrollbar
|
||||
// Use flag to prevent infinite loops
|
||||
connect(m_plc_shared_scrollbar, &QScrollBar::valueChanged,
|
||||
this, [this](int value) {
|
||||
if (m_plc_scroll_sync) return;
|
||||
m_plc_scroll_sync = true;
|
||||
m_plc_table->verticalScrollBar()->setValue(value);
|
||||
m_plc_terminal_table->verticalScrollBar()->setValue(value);
|
||||
m_plc_scroll_sync = false;
|
||||
});
|
||||
connect(m_plc_table->verticalScrollBar(), &QScrollBar::valueChanged,
|
||||
this, [this](int value) {
|
||||
if (m_plc_scroll_sync) return;
|
||||
m_plc_scroll_sync = true;
|
||||
m_plc_shared_scrollbar->setValue(value);
|
||||
m_plc_terminal_table->verticalScrollBar()->setValue(value);
|
||||
m_plc_scroll_sync = false;
|
||||
});
|
||||
connect(m_plc_terminal_table->verticalScrollBar(), &QScrollBar::valueChanged,
|
||||
this, [this](int value) {
|
||||
if (m_plc_scroll_sync) return;
|
||||
m_plc_scroll_sync = true;
|
||||
m_plc_shared_scrollbar->setValue(value);
|
||||
m_plc_table->verticalScrollBar()->setValue(value);
|
||||
m_plc_scroll_sync = false;
|
||||
});
|
||||
|
||||
// Update shared scrollbar range from both tables
|
||||
auto syncRange = [this]() {
|
||||
int max = qMax(m_plc_table->verticalScrollBar()->maximum(),
|
||||
m_plc_terminal_table->verticalScrollBar()->maximum());
|
||||
m_plc_shared_scrollbar->blockSignals(true);
|
||||
m_plc_shared_scrollbar->setMaximum(max);
|
||||
m_plc_shared_scrollbar->blockSignals(false);
|
||||
};
|
||||
connect(m_plc_table->verticalScrollBar(), &QScrollBar::rangeChanged,
|
||||
this, syncRange);
|
||||
connect(m_plc_terminal_table->verticalScrollBar(), &QScrollBar::rangeChanged,
|
||||
this, syncRange);
|
||||
|
||||
plc_layout->addWidget(tables_container);
|
||||
|
||||
// Font settings
|
||||
auto *font_layout = new QHBoxLayout();
|
||||
@@ -790,12 +847,25 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
|
||||
}
|
||||
plc_layout->addLayout(col_layout);
|
||||
|
||||
// Add to master group box
|
||||
ui->m_master_gb->layout()->addWidget(m_plc_gb);
|
||||
// Add to master group box - row 4, full width
|
||||
auto *gl = qobject_cast<QGridLayout*>(ui->m_master_gb->layout());
|
||||
gl->addWidget(m_plc_gb, 4, 0, 1, 2);
|
||||
|
||||
// Connect signals
|
||||
connect(add_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcAddRow);
|
||||
connect(remove_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcRemoveRow);
|
||||
|
||||
// Ctrl+V shortcut for paste
|
||||
auto *paste_shortcut = new QShortcut(QKeySequence::Paste, m_plc_table);
|
||||
connect(paste_shortcut, &QShortcut::activated, this, &ElementPropertiesEditorWidget::plcPasteFromClipboard);
|
||||
|
||||
// Context menu for the PLC table
|
||||
m_plc_table->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_plc_table, &QTableWidget::customContextMenuRequested, this, [this](const QPoint &pos) {
|
||||
QMenu menu;
|
||||
menu.addAction(tr("Coller depuis le presse-papiers"), this, &ElementPropertiesEditorWidget::plcPasteFromClipboard);
|
||||
menu.exec(m_plc_table->mapToGlobal(pos));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -877,12 +947,12 @@ void ElementPropertiesEditorWidget::populatePlcTable()
|
||||
m_plc_header_font = plc_data.headerFont;
|
||||
m_plc_cell_font = plc_data.cellFont;
|
||||
if (m_plc_header_font.family().isEmpty()) {
|
||||
m_plc_header_font = QFont(m_plc_table->font());
|
||||
m_plc_header_font = QETApp::diagramTextsFont();
|
||||
m_plc_header_font.setBold(true);
|
||||
m_plc_header_font.setPointSize(8);
|
||||
}
|
||||
if (m_plc_cell_font.family().isEmpty()) {
|
||||
m_plc_cell_font = QFont(m_plc_table->font());
|
||||
m_plc_cell_font = QETApp::diagramTextsFont();
|
||||
m_plc_cell_font.setPointSize(8);
|
||||
}
|
||||
m_plc_header_font_btn->setText(tr("Police des en-têtes: %1 %2pt")
|
||||
@@ -920,6 +990,21 @@ void ElementPropertiesEditorWidget::populatePlcTable()
|
||||
hdr->moveSection(hdr->visualIndex(logical), visual);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure scrollbars stay hidden and sync shared scrollbar range
|
||||
if (m_plc_table) {
|
||||
m_plc_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_plc_table->verticalScrollBar()->setVisible(false);
|
||||
}
|
||||
if (m_plc_terminal_table) {
|
||||
m_plc_terminal_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_plc_terminal_table->verticalScrollBar()->setVisible(false);
|
||||
}
|
||||
if (m_plc_shared_scrollbar && m_plc_table) {
|
||||
int max = qMax(m_plc_table->verticalScrollBar()->maximum(),
|
||||
m_plc_terminal_table->verticalScrollBar()->maximum());
|
||||
m_plc_shared_scrollbar->setMaximum(max);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1158,53 +1243,80 @@ void ElementPropertiesEditorWidget::plcPasteFromClipboard()
|
||||
return;
|
||||
|
||||
QStringList lines = clipboard_text.split('\n', Qt::SkipEmptyParts);
|
||||
if (lines.isEmpty())
|
||||
return;
|
||||
|
||||
int start_row = m_plc_table->rowCount();
|
||||
m_plc_table->setRowCount(start_row + lines.size());
|
||||
|
||||
for (int i = 0; i < lines.size(); ++i) {
|
||||
QStringList cells = lines.at(i).split('\t');
|
||||
int row = start_row + i;
|
||||
|
||||
// Type combo
|
||||
auto *type_cb = new QComboBox(m_plc_table);
|
||||
QStringList plc_types = ElementData::plcIOTypeList();
|
||||
for (int t = 0; t < plc_types.size(); ++t) {
|
||||
type_cb->addItem(plc_types.at(t), t);
|
||||
bool has_tabs = false;
|
||||
for (const QString &line : lines) {
|
||||
if (line.contains('\t')) {
|
||||
has_tabs = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to match type from clipboard
|
||||
if (!cells.isEmpty()) {
|
||||
QString type_str = cells.at(0).trimmed();
|
||||
int type_idx = -1;
|
||||
if (!has_tabs) {
|
||||
// Vertical paste: values go down the same column
|
||||
int target_col = m_plc_table->currentColumn();
|
||||
if (target_col < 0) target_col = 0;
|
||||
int target_row = m_plc_table->currentRow();
|
||||
if (target_row < 0) target_row = 0;
|
||||
int max_rows = m_plc_table->rowCount();
|
||||
|
||||
for (int i = 0; i < lines.size(); ++i) {
|
||||
int row = target_row + i;
|
||||
if (row >= max_rows) break;
|
||||
plcSetCellFromValue(row, target_col, lines.at(i).trimmed());
|
||||
}
|
||||
} else {
|
||||
// Horizontal paste: each line is a separate IO row
|
||||
int target_row = m_plc_table->currentRow();
|
||||
if (target_row < 0) target_row = 0;
|
||||
int max_rows = m_plc_table->rowCount();
|
||||
|
||||
for (int i = 0; i < lines.size(); ++i) {
|
||||
int row = target_row + i;
|
||||
if (row >= max_rows) break;
|
||||
QStringList cells = lines.at(i).split('\t');
|
||||
for (int c = 0; c < cells.size(); ++c) {
|
||||
if (c > 4) break;
|
||||
plcSetCellFromValue(row, c, cells.at(c).trimmed());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ElementPropertiesEditorWidget::plcSetCellFromValue
|
||||
* Set a single table cell value, respecting the column widget type.
|
||||
*/
|
||||
void ElementPropertiesEditorWidget::plcSetCellFromValue(int row, int col, const QString &val)
|
||||
{
|
||||
if (!m_plc_table || row < 0 || col < 0 || col > 4)
|
||||
return;
|
||||
|
||||
if (col == 0) {
|
||||
auto *type_cb = qobject_cast<QComboBox*>(m_plc_table->cellWidget(row, col));
|
||||
if (!type_cb)
|
||||
return;
|
||||
if (!val.isEmpty()) {
|
||||
QStringList plc_types = ElementData::plcIOTypeList();
|
||||
for (int t = 0; t < plc_types.size(); ++t) {
|
||||
if (plc_types.at(t).compare(type_str, Qt::CaseInsensitive) == 0) {
|
||||
type_idx = t;
|
||||
break;
|
||||
if (plc_types.at(t).compare(val, Qt::CaseInsensitive) == 0) {
|
||||
type_cb->setCurrentIndex(t);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (type_idx >= 0)
|
||||
type_cb->setCurrentIndex(type_idx);
|
||||
}
|
||||
m_plc_table->setCellWidget(row, 0, type_cb);
|
||||
|
||||
// Address
|
||||
m_plc_table->setItem(row, 1, new QTableWidgetItem(
|
||||
cells.size() > 1 ? cells.at(1).trimmed() : QString()));
|
||||
|
||||
// Function text
|
||||
m_plc_table->setItem(row, 2, new QTableWidgetItem(
|
||||
cells.size() > 2 ? cells.at(2).trimmed() : QString()));
|
||||
|
||||
// Comment
|
||||
m_plc_table->setItem(row, 3, new QTableWidgetItem(
|
||||
cells.size() > 3 ? cells.at(3).trimmed() : QString()));
|
||||
|
||||
// CrossRef (read-only)
|
||||
auto *crossref_item = new QTableWidgetItem(
|
||||
cells.size() > 4 ? cells.at(4).trimmed() : QString());
|
||||
crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable);
|
||||
m_plc_table->setItem(row, 4, crossref_item);
|
||||
}
|
||||
else if (col == 4) {
|
||||
// CrossRef - read-only
|
||||
auto *item = new QTableWidgetItem(val);
|
||||
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
|
||||
m_plc_table->setItem(row, col, item);
|
||||
}
|
||||
else {
|
||||
// Text columns: Address, Function, Comment
|
||||
m_plc_table->setItem(row, col, new QTableWidgetItem(val));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ class QCheckBox;
|
||||
class QGroupBox;
|
||||
class QPushButton;
|
||||
class QLineEdit;
|
||||
class QScrollBar;
|
||||
|
||||
namespace Ui {
|
||||
class ElementPropertiesEditorWidget;
|
||||
@@ -74,6 +75,7 @@ class ElementPropertiesEditorWidget : public QDialog
|
||||
void plcTerminalCountChanged(int row, int count);
|
||||
void plcSelectHeaderFont();
|
||||
void plcSelectCellFont();
|
||||
void plcSetCellFromValue(int row, int col, const QString &val);
|
||||
|
||||
//ATTRIBUTES
|
||||
private:
|
||||
@@ -92,9 +94,11 @@ class ElementPropertiesEditorWidget : public QDialog
|
||||
QCheckBox *m_plc_show_headers_cb = nullptr;
|
||||
QFont m_plc_header_font;
|
||||
QFont m_plc_cell_font;
|
||||
QList<QCheckBox *> m_plc_col_visibility_checkboxes;
|
||||
QList<QSpinBox *> m_plc_col_width_spinboxes;
|
||||
QList<QLineEdit *> m_plc_col_name_edits;
|
||||
QList<QCheckBox *> m_plc_col_visibility_checkboxes;
|
||||
QList<QSpinBox *> m_plc_col_width_spinboxes;
|
||||
QList<QLineEdit *> m_plc_col_name_edits;
|
||||
QScrollBar *m_plc_shared_scrollbar = nullptr;
|
||||
bool m_plc_scroll_sync = false;
|
||||
};
|
||||
|
||||
#endif // ELEMENTPROPERTIESEDITORWIDGET_H
|
||||
|
||||
@@ -39,7 +39,20 @@
|
||||
<item>
|
||||
<widget class="QComboBox" name="m_base_type_cb"/>
|
||||
</item>
|
||||
</layout>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="m_slave_gb">
|
||||
@@ -93,17 +106,34 @@
|
||||
<property name="title">
|
||||
<string>Élément maître</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Type concret</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="m_master_type_cb"/>
|
||||
</item>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="type_concret_layout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Type concret</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="type_concret_spacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="m_master_type_cb"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="max_slaves_checkbox">
|
||||
<property name="text">
|
||||
@@ -202,20 +232,7 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="Informations">
|
||||
<attribute name="title">
|
||||
|
||||
@@ -53,6 +53,8 @@
|
||||
|
||||
#include <QSettings>
|
||||
#include <QActionGroup>
|
||||
#include <QFileDialog>
|
||||
#include <QSvgGenerator>
|
||||
|
||||
/**
|
||||
* @brief QETElementEditor::QETElementEditor
|
||||
@@ -1377,6 +1379,94 @@ bool QETElementEditor::on_m_save_as_file_action_triggered()
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETElementEditor::on_m_export_svg_action_triggered
|
||||
Export the element currently open in this editor to a standalone SVG
|
||||
file.
|
||||
|
||||
Renders the live ElementScene directly, the same way
|
||||
ExportDialog::generateSvg() renders the live Diagram for the diagram
|
||||
editor's own SVG export -- not ElementPictureFactory's cached picture.
|
||||
That cache is keyed by the element's saved-to-disk uuid and is never
|
||||
invalidated on edit (nothing in this editor ever tells it to), so it
|
||||
would silently export stale content for any element already previewed
|
||||
once in the elements panel, and nothing at all for one that has never
|
||||
been saved. Rendering the scene directly has neither problem and
|
||||
always reflects exactly what is currently on screen, saved or not.
|
||||
@return true if the file was written
|
||||
*/
|
||||
bool QETElementEditor::on_m_export_svg_action_triggered()
|
||||
{
|
||||
//Suggest the element's own filename (without its .elmt extension) as
|
||||
//the default export name, per plc-user's review on #637 -- the
|
||||
//directory-only default below is unchanged for an element that has
|
||||
//never been saved, since there is no filename to derive one from.
|
||||
QString suggested_path = QETApp::customElementsDir();
|
||||
if (!m_file_name.isEmpty()) {
|
||||
QFileInfo file_info(m_file_name);
|
||||
suggested_path = QDir(file_info.absolutePath()).filePath(file_info.completeBaseName());
|
||||
}
|
||||
|
||||
QString fn = QFileDialog::getSaveFileName(
|
||||
this,
|
||||
tr("Exporter en SVG", "dialog title"),
|
||||
suggested_path,
|
||||
tr("Image SVG (*.svg)", "filetypes allowed when exporting an element to SVG"));
|
||||
|
||||
if (fn.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (!fn.endsWith(".svg", Qt::CaseInsensitive)) {
|
||||
fn += ".svg";
|
||||
}
|
||||
|
||||
QFile file(fn);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
QMessageBox::critical(this, tr("Échec de l'export"),
|
||||
tr("Impossible d'écrire dans le fichier « %1 ».").arg(fn));
|
||||
return false;
|
||||
}
|
||||
|
||||
//Margin-less bounding rect of the element's own drawn content
|
||||
//(lines, rects, terminals, text...), excluding the origin cross
|
||||
//and other editor-only decoration -- see
|
||||
//ElementScene::elementSceneGeometricRect()'s own doc comment.
|
||||
//Falls back to itemsBoundingRect() for the rare element made up
|
||||
//only of item types that helper deliberately excludes.
|
||||
QRectF source_rect = m_elmt_scene->elementSceneGeometricRect();
|
||||
if (source_rect.isEmpty()) {
|
||||
source_rect = m_elmt_scene->itemsBoundingRect();
|
||||
}
|
||||
constexpr qreal margin = 5.0;
|
||||
source_rect.adjust(-margin, -margin, margin, margin);
|
||||
|
||||
QSize target_size = source_rect.size().toSize();
|
||||
if (target_size.isEmpty()) {
|
||||
target_size = QSize(1, 1);
|
||||
}
|
||||
|
||||
QSvgGenerator svg_engine;
|
||||
svg_engine.setSize(target_size);
|
||||
svg_engine.setViewBox(QRect(QPoint(0, 0), target_size));
|
||||
svg_engine.setOutputDevice(&file);
|
||||
|
||||
QPainter svg_painter(&svg_engine);
|
||||
svg_painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
svg_painter.setRenderHint(QPainter::TextAntialiasing, true);
|
||||
|
||||
//The hotspot cross is ElementScene::drawForeground()'s editing aid,
|
||||
//drawn unconditionally on every render() call including this one
|
||||
//unless told not to -- it is not part of the element being
|
||||
//exported.
|
||||
m_elmt_scene->setHotspotVisible(false);
|
||||
m_elmt_scene->render(&svg_painter, QRectF(QPointF(0, 0), target_size), source_rect);
|
||||
m_elmt_scene->setHotspotVisible(true);
|
||||
|
||||
svg_painter.end();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void QETElementEditor::on_m_reload_action_triggered()
|
||||
{
|
||||
//If user already edit the element, ask confirmation to reload
|
||||
|
||||
@@ -87,6 +87,7 @@ class QETElementEditor : public QMainWindow
|
||||
void on_m_open_action_triggered();
|
||||
void on_m_open_from_file_action_triggered();
|
||||
bool on_m_save_as_file_action_triggered();
|
||||
bool on_m_export_svg_action_triggered();
|
||||
void on_m_reload_action_triggered();
|
||||
void on_m_quit_action_triggered();
|
||||
void on_m_deselect_all_action_triggered();
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
<addaction name="m_save_action"/>
|
||||
<addaction name="m_save_as_action"/>
|
||||
<addaction name="m_save_as_file_action"/>
|
||||
<addaction name="m_export_svg_action"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="m_reload_action"/>
|
||||
<addaction name="separator"/>
|
||||
@@ -262,6 +263,15 @@
|
||||
<string>Enregistrer dans un fichier</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="m_export_svg_action">
|
||||
<property name="icon">
|
||||
<iconset resource="../../../qelectrotech.qrc">
|
||||
<normaloff>:/ico/22x22/document-export.png</normaloff>:/ico/22x22/document-export.png</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Exporter en SVG</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="m_reload_action">
|
||||
<property name="icon">
|
||||
<iconset resource="../../../qelectrotech.qrc">
|
||||
|
||||
@@ -61,6 +61,8 @@ ElementsPanelWidget::ElementsPanelWidget(QWidget *parent) : QWidget(parent) {
|
||||
prj_edit_prop = new QAction(QET::Icons::DialogInformation, tr("Propriétés du projet"), this);
|
||||
prj_prop_diagram = new QAction(QET::Icons::DialogInformation, tr("Propriétés du folio"), this);
|
||||
prj_add_diagram = new QAction(QET::Icons::DiagramAdd, tr("Ajouter un folio"), this);
|
||||
prj_insert_diagram_above = new QAction(QET::Icons::DiagramAdd, tr("Insérer un folio au-dessus"), this);
|
||||
prj_insert_diagram_below = new QAction(QET::Icons::DiagramAdd, tr("Insérer un folio en dessous"), this);
|
||||
prj_duplicate_diagram = new QAction(QET::Icons::IC_CopyFile, tr("Copier et coller"), this);
|
||||
prj_del_diagram = new QAction(QET::Icons::DiagramDelete, tr("Supprimer ce folio"), this);
|
||||
prj_move_diagram_up = new QAction(QET::Icons::GoUp, tr("Remonter ce folio"), this);
|
||||
@@ -101,6 +103,8 @@ ElementsPanelWidget::ElementsPanelWidget(QWidget *parent) : QWidget(parent) {
|
||||
connect(prj_edit_prop, SIGNAL(triggered()), this, SLOT(editProjectProperties()));
|
||||
connect(prj_prop_diagram, SIGNAL(triggered()), this, SLOT(editDiagramProperties()));
|
||||
connect(prj_add_diagram, SIGNAL(triggered()), this, SLOT(newDiagram()));
|
||||
connect(prj_insert_diagram_above, SIGNAL(triggered()), this, SLOT(insertDiagramAbove()));
|
||||
connect(prj_insert_diagram_below, SIGNAL(triggered()), this, SLOT(insertDiagramBelow()));
|
||||
connect(prj_del_diagram, SIGNAL(triggered()), this, SLOT(deleteDiagram()));
|
||||
connect(prj_duplicate_diagram, SIGNAL(triggered()), this, SLOT(duplicateDiagram()));
|
||||
connect(prj_move_diagram_up, SIGNAL(triggered()), this, SLOT(moveDiagramUp()));
|
||||
@@ -245,6 +249,32 @@ void ElementsPanelWidget::newDiagram()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementsPanelWidget::insertDiagramAbove
|
||||
Emit requestForNewDiagramAt with the position of the currently
|
||||
selected diagram, inserting the new folio right before it.
|
||||
*/
|
||||
void ElementsPanelWidget::insertDiagramAbove()
|
||||
{
|
||||
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
|
||||
QETProject *project = selected_diagram->project();
|
||||
emit(requestForNewDiagramAt(project, project->folioIndex(selected_diagram)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementsPanelWidget::insertDiagramBelow
|
||||
Emit requestForNewDiagramAt with the position right after the
|
||||
currently selected diagram, inserting the new folio right after it.
|
||||
*/
|
||||
void ElementsPanelWidget::insertDiagramBelow()
|
||||
{
|
||||
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
|
||||
QETProject *project = selected_diagram->project();
|
||||
emit(requestForNewDiagramAt(project, project->folioIndex(selected_diagram) + 1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emet le signal requestForDiagramsDeletion avec les schemas selectionnes
|
||||
*/
|
||||
@@ -451,6 +481,8 @@ void ElementsPanelWidget::updateButtons()
|
||||
|
||||
prj_del_diagram -> setEnabled(is_writable);
|
||||
prj_duplicate_diagram -> setEnabled(is_writable);
|
||||
prj_insert_diagram_above -> setEnabled(is_writable);
|
||||
prj_insert_diagram_below -> setEnabled(is_writable);
|
||||
prj_move_diagram_up -> setEnabled(is_writable && min_position > 0);
|
||||
prj_move_diagram_down -> setEnabled(is_writable && max_position < project_diagrams_count - 1);
|
||||
prj_move_diagram_top -> setEnabled(is_writable && min_position > 0);
|
||||
@@ -504,6 +536,8 @@ void ElementsPanelWidget::handleContextMenu(const QPoint &pos) {
|
||||
break;
|
||||
case QET::Diagram:
|
||||
context_menu -> addAction(prj_prop_diagram);
|
||||
context_menu -> addAction(prj_insert_diagram_above);
|
||||
context_menu -> addAction(prj_insert_diagram_below);
|
||||
context_menu -> addAction(prj_del_diagram);
|
||||
context_menu -> addAction(prj_duplicate_diagram);
|
||||
context_menu -> addAction(prj_move_diagram_top);
|
||||
|
||||
@@ -46,6 +46,8 @@ class ElementsPanelWidget : public QWidget {
|
||||
*prj_edit_prop,
|
||||
*prj_prop_diagram,
|
||||
*prj_add_diagram,
|
||||
*prj_insert_diagram_above,
|
||||
*prj_insert_diagram_below,
|
||||
*prj_del_diagram,
|
||||
*prj_duplicate_diagram,
|
||||
*prj_move_diagram_up,
|
||||
@@ -66,6 +68,7 @@ class ElementsPanelWidget : public QWidget {
|
||||
signals:
|
||||
void requestForProject(QETProject *);
|
||||
void requestForNewDiagram(QETProject *);
|
||||
void requestForNewDiagramAt(QETProject *, int);
|
||||
void requestForProjectClosing(QETProject *);
|
||||
void requestForProjectPropertiesEdition(QETProject *);
|
||||
void requestForDiagramPropertiesEdition(Diagram *);
|
||||
@@ -88,6 +91,8 @@ class ElementsPanelWidget : public QWidget {
|
||||
void editProjectProperties();
|
||||
void editDiagramProperties();
|
||||
void newDiagram();
|
||||
void insertDiagramAbove();
|
||||
void insertDiagramBelow();
|
||||
void deleteDiagram();
|
||||
void duplicateDiagram();
|
||||
void moveDiagramUp();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)";
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -16,6 +16,8 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "cli_export.h"
|
||||
#include "logging/eventloopwatchdog.h"
|
||||
#include "logging/qetlogger.h"
|
||||
#include "machine_info.h"
|
||||
#include "qet.h"
|
||||
#include "qetapp.h"
|
||||
@@ -62,131 +64,16 @@ class EarlyFileOpenCatcher : public QObject
|
||||
#endif
|
||||
|
||||
/**
|
||||
@brief myMessageOutput
|
||||
for debugging
|
||||
@param type : the messages that can be sent to a message handler
|
||||
@param context : were? wat?
|
||||
@param msg : Message
|
||||
@brief qetLogMessageHandler
|
||||
Installed via qInstallMessageHandler(); forwards to QetLogger, which
|
||||
holds all the actual formatting/ring/rotation state. See
|
||||
logging/qetlogger.h for the rationale (discussion #644).
|
||||
*/
|
||||
void myMessageOutput(QtMsgType type,
|
||||
void qetLogMessageHandler(QtMsgType type,
|
||||
const QMessageLogContext &context,
|
||||
const QString &msg)
|
||||
{
|
||||
|
||||
QString txt=QTime::currentTime().toString("hh:mm:ss.zzz");
|
||||
QByteArray dbs =txt.toLocal8Bit();
|
||||
QByteArray localMsg = msg.toLocal8Bit();
|
||||
const char *file = context.file ? context.file : "";
|
||||
const char *function = context.function ? context.function : "";
|
||||
|
||||
switch (type) {
|
||||
case QtDebugMsg:
|
||||
fprintf(stderr,
|
||||
"%s Debug: %s (%s:%u, %s)\n",
|
||||
dbs.constData(),
|
||||
localMsg.constData(),
|
||||
file,
|
||||
context.line,
|
||||
function);
|
||||
txt+=" Debug: ";
|
||||
break;
|
||||
case QtInfoMsg:
|
||||
fprintf(stderr,
|
||||
"%s Info: %s \n",
|
||||
dbs.constData(),
|
||||
localMsg.constData());
|
||||
txt+=" Info: ";
|
||||
break;
|
||||
case QtWarningMsg:
|
||||
fprintf(stderr,
|
||||
"%s Warning: %s (%s:%u, %s)\n",
|
||||
dbs.constData(),
|
||||
localMsg.constData(),
|
||||
file, context.line,
|
||||
function);
|
||||
txt+=" Warning: ";
|
||||
break;
|
||||
case QtCriticalMsg:
|
||||
fprintf(stderr,
|
||||
"%s Critical: %s (%s:%u, %s)\n",
|
||||
dbs.constData(),
|
||||
localMsg.constData(),
|
||||
file,
|
||||
context.line,
|
||||
function);
|
||||
txt+=" Critical: ";
|
||||
break;
|
||||
case QtFatalMsg:
|
||||
fprintf(stderr,
|
||||
"%s Fatal: %s (%s:%u, %s)\n",
|
||||
dbs.constData(),
|
||||
localMsg.constData(),
|
||||
file,
|
||||
context.line,
|
||||
function);
|
||||
txt+=" Fatal: ";
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr,
|
||||
"%s Unknown: %s (%s:%u, %s)\n",
|
||||
dbs.constData(),
|
||||
localMsg.constData(),
|
||||
file,
|
||||
context.line,
|
||||
function);
|
||||
txt+=" Unknown: ";
|
||||
}
|
||||
txt+= msg;
|
||||
if(type==QtInfoMsg){
|
||||
txt+=" \n";
|
||||
} else {
|
||||
txt+= " (";
|
||||
txt+= context.file ? context.file : "";
|
||||
txt+= ":";
|
||||
txt+=QString::number(context.line ? context.line :0);
|
||||
txt+= ", ";
|
||||
txt+= context.function ? context.function : "";
|
||||
txt+=")\n";
|
||||
}
|
||||
QFile outFile(QETApp::dataDir()
|
||||
+"/"
|
||||
+QDate::currentDate().toString("yyyyMMdd")
|
||||
+".log");
|
||||
if(outFile.open(QIODevice::WriteOnly | QIODevice::Append))
|
||||
{
|
||||
QTextStream ts(&outFile);
|
||||
ts << txt;
|
||||
}
|
||||
outFile.close();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief delete_old_log_files
|
||||
delete old log files
|
||||
@param days : max days old
|
||||
*/
|
||||
void delete_old_log_files(int days)
|
||||
{
|
||||
const QDate today = QDate::currentDate();
|
||||
const QString path = QETApp::dataDir() % "/";
|
||||
|
||||
QString filter("%1%1%1%1%1%1%1%1.log"); // pattern
|
||||
filter = filter.arg("[0123456789]"); // valid characters
|
||||
|
||||
Q_FOREACH (auto fileInfo,
|
||||
QDir(path).entryInfoList(
|
||||
QStringList(filter),
|
||||
QDir::Files))
|
||||
{
|
||||
if (fileInfo.lastRead().date().daysTo(today) > days)
|
||||
{
|
||||
QString filepath = fileInfo.absoluteFilePath();
|
||||
QDir deletefile;
|
||||
deletefile.setPath(filepath);
|
||||
deletefile.remove(filepath);
|
||||
qDebug() << "File " % filepath % " is deleted!";
|
||||
}
|
||||
}
|
||||
QetLogger::instance().handleMessage(type, context, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,13 +140,25 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the logger's state (log directory, session filename, open
|
||||
// file handle) explicitly here, immediately before installing the
|
||||
// handler -- not implicitly on whichever thread happens to log
|
||||
// first. See QetLogger::init().
|
||||
//
|
||||
// Install the log-file message handler BEFORE the application starts:
|
||||
// QETApp's constructor does the whole startup (collections, editor,
|
||||
// opening the projects given on the command line), so installing the
|
||||
// handler afterwards - as was done in the startup worker below - meant
|
||||
// exactly the interesting lines (collection and project load timers)
|
||||
// went to stderr, which is invisible in a Windows GUI session.
|
||||
qInstallMessageHandler(myMessageOutput);
|
||||
QetLogger::instance().init();
|
||||
qInstallMessageHandler(qetLogMessageHandler);
|
||||
// Step 4 (discussion #644): flush the ring to a crash-dump file if
|
||||
// the process dies from here on. Installed right after the ring
|
||||
// exists (init() just constructed it) and as early as reasonably
|
||||
// possible, so it also covers whatever runs between here and
|
||||
// QETApp's own construction below.
|
||||
QetLogger::instance().installCrashHandler();
|
||||
|
||||
SingleApplication app(argc, argv, true);
|
||||
#ifdef Q_OS_MACOS
|
||||
@@ -308,9 +207,16 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
|
||||
{
|
||||
qInfo("Start-up");
|
||||
// delete old log files of max 7 days old.
|
||||
delete_old_log_files(7);
|
||||
QetLogger::instance().pruneOldLogFiles(7);
|
||||
MachineInfo::instance()->send_info_to_debug();
|
||||
});
|
||||
|
||||
// Constructed here rather than earlier: start() measures ticks against
|
||||
// the event loop app.exec() is about to run, so there is no point
|
||||
// (and no accurate baseline) before this line.
|
||||
EventLoopWatchdog watchdog;
|
||||
watchdog.start();
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
|
||||
|
||||
@@ -842,7 +842,7 @@ void ProjectView::initWidgets()
|
||||
QHBoxLayout *TopRightCorner_Layout = new QHBoxLayout();
|
||||
TopRightCorner_Layout->setContentsMargins(0,0,0,0);
|
||||
// some place left to the 'next_right_view_button' button
|
||||
TopRightCorner_Layout->insertSpacing(1,10);
|
||||
TopRightCorner_Layout->addSpacing(10);
|
||||
|
||||
QHBoxLayout *TopLeftCorner_Layout = new QHBoxLayout();
|
||||
TopLeftCorner_Layout->setContentsMargins(0,0,0,0);
|
||||
|
||||
@@ -267,6 +267,211 @@ QDomElement ElementData::kindInfoToXml(QDomDocument &document)
|
||||
return returned_elmt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ElementData::plcMasterDataToXml
|
||||
* Serialize PLC master data to XML (used by Element::toXml for diagram instances)
|
||||
*/
|
||||
QDomElement ElementData::plcMasterDataToXml(QDomDocument &document) const
|
||||
{
|
||||
auto xml_plc = document.createElement(QStringLiteral("plcMasterData"));
|
||||
xml_plc.setAttribute(QStringLiteral("rowHeight"),
|
||||
QString::number(m_plc_master_data.rowHeight, 'f', 2));
|
||||
|
||||
// Save break positions
|
||||
{
|
||||
auto xml_breaks = document.createElement(QStringLiteral("breakPositions"));
|
||||
for (int bp : m_plc_master_data.breakPositions) {
|
||||
auto xml_bp = document.createElement(QStringLiteral("break"));
|
||||
xml_bp.appendChild(document.createTextNode(QString::number(bp)));
|
||||
xml_breaks.appendChild(xml_bp);
|
||||
}
|
||||
xml_plc.appendChild(xml_breaks);
|
||||
}
|
||||
|
||||
// Save column widths
|
||||
auto xml_col_widths = document.createElement(QStringLiteral("columnWidths"));
|
||||
for (auto it = m_plc_master_data.colWidths.constBegin();
|
||||
it != m_plc_master_data.colWidths.constEnd(); ++it) {
|
||||
auto xml_col = document.createElement(QStringLiteral("column"));
|
||||
xml_col.setAttribute(QStringLiteral("index"), it.key());
|
||||
xml_col.setAttribute(QStringLiteral("width"), QString::number(it.value(), 'f', 2));
|
||||
xml_col_widths.appendChild(xml_col);
|
||||
}
|
||||
xml_plc.appendChild(xml_col_widths);
|
||||
|
||||
// Save column visibility
|
||||
auto xml_col_vis = document.createElement(QStringLiteral("columnVisibility"));
|
||||
for (auto it = m_plc_master_data.colVisible.constBegin();
|
||||
it != m_plc_master_data.colVisible.constEnd(); ++it) {
|
||||
auto xml_col = document.createElement(QStringLiteral("column"));
|
||||
xml_col.setAttribute(QStringLiteral("index"), it.key());
|
||||
xml_col.setAttribute(QStringLiteral("visible"), it.value() ? "true" : "false");
|
||||
xml_col_vis.appendChild(xml_col);
|
||||
}
|
||||
xml_plc.appendChild(xml_col_vis);
|
||||
|
||||
// Save fonts
|
||||
if (!m_plc_master_data.headerFont.family().isEmpty()) {
|
||||
auto xml_hfont = document.createElement(QStringLiteral("headerFont"));
|
||||
xml_hfont.setAttribute(QStringLiteral("family"), m_plc_master_data.headerFont.family());
|
||||
xml_hfont.setAttribute(QStringLiteral("size"), m_plc_master_data.headerFont.pointSize());
|
||||
xml_hfont.setAttribute(QStringLiteral("bold"), m_plc_master_data.headerFont.bold() ? "true" : "false");
|
||||
xml_plc.appendChild(xml_hfont);
|
||||
}
|
||||
if (!m_plc_master_data.cellFont.family().isEmpty()) {
|
||||
auto xml_cfont = document.createElement(QStringLiteral("cellFont"));
|
||||
xml_cfont.setAttribute(QStringLiteral("family"), m_plc_master_data.cellFont.family());
|
||||
xml_cfont.setAttribute(QStringLiteral("size"), m_plc_master_data.cellFont.pointSize());
|
||||
xml_cfont.setAttribute(QStringLiteral("bold"), m_plc_master_data.cellFont.bold() ? "true" : "false");
|
||||
xml_plc.appendChild(xml_cfont);
|
||||
}
|
||||
|
||||
// Save custom column names
|
||||
if (!m_plc_master_data.columnNames.isEmpty()) {
|
||||
auto xml_names = document.createElement(QStringLiteral("columnNames"));
|
||||
for (int i = 0; i < m_plc_master_data.columnNames.size(); ++i) {
|
||||
auto xml_name = document.createElement(QStringLiteral("column"));
|
||||
xml_name.setAttribute(QStringLiteral("index"), i);
|
||||
xml_name.appendChild(document.createTextNode(m_plc_master_data.columnNames.at(i)));
|
||||
xml_names.appendChild(xml_name);
|
||||
}
|
||||
xml_plc.appendChild(xml_names);
|
||||
}
|
||||
|
||||
// Save column order
|
||||
if (!m_plc_master_data.columnOrder.isEmpty()) {
|
||||
auto xml_order = document.createElement(QStringLiteral("columnOrder"));
|
||||
QString order_str;
|
||||
for (int i = 0; i < m_plc_master_data.columnOrder.size(); ++i) {
|
||||
if (i > 0) order_str += QStringLiteral(",");
|
||||
order_str += QString::number(m_plc_master_data.columnOrder.at(i));
|
||||
}
|
||||
xml_order.appendChild(document.createTextNode(order_str));
|
||||
xml_plc.appendChild(xml_order);
|
||||
}
|
||||
|
||||
// Save showHeaders
|
||||
{
|
||||
auto xml_sh = document.createElement(QStringLiteral("showHeaders"));
|
||||
xml_sh.appendChild(document.createTextNode(
|
||||
m_plc_master_data.showHeaders ? QStringLiteral("1") : QStringLiteral("0")));
|
||||
xml_plc.appendChild(xml_sh);
|
||||
}
|
||||
|
||||
// Save IO entries
|
||||
auto xml_ios = document.createElement(QStringLiteral("plcIOs"));
|
||||
for (const auto &io : m_plc_master_data.ios) {
|
||||
auto xml_io = document.createElement(QStringLiteral("plcIO"));
|
||||
xml_io.setAttribute(QStringLiteral("type"), plcIOTypeToString(io.type));
|
||||
xml_io.setAttribute(QStringLiteral("address"), io.address);
|
||||
xml_io.setAttribute(QStringLiteral("functionText"), io.functionText);
|
||||
xml_io.setAttribute(QStringLiteral("comment"), io.comment);
|
||||
xml_io.setAttribute(QStringLiteral("crossRef"), io.crossRef);
|
||||
xml_io.setAttribute(QStringLiteral("terminalCount"), io.terminalCount);
|
||||
for (const auto &t : io.terminals) {
|
||||
auto xml_term = document.createElement(QStringLiteral("terminal"));
|
||||
xml_term.appendChild(document.createTextNode(t));
|
||||
xml_io.appendChild(xml_term);
|
||||
}
|
||||
xml_ios.appendChild(xml_io);
|
||||
}
|
||||
xml_plc.appendChild(xml_ios);
|
||||
|
||||
return xml_plc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ElementData::plcMasterDataFromXml
|
||||
* Deserialize PLC master data from XML
|
||||
*/
|
||||
void ElementData::plcMasterDataFromXml(const QDomElement &xml_plc)
|
||||
{
|
||||
if (xml_plc.isNull())
|
||||
return;
|
||||
|
||||
// Reset PLC data before loading to avoid appending to existing data
|
||||
m_plc_master_data = PlcMasterData();
|
||||
|
||||
m_plc_master_data.rowHeight = xml_plc.attribute(
|
||||
QStringLiteral("rowHeight"), QStringLiteral("8.0")).toDouble();
|
||||
|
||||
// Load break positions
|
||||
auto xml_breaks = xml_plc.firstChildElement(QStringLiteral("breakPositions"));
|
||||
for (const auto &xml_bp : QETXML::findInDomElement(xml_breaks, QStringLiteral("break"))) {
|
||||
m_plc_master_data.breakPositions.append(xml_bp.text().toInt());
|
||||
}
|
||||
|
||||
// Load column widths
|
||||
auto xml_col_widths = xml_plc.firstChildElement(QStringLiteral("columnWidths"));
|
||||
for (const auto &xml_col : QETXML::findInDomElement(xml_col_widths, QStringLiteral("column"))) {
|
||||
int idx = xml_col.attribute(QStringLiteral("index")).toInt();
|
||||
qreal w = xml_col.attribute(QStringLiteral("width")).toDouble();
|
||||
m_plc_master_data.colWidths.insert(idx, w);
|
||||
}
|
||||
|
||||
// Load column visibility
|
||||
auto xml_col_vis = xml_plc.firstChildElement(QStringLiteral("columnVisibility"));
|
||||
for (const auto &xml_col : QETXML::findInDomElement(xml_col_vis, QStringLiteral("column"))) {
|
||||
int idx = xml_col.attribute(QStringLiteral("index")).toInt();
|
||||
bool vis = xml_col.attribute(QStringLiteral("visible")) == QLatin1String("true");
|
||||
m_plc_master_data.colVisible.insert(idx, vis);
|
||||
}
|
||||
|
||||
// Load fonts
|
||||
auto xml_hfont = xml_plc.firstChildElement(QStringLiteral("headerFont"));
|
||||
if (!xml_hfont.isNull()) {
|
||||
m_plc_master_data.headerFont.setFamily(xml_hfont.attribute(QStringLiteral("family")));
|
||||
m_plc_master_data.headerFont.setPointSize(xml_hfont.attribute(QStringLiteral("size")).toInt());
|
||||
m_plc_master_data.headerFont.setBold(xml_hfont.attribute(QStringLiteral("bold")) == QLatin1String("true"));
|
||||
}
|
||||
auto xml_cfont = xml_plc.firstChildElement(QStringLiteral("cellFont"));
|
||||
if (!xml_cfont.isNull()) {
|
||||
m_plc_master_data.cellFont.setFamily(xml_cfont.attribute(QStringLiteral("family")));
|
||||
m_plc_master_data.cellFont.setPointSize(xml_cfont.attribute(QStringLiteral("size")).toInt());
|
||||
m_plc_master_data.cellFont.setBold(xml_cfont.attribute(QStringLiteral("bold")) == QLatin1String("true"));
|
||||
}
|
||||
|
||||
// Load custom column names
|
||||
auto xml_names = xml_plc.firstChildElement(QStringLiteral("columnNames"));
|
||||
for (const auto &xml_col : QETXML::findInDomElement(xml_names, QStringLiteral("column"))) {
|
||||
int idx = xml_col.attribute(QStringLiteral("index")).toInt();
|
||||
while (m_plc_master_data.columnNames.size() <= idx)
|
||||
m_plc_master_data.columnNames.append(QString());
|
||||
m_plc_master_data.columnNames.replace(idx, xml_col.text());
|
||||
}
|
||||
|
||||
// Load column order
|
||||
auto xml_order = xml_plc.firstChildElement(QStringLiteral("columnOrder"));
|
||||
if (!xml_order.isNull()) {
|
||||
QStringList order_str_list = xml_order.text().split(',');
|
||||
for (const auto &s : order_str_list) {
|
||||
m_plc_master_data.columnOrder.append(s.trimmed().toInt());
|
||||
}
|
||||
}
|
||||
|
||||
// Load showHeaders
|
||||
auto xml_sh = xml_plc.firstChildElement(QStringLiteral("showHeaders"));
|
||||
if (!xml_sh.isNull()) {
|
||||
m_plc_master_data.showHeaders = xml_sh.text() == QLatin1String("1");
|
||||
}
|
||||
|
||||
// Load IO entries
|
||||
auto xml_ios = xml_plc.firstChildElement(QStringLiteral("plcIOs"));
|
||||
for (const auto &xml_io : QETXML::findInDomElement(xml_ios, QStringLiteral("plcIO"))) {
|
||||
PlcIO io;
|
||||
io.type = plcIOTypeFromString(xml_io.attribute(QStringLiteral("type")));
|
||||
io.address = xml_io.attribute(QStringLiteral("address"));
|
||||
io.functionText = xml_io.attribute(QStringLiteral("functionText"));
|
||||
io.comment = xml_io.attribute(QStringLiteral("comment"));
|
||||
io.crossRef = xml_io.attribute(QStringLiteral("crossRef"));
|
||||
io.terminalCount = xml_io.attribute(QStringLiteral("terminalCount")).toInt();
|
||||
for (const auto &xml_term : QETXML::findInDomElement(xml_io, QStringLiteral("terminal"))) {
|
||||
io.terminals.append(xml_term.text());
|
||||
}
|
||||
m_plc_master_data.ios.append(io);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ElementData::setTerminalType
|
||||
* Override the terminal type by \p t_type
|
||||
|
||||
@@ -130,6 +130,11 @@ class ElementData : public PropertiesInterface
|
||||
QList<int> columnOrder; ///< Column display order (logical indices)
|
||||
bool showHeaders = true; ///< Show column headers on sheet
|
||||
|
||||
PlcMasterData() {
|
||||
headerFont.setFamily(QString());
|
||||
cellFont.setFamily(QString());
|
||||
}
|
||||
|
||||
bool operator==(const PlcMasterData &other) const {
|
||||
return ios == other.ios
|
||||
&& breakPositions == other.breakPositions
|
||||
@@ -201,6 +206,8 @@ class ElementData : public PropertiesInterface
|
||||
QDomElement toXml(QDomDocument &xml_element) const override;
|
||||
bool fromXml(const QDomElement &xml_element) override;
|
||||
QDomElement kindInfoToXml(QDomDocument &document);
|
||||
QDomElement plcMasterDataToXml(QDomDocument &document) const;
|
||||
void plcMasterDataFromXml(const QDomElement &xml_plc);
|
||||
|
||||
void setTerminalType(ElementData::TerminalType t_type);
|
||||
ElementData::TerminalType terminalType() const;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
#include "terminaldata.h"
|
||||
|
||||
#include "../qetapp.h"
|
||||
#include "../utils/qetutils.h"
|
||||
|
||||
#include <QGraphicsObject>
|
||||
@@ -38,6 +39,7 @@ TerminalData::TerminalData(QGraphicsObject *parent):
|
||||
|
||||
void TerminalData::init()
|
||||
{
|
||||
m_label_font = QETApp::diagramTextsFont();
|
||||
}
|
||||
|
||||
TerminalData::~TerminalData()
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
#include "machine_info.h"
|
||||
#include "TerminalStrip/ui/terminalstripeditorwindow.h"
|
||||
#include "qetversion.h"
|
||||
#include "logging/qetlogger.h"
|
||||
#include "logging/ui/diagnosticsreportdialog.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
@@ -2575,6 +2577,10 @@ void QETApp::checkBackupFiles()
|
||||
}
|
||||
|
||||
if (stale_files.isEmpty()) {
|
||||
// Only offer an unretrieved crash dump when there's no project
|
||||
// to recover this run -- discussion #644 step 5 is explicit
|
||||
// that the two prompts must never both show at once.
|
||||
checkCrashDump();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2628,6 +2634,53 @@ void QETApp::checkBackupFiles()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETApp::checkCrashDump
|
||||
Discussion #644, step 5: if the crash handler (step 4) left an
|
||||
unretrieved dump from a previous run, offer it to the user. Only
|
||||
called from checkBackupFiles() when there was no stale project file
|
||||
to recover this run, so the two prompts never both show at once.
|
||||
*/
|
||||
void QETApp::checkCrashDump()
|
||||
{
|
||||
QetLogger &logger = QetLogger::instance();
|
||||
if (!logger.hasPendingCrashDump()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QByteArray content = logger.pendingCrashDumpContents();
|
||||
|
||||
DiagnosticsReportDialog dialog(
|
||||
tr("Rapport de plantage"),
|
||||
tr("QElectroTech ne s'est pas fermé correctement lors de sa dernière exécution.\n"
|
||||
"Voici les derniers messages enregistrés avant l'arrêt -- vous pouvez les "
|
||||
"enregistrer pour les joindre à un rapport de bug."),
|
||||
content);
|
||||
dialog.exec();
|
||||
|
||||
// Offered once, then marked retrieved -- regardless of whether the
|
||||
// user chose to save it -- so it is never offered a second time.
|
||||
logger.clearPendingCrashDump();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETApp::showDiagnosticsReport
|
||||
Discussion #644, step 5: the manual "Help > Diagnostics > Save
|
||||
report" action. Unlike checkCrashDump(), this is about the *current*,
|
||||
still-running session, not a previous one.
|
||||
*/
|
||||
void QETApp::showDiagnosticsReport()
|
||||
{
|
||||
const QByteArray content = QetLogger::instance().buildDiagnosticsReport();
|
||||
|
||||
DiagnosticsReportDialog dialog(
|
||||
tr("Rapport de diagnostic"),
|
||||
tr("Ceci contient les derniers messages de journalisation de cette session. "
|
||||
"Vérifiez le contenu avant de le joindre à un rapport de bug public."),
|
||||
content);
|
||||
dialog.exec();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETApp::fetchWindowStats
|
||||
Updates the booleans concerning the state of the windows
|
||||
|
||||
@@ -271,6 +271,7 @@ class QETApp : public QObject
|
||||
void openTitleBlockTemplateFiles(const QStringList &);
|
||||
void configureQET();
|
||||
void aboutQET();
|
||||
void showDiagnosticsReport();
|
||||
void receiveMessage(int instanceId, QByteArray message);
|
||||
|
||||
private:
|
||||
@@ -287,6 +288,7 @@ class QETApp : public QObject
|
||||
void initSystemTray();
|
||||
void buildSystemTrayMenu();
|
||||
void checkBackupFiles();
|
||||
void checkCrashDump();
|
||||
void fetchWindowStats(
|
||||
const QList<QETDiagramEditor *> &,
|
||||
const QList<QETElementEditor *> &,
|
||||
|
||||
@@ -185,6 +185,7 @@ void QETDiagramEditor::setUpElementsPanel()
|
||||
connect(pa, SIGNAL(requestForProjectClosing (QETProject *)), this, SLOT(closeProject(QETProject *)));
|
||||
connect(pa, SIGNAL(requestForProjectPropertiesEdition (QETProject *)), this, SLOT(editProjectProperties(QETProject *)));
|
||||
connect(pa, SIGNAL(requestForNewDiagram (QETProject *)), this, SLOT(addDiagramToProject(QETProject *)));
|
||||
connect(pa, SIGNAL(requestForNewDiagramAt (QETProject *, int)), this, SLOT(addDiagramToProjectAt(QETProject *, int)));
|
||||
connect(pa, SIGNAL(requestForDiagramPropertiesEdition (Diagram *)), this, SLOT(editDiagramProperties(Diagram *)));
|
||||
connect(pa, SIGNAL(requestForDiagramsDeletion (const QList<Diagram *> &)), this, SLOT(removeDiagrams(const QList<Diagram *> &)));
|
||||
connect(pa, SIGNAL(requestForDiagramMoveUp (const QList<Diagram *> &)), this, SLOT(moveDiagramUp(const QList<Diagram *>&)));
|
||||
@@ -897,6 +898,8 @@ void QETDiagramEditor::setUpMenu()
|
||||
|
||||
// menu Projet
|
||||
menu_project -> addAction(m_project_edit_properties);
|
||||
menu_project -> addAction(m_auto_conductor);
|
||||
menu_project -> addSeparator();
|
||||
menu_project -> addAction(m_project_add_diagram);
|
||||
menu_project -> addAction(m_remove_diagram_from_project);
|
||||
menu_project -> addAction(m_clean_project);
|
||||
@@ -932,6 +935,7 @@ void QETDiagramEditor::setUpMenu()
|
||||
menu_affichage -> addAction(m_mode_visualise);
|
||||
menu_affichage -> addSeparator();
|
||||
menu_affichage -> addAction(m_draw_grid);
|
||||
menu_affichage -> addAction(m_draw_guides);
|
||||
menu_affichage -> addAction(m_grey_background);
|
||||
menu_affichage -> addSeparator();
|
||||
menu_affichage -> addActions(m_zoom_actions_group.actions());
|
||||
@@ -2295,6 +2299,25 @@ void QETDiagramEditor::addDiagramToProject(QETProject *project)
|
||||
project_view->project()->addNewDiagram();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETDiagramEditor::addDiagramToProjectAt
|
||||
Add a diagram to project, inserted at a specific position.
|
||||
@param project
|
||||
@param pos
|
||||
*/
|
||||
void QETDiagramEditor::addDiagramToProjectAt(QETProject *project, int pos)
|
||||
{
|
||||
if (!project) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ProjectView *project_view = findProject(project))
|
||||
{
|
||||
activateProject(project);
|
||||
project_view->project()->addNewDiagram(pos);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @brief QETDiagramEditor::removeDiagram
|
||||
* Wrapper für einzelne Diagramme, um Abwärtskompatibilität zu erhalten.
|
||||
|
||||
@@ -137,6 +137,7 @@ class QETDiagramEditor : public QETMainWindow
|
||||
void editDiagramProperties(DiagramView *);
|
||||
void editDiagramProperties(Diagram *);
|
||||
void addDiagramToProject(QETProject *);
|
||||
void addDiagramToProjectAt(QETProject *, int);
|
||||
void removeDiagram(Diagram *);
|
||||
void removeDiagrams(const QList<Diagram *> &diagrams);
|
||||
void removeDiagramFromProject();
|
||||
@@ -200,7 +201,6 @@ class QETDiagramEditor : public QETMainWindow
|
||||
*m_project_add_diagram, ///< Add a diagram to the current project.
|
||||
*m_remove_diagram_from_project, ///< Delete a diagram from the current project
|
||||
*m_clean_project, ///< Clean the content of the current project by removing useless items
|
||||
*m_project_folio_list, ///< Sommaire des schemas
|
||||
*m_csv_export, ///< generate nomenclature
|
||||
*m_add_nomenclature, ///< Add nomenclature graphics item;
|
||||
*m_add_summary, ///<Add summary graphics item
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "element.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../PropertiesEditor/propertieseditordialog.h"
|
||||
#include "../autoNum/assignvariables.h"
|
||||
@@ -33,6 +34,7 @@
|
||||
#include "../qetgraphicsitem/terminal.h"
|
||||
#include "../ui/elementpropertieswidget.h"
|
||||
#include "../undocommand/changeelementinformationcommand.h"
|
||||
#include "../undocommand/setautonumcontextcommand.h"
|
||||
#include "dynamicelementtextitem.h"
|
||||
#include "elementtextitemgroup.h"
|
||||
#include "iostream"
|
||||
@@ -860,6 +862,15 @@ bool Element::fromXml(QDomElement &e,
|
||||
}
|
||||
}
|
||||
|
||||
//Load PLC master data override from diagram XML
|
||||
if (m_data.m_type == ElementData::Master &&
|
||||
m_data.m_master_type == ElementData::PLC)
|
||||
{
|
||||
auto xml_plc = e.firstChildElement(QStringLiteral("plcMasterData"));
|
||||
if (!xml_plc.isNull())
|
||||
m_data.plcMasterDataFromXml(xml_plc);
|
||||
}
|
||||
|
||||
//We must block the update of the alignment when loading the information
|
||||
//otherwise the pos of the text will not be the same as it was at save time.
|
||||
for(DynamicElementTextItem *deti : m_dynamic_text_list)
|
||||
@@ -993,6 +1004,15 @@ QDomElement Element::toXml(
|
||||
element.appendChild(properties);
|
||||
}
|
||||
|
||||
//Save PLC master data override for elements on diagram
|
||||
if (m_data.m_type == ElementData::Master &&
|
||||
m_data.m_master_type == ElementData::PLC)
|
||||
{
|
||||
auto xml_plc = m_data.plcMasterDataToXml(document);
|
||||
if (!xml_plc.isNull())
|
||||
element.appendChild(xml_plc);
|
||||
}
|
||||
|
||||
//Dynamic texts
|
||||
QDomElement dyn_text = document.createElement(QStringLiteral("dynamic_texts"));
|
||||
for (DynamicElementTextItem *deti : m_dynamic_text_list)
|
||||
@@ -1626,7 +1646,7 @@ void Element::hoverLeaveEvent(QGraphicsSceneHoverEvent *e)
|
||||
(ex K for coil) with condition :
|
||||
formula is empty, text tagged "label" is emptty or "_";
|
||||
*/
|
||||
void Element::setUpFormula(bool code_letter)
|
||||
void Element::setUpFormula(bool code_letter, QUndoCommand *parent_undo)
|
||||
{
|
||||
Q_UNUSED(code_letter)
|
||||
|
||||
@@ -1655,8 +1675,21 @@ void Element::setUpFormula(bool code_letter)
|
||||
nc,
|
||||
diagram(),
|
||||
element_currentAutoNum);
|
||||
diagram()->project()->addElementAutoNum(element_currentAutoNum,
|
||||
ncc.next());
|
||||
|
||||
NumerotationContext new_context = ncc.next();
|
||||
QETProject *project = diagram()->project();
|
||||
auto setter = [project](const QString &k, const NumerotationContext &c) {project->addElementAutoNum(k, c);};
|
||||
|
||||
if (parent_undo)
|
||||
{
|
||||
new SetAutoNumContextCommand(setter, element_currentAutoNum, nc, new_context, parent_undo);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto *undo = new SetAutoNumContextCommand(setter, element_currentAutoNum, nc, new_context);
|
||||
undo->setText(tr("Numéroter automatiquement un élément", "undo caption"));
|
||||
diagram()->undoStack().push(undo);
|
||||
}
|
||||
|
||||
if(!m_freeze_label && !formula.isEmpty())
|
||||
{
|
||||
@@ -1876,12 +1909,12 @@ void Element::drawPlcTable(QPainter *painter)
|
||||
// Fonts
|
||||
QFont header_font = plc_data.headerFont;
|
||||
if (header_font.family().isEmpty()) {
|
||||
header_font = painter->font();
|
||||
header_font = QETApp::diagramTextsFont();
|
||||
header_font.setBold(true);
|
||||
}
|
||||
QFont cell_font = plc_data.cellFont;
|
||||
if (cell_font.family().isEmpty()) {
|
||||
cell_font = painter->font();
|
||||
cell_font = QETApp::diagramTextsFont();
|
||||
}
|
||||
|
||||
for (const QPointF &pos : positions) {
|
||||
|
||||
@@ -35,6 +35,7 @@ class Terminal;
|
||||
class Conductor;
|
||||
class DynamicElementTextItem;
|
||||
class ElementTextItemGroup;
|
||||
class QUndoCommand;
|
||||
|
||||
/**
|
||||
This is the base class for electrical elements.
|
||||
@@ -142,7 +143,7 @@ class Element : public QetGraphicsItem
|
||||
{return m_autoNum_seq;}
|
||||
autonum::sequentialNumbers& rSequenceStruct()
|
||||
{return m_autoNum_seq;}
|
||||
void setUpFormula(bool code_letter = true);
|
||||
void setUpFormula(bool code_letter = true, QUndoCommand *parent_undo = nullptr);
|
||||
void setPrefix(QString);
|
||||
QString getPrefix() const;
|
||||
void freezeLabel(bool freeze);
|
||||
|
||||
@@ -136,6 +136,12 @@ void QETMainWindow::initCommonActions()
|
||||
about_qt_ = new QAction(QET::Icons::QtLogo, tr("À propos de &Qt"), this);
|
||||
about_qt_ -> setStatusTip(tr("Affiche des informations sur la bibliothèque Qt", "status bar tip"));
|
||||
connect(about_qt_, SIGNAL(triggered()), qApp, SLOT(aboutQt()));
|
||||
|
||||
diagnostics_action_ = new QAction(QET::Icons::DialogInformation, tr("Enregistrer un rapport de diagnostic..."), this);
|
||||
diagnostics_action_ -> setStatusTip(tr("Génère un rapport avec les derniers messages de journalisation, pour l'inclure dans un rapport de bug", "status bar tip"));
|
||||
connect(diagnostics_action_, &QAction::triggered, this, []() {
|
||||
QETApp::instance()->showDiagnosticsReport();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,6 +164,8 @@ void QETMainWindow::initCommonMenus()
|
||||
help_menu_ -> addAction(donate_);
|
||||
help_menu_ -> addAction(about_qt_);
|
||||
help_menu_ -> addAction(about_qet_);
|
||||
help_menu_ -> addSeparator();
|
||||
help_menu_ -> addAction(diagnostics_action_);
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
upgrade_ -> setVisible(true);
|
||||
|
||||
@@ -62,6 +62,7 @@ class QETMainWindow : public QMainWindow {
|
||||
QAction *upgrade_M; ///< Launch browser on QElectroTech MAC_OS_X builds
|
||||
QAction *donate_; ///< Launch browser to donate link
|
||||
QAction *about_qt_; ///< launch the "About Qt" dialog
|
||||
QAction *diagnostics_action_; ///< Open the diagnostics report dialog (discussion #644, step 5)
|
||||
QMenu *settings_menu_; ///< Settings menu
|
||||
QMenu *help_menu_; ///< Help menu
|
||||
QMenu *display_toolbars_; ///< Show/hide toolbars/docks
|
||||
|
||||
@@ -730,6 +730,7 @@ QHash <QString, NumerotationContext> QETProject::folioAutoNum() const
|
||||
*/
|
||||
void QETProject::addConductorAutoNum(const QString& key, const NumerotationContext& context) {
|
||||
m_conductor_autonum.insert(key, context);
|
||||
emit autoNumContextUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -743,6 +744,7 @@ void QETProject::addElementAutoNum(const QString& key, const NumerotationContext
|
||||
{
|
||||
m_element_autonum.insert(key, context);
|
||||
emit elementAutoNumAdded(key);
|
||||
emit autoNumContextUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -754,6 +756,7 @@ void QETProject::addElementAutoNum(const QString& key, const NumerotationContext
|
||||
*/
|
||||
void QETProject::addFolioAutoNum(const QString& key, const NumerotationContext& context) {
|
||||
m_folio_autonum.insert(key, context);
|
||||
emit autoNumContextUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -237,6 +237,12 @@ class QETProject : public QObject
|
||||
void conductorAutoNumAdded();
|
||||
void conductorAutoNumRemoved();
|
||||
void folioAutoNumAdded();
|
||||
/// A numerotation context's *values* changed -- as happens every
|
||||
/// time an element or conductor consumes the next number, not
|
||||
/// only when a rule is added or removed. Deliberately separate
|
||||
/// from the *Added/*Removed signals above, which make listeners
|
||||
/// rebuild their rule lists; this one just says "re-read me".
|
||||
void autoNumContextUpdated();
|
||||
void folioAutoNumRemoved();
|
||||
void folioAutoNumChanged(QString);
|
||||
void defaultTitleBlockPropertiesChanged();
|
||||
|
||||
@@ -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
|
||||
@@ -214,7 +214,11 @@ void NewDiagramPage::applyConf()
|
||||
rpw->toSettings(settings, "diagrameditor/defaultreport");
|
||||
|
||||
// default xref properties
|
||||
QHash <QString, XRefProperties> hash_xrp = xrefpw -> properties();
|
||||
const QHash<QString, XRefProperties> hash_xrp = xrefpw->properties();
|
||||
for (auto it = hash_xrp.constBegin() ; it != hash_xrp.constEnd() ; ++it) {
|
||||
it.value().toSettings(settings,
|
||||
QStringLiteral("diagrameditor/defaultxref") % it.key());
|
||||
}
|
||||
|
||||
// Global in QSettings speichern
|
||||
QList<Diagram::Guide> current_guides = m_gpw->guides();
|
||||
|
||||
@@ -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;"));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -17,12 +17,14 @@
|
||||
*/
|
||||
#include "elementinfowidget.h"
|
||||
#include <QCheckBox>
|
||||
#include <QPushButton>
|
||||
#include "../diagram.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "../qetinformation.h"
|
||||
#include "../ui_elementinfowidget.h"
|
||||
#include "../undocommand/changeelementinformationcommand.h"
|
||||
#include "customelementinfopartwidget.h"
|
||||
#include "elementinfopartwidget.h"
|
||||
|
||||
/**
|
||||
@@ -47,6 +49,7 @@ ElementInfoWidget::ElementInfoWidget(Element *elmt, QWidget *parent) :
|
||||
ElementInfoWidget::~ElementInfoWidget()
|
||||
{
|
||||
qDeleteAll(m_eipw_list);
|
||||
qDeleteAll(m_custom_eipw_list);
|
||||
delete ui;
|
||||
}
|
||||
|
||||
@@ -207,6 +210,11 @@ void ElementInfoWidget::buildInterface()
|
||||
ui->scroll_vlayout->addWidget(eipw);
|
||||
m_eipw_list << eipw;
|
||||
}
|
||||
|
||||
m_add_custom_property_btn = new QPushButton(tr("Ajouter une propriété personnalisée"), this);
|
||||
connect(m_add_custom_property_btn, &QPushButton::clicked, this, [this]() { addCustomProperty(); });
|
||||
ui->scroll_vlayout->addWidget(m_add_custom_property_btn);
|
||||
|
||||
ui->scroll_vlayout->addStretch();
|
||||
|
||||
// Existing potential isolating checkbox
|
||||
@@ -235,6 +243,67 @@ void ElementInfoWidget::buildInterface()
|
||||
m_potential_isolating_cb->setVisible(false);
|
||||
}
|
||||
}
|
||||
/**
|
||||
@brief ElementInfoWidget::predefinedKeys
|
||||
@return every key this widget already exposes a dedicated row for,
|
||||
whether through ElementInfoPartWidget (the ~40 ELMT_* keys) or one
|
||||
of the standalone checkboxes. Anything present in the element's
|
||||
informations but absent from this list is a user-defined custom
|
||||
property.
|
||||
*/
|
||||
QStringList ElementInfoWidget::predefinedKeys() const
|
||||
{
|
||||
QStringList keys = (m_element.data()->elementData().m_type == ElementData::Terminal)
|
||||
? QETInformation::terminalElementInfoKeys()
|
||||
: QETInformation::elementInfoKeys();
|
||||
|
||||
keys << QStringLiteral("auto_num_locked")
|
||||
<< QStringLiteral("potential_isolating")
|
||||
<< QStringLiteral("exclude_from_bom");
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoWidget::addCustomProperty
|
||||
Append a new user-defined key/value row to the widget.
|
||||
@param key initial key, left empty for a freshly added row
|
||||
@param value initial value
|
||||
*/
|
||||
void ElementInfoWidget::addCustomProperty(const QString &key, const QString &value)
|
||||
{
|
||||
auto *widget = new CustomElementInfoPartWidget(key, value, this);
|
||||
|
||||
const int insert_index = ui->scroll_vlayout->indexOf(m_add_custom_property_btn);
|
||||
ui->scroll_vlayout->insertWidget(insert_index >= 0 ? insert_index : ui->scroll_vlayout->count(), widget);
|
||||
m_custom_eipw_list << widget;
|
||||
|
||||
connect(widget, &CustomElementInfoPartWidget::removeRequested, this, &ElementInfoWidget::removeCustomProperty);
|
||||
connect(widget, &CustomElementInfoPartWidget::changed, this, [this]() {
|
||||
if (m_live_edit) apply();
|
||||
});
|
||||
|
||||
if (key.isEmpty()) {
|
||||
widget->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoWidget::removeCustomProperty
|
||||
Remove a user-defined key/value row.
|
||||
@param widget the row to remove
|
||||
*/
|
||||
void ElementInfoWidget::removeCustomProperty(CustomElementInfoPartWidget *widget)
|
||||
{
|
||||
if (!m_custom_eipw_list.removeOne(widget))
|
||||
return;
|
||||
|
||||
ui->scroll_vlayout->removeWidget(widget);
|
||||
widget->deleteLater();
|
||||
|
||||
if (m_live_edit) apply();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoWidget::infoPartWidgetForKey
|
||||
@param key
|
||||
@@ -271,6 +340,21 @@ void ElementInfoWidget::updateUi()
|
||||
for (ElementInfoPartWidget *eipw : m_eipw_list) {
|
||||
eipw -> setText (element_info[eipw->key()].toString());
|
||||
}
|
||||
|
||||
// Rebuild the custom-property rows to match whatever
|
||||
// user-defined keys this element currently carries.
|
||||
while (!m_custom_eipw_list.isEmpty()) {
|
||||
CustomElementInfoPartWidget *w = m_custom_eipw_list.takeLast();
|
||||
ui->scroll_vlayout->removeWidget(w);
|
||||
delete w;
|
||||
}
|
||||
const auto known_keys = predefinedKeys();
|
||||
for (const QString &key : element_info.keys()) {
|
||||
if (!known_keys.contains(key)) {
|
||||
addCustomProperty(key, element_info[key].toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Load the lock status for auto numbering
|
||||
if (m_element->elementData().m_type == ElementData::Terminal) {
|
||||
QString lock_value = element_info.value(QStringLiteral("auto_num_locked")).toString();
|
||||
@@ -314,6 +398,17 @@ DiagramContext ElementInfoWidget::currentInfo() const
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &custom : std::as_const(m_custom_eipw_list))
|
||||
{
|
||||
if (custom->hasValidKey() && !custom->value().isEmpty())
|
||||
{
|
||||
QString txt{custom->value()};
|
||||
txt.remove(QStringLiteral("\r"));
|
||||
txt.remove(QStringLiteral("\n"));
|
||||
info_.addValue(custom->key(), txt);
|
||||
}
|
||||
}
|
||||
|
||||
// Save the auto numbering lock status
|
||||
if (m_element->elementData().m_type == ElementData::Terminal) {
|
||||
info_.addValue(QStringLiteral("auto_num_locked"), ui->m_auto_num_locked_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"));
|
||||
|
||||
@@ -26,8 +26,10 @@
|
||||
class Element;
|
||||
class QUndoCommand;
|
||||
class ElementInfoPartWidget;
|
||||
class CustomElementInfoPartWidget;
|
||||
class ChangeElementInformationCommand;
|
||||
class QCheckBox;
|
||||
class QPushButton;
|
||||
|
||||
namespace Ui {
|
||||
class ElementInfoWidget;
|
||||
@@ -63,15 +65,20 @@ class ElementInfoWidget : public AbstractElementPropertiesEditorWidget
|
||||
private:
|
||||
void buildInterface();
|
||||
ElementInfoPartWidget *infoPartWidgetForKey(const QString &key) const;
|
||||
QStringList predefinedKeys() const;
|
||||
|
||||
private slots:
|
||||
void firstActivated();
|
||||
void elementInfoChange();
|
||||
void addCustomProperty(const QString &key = QString(), const QString &value = QString());
|
||||
void removeCustomProperty(CustomElementInfoPartWidget *widget);
|
||||
|
||||
//ATTRIBUTES
|
||||
private:
|
||||
Ui::ElementInfoWidget *ui;
|
||||
QList <ElementInfoPartWidget *> m_eipw_list;
|
||||
QList <CustomElementInfoPartWidget *> m_custom_eipw_list;
|
||||
QPushButton *m_add_custom_property_btn = nullptr;
|
||||
QCheckBox *m_potential_isolating_cb = nullptr;
|
||||
QCheckBox *m_exclude_from_bom_cb = nullptr;
|
||||
bool m_first_activation;
|
||||
|
||||
@@ -535,7 +535,7 @@ void MasterPropertiesWidget::updateUi()
|
||||
tr("Commentaire"), tr("Réf. croisée")
|
||||
});
|
||||
m_plc_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
||||
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems);
|
||||
m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
m_plc_table->setMinimumHeight(200);
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "setautonumcontextcommand.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
@brief SetAutoNumContextCommand::SetAutoNumContextCommand
|
||||
@param setter the QETProject setter to call on undo/redo
|
||||
(addConductorAutoNum/addElementAutoNum/addFolioAutoNum, bound to a project)
|
||||
@param key the numerotation context's name/key
|
||||
@param old_context the context's value before this placement
|
||||
@param new_context the context's value after this placement
|
||||
@param parent parent undo command
|
||||
*/
|
||||
SetAutoNumContextCommand::SetAutoNumContextCommand(
|
||||
Setter setter,
|
||||
const QString &key,
|
||||
const NumerotationContext &old_context,
|
||||
const NumerotationContext &new_context,
|
||||
QUndoCommand *parent) :
|
||||
QUndoCommand(parent),
|
||||
m_setter(std::move(setter)),
|
||||
m_key(key),
|
||||
m_old_context(old_context),
|
||||
m_new_context(new_context)
|
||||
{}
|
||||
|
||||
void SetAutoNumContextCommand::redo()
|
||||
{
|
||||
m_setter(m_key, m_new_context);
|
||||
}
|
||||
|
||||
void SetAutoNumContextCommand::undo()
|
||||
{
|
||||
m_setter(m_key, m_old_context);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user