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