mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-08-13 10:04:13 +02:00
Merge branch 'master' into master-modernize-signal-slot
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
|
||||||
@@ -205,6 +215,8 @@ set(QET_SRC_FILES
|
|||||||
${QET_DIR}/sources/exportpropertieswidget.h
|
${QET_DIR}/sources/exportpropertieswidget.h
|
||||||
${QET_DIR}/sources/genericpanel.cpp
|
${QET_DIR}/sources/genericpanel.cpp
|
||||||
${QET_DIR}/sources/genericpanel.h
|
${QET_DIR}/sources/genericpanel.h
|
||||||
|
${QET_DIR}/sources/lastusedstyle.cpp
|
||||||
|
${QET_DIR}/sources/lastusedstyle.h
|
||||||
${QET_DIR}/sources/machine_info.cpp
|
${QET_DIR}/sources/machine_info.cpp
|
||||||
${QET_DIR}/sources/machine_info.h
|
${QET_DIR}/sources/machine_info.h
|
||||||
${QET_DIR}/sources/main.cpp
|
${QET_DIR}/sources/main.cpp
|
||||||
@@ -530,7 +542,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 +694,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 +774,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
|
||||||
|
|||||||
+1027
-743
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
+1021
-743
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+1020
-743
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+1021
-743
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+1044
-768
File diff suppressed because it is too large
Load Diff
+1026
-748
File diff suppressed because it is too large
Load Diff
+1017
-743
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
+1017
-743
File diff suppressed because it is too large
Load Diff
+1017
-743
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
+1047
-771
File diff suppressed because it is too large
Load Diff
+1021
-743
File diff suppressed because it is too large
Load Diff
+1047
-771
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
+1050
-772
File diff suppressed because it is too large
Load Diff
+1049
-771
File diff suppressed because it is too large
Load Diff
+1021
-743
File diff suppressed because it is too large
Load Diff
+1049
-771
File diff suppressed because it is too large
Load Diff
+1051
-771
File diff suppressed because it is too large
Load Diff
+1049
-771
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
+1017
-743
File diff suppressed because it is too large
Load Diff
+1021
-743
File diff suppressed because it is too large
Load Diff
+1017
-743
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
|
||||||
|
|||||||
@@ -220,6 +220,24 @@ void NumerotationContext::replaceValue(int index, QString content) {
|
|||||||
content_[index] = type + "|" + value + "|" + increase + "|" + initvalue + "|" + modulus + "|" + format;
|
content_[index] = type + "|" + value + "|" + increase + "|" + initvalue + "|" + modulus + "|" + format;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief NumerotationContext::replaceIncrease
|
||||||
|
Change how much this part advances per step, leaving its current value,
|
||||||
|
initial value, modulus and format untouched. Sibling to replaceValue(),
|
||||||
|
which deliberately never touches this field.
|
||||||
|
@param index of NC item
|
||||||
|
@param increase new increase for that item
|
||||||
|
*/
|
||||||
|
void NumerotationContext::replaceIncrease(int index, int increase) {
|
||||||
|
QStringList strl = content_[index].split("|");
|
||||||
|
QString type = strl.at(0);
|
||||||
|
QString value = strl.at(1);
|
||||||
|
QString initvalue = strl.at(3);
|
||||||
|
QString modulus = strl.size() > 4 ? strl.at(4) : QStringLiteral("0");
|
||||||
|
QString format = strl.size() > 5 ? strl.at(5) : QString();
|
||||||
|
content_[index] = type + "|" + value + "|" + QString::number(increase) + "|" + initvalue + "|" + modulus + "|" + format;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief NumerotationContext::formatOf
|
@brief NumerotationContext::formatOf
|
||||||
@param item : a context item as returned by itemAt()
|
@param item : a context item as returned by itemAt()
|
||||||
@@ -230,3 +248,30 @@ QString NumerotationContext::formatOf(const QStringList &item)
|
|||||||
{
|
{
|
||||||
return item.size() > 5 ? item.at(5) : QString();
|
return item.size() > 5 ? item.at(5) : QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief NumerotationContext::formatValue
|
||||||
|
@param item : a context item as returned by itemAt()
|
||||||
|
@return the part's value, zero-padded exactly as
|
||||||
|
autonum::setSequentialToList() pads it when composing a real label: an
|
||||||
|
explicit format mask wins, then "ten"/"hundred" parts get their
|
||||||
|
implicit 2/3-digit width, "alpha" is used as-is, everything else is a
|
||||||
|
plain number. Kept in step with that function by hand since the two
|
||||||
|
cannot share code without exposing an assignvariables.cpp-local helper.
|
||||||
|
*/
|
||||||
|
QString NumerotationContext::formatValue(const QStringList &item)
|
||||||
|
{
|
||||||
|
const QString &type = item.at(0);
|
||||||
|
const QString &value = item.at(1);
|
||||||
|
if (type == QLatin1String("alpha"))
|
||||||
|
return value;
|
||||||
|
|
||||||
|
const QString mask = formatOf(item);
|
||||||
|
if (!mask.isEmpty())
|
||||||
|
return QString("%1").arg(value.toInt(), mask.length(), 10, QChar('0'));
|
||||||
|
if (type == QLatin1String("ten") || type == QLatin1String("tenfolio"))
|
||||||
|
return QString("%1").arg(value.toInt(), 2, 10, QChar('0'));
|
||||||
|
if (type == QLatin1String("hundred") || type == QLatin1String("hundredfolio"))
|
||||||
|
return QString("%1").arg(value.toInt(), 3, 10, QChar('0'));
|
||||||
|
return QString::number(value.toInt());
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ class NumerotationContext
|
|||||||
QDomElement toXml(QDomDocument &, const QString&);
|
QDomElement toXml(QDomDocument &, const QString&);
|
||||||
void fromXml(QDomElement &);
|
void fromXml(QDomElement &);
|
||||||
void replaceValue(int, QString);
|
void replaceValue(int, QString);
|
||||||
|
void replaceIncrease(int, int);
|
||||||
|
/// Zero-pad a part's value the same way the real numbering engine
|
||||||
|
/// does (autonum::setSequentialToList in assignvariables.cpp), so a
|
||||||
|
/// UI preview of a part's value matches what actually gets rendered.
|
||||||
|
static QString formatValue(const QStringList &item);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QStringList content_;
|
QStringList content_;
|
||||||
|
|||||||
@@ -24,10 +24,13 @@
|
|||||||
#include "../../titleblockproperties.h"
|
#include "../../titleblockproperties.h"
|
||||||
#include "../../ui/projectpropertiesdialog.h"
|
#include "../../ui/projectpropertiesdialog.h"
|
||||||
#include "../numerotationcontext.h"
|
#include "../numerotationcontext.h"
|
||||||
|
#include "../numerotationcontextcommands.h"
|
||||||
#include "ui_autonumberingdockwidget.h"
|
#include "ui_autonumberingdockwidget.h"
|
||||||
|
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
|
#include <QSignalBlocker>
|
||||||
|
#include <QSpinBox>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief AutoNumberingDockWidget::AutoNumberingDockWidget
|
@brief AutoNumberingDockWidget::AutoNumberingDockWidget
|
||||||
@@ -64,6 +67,29 @@ void AutoNumberingDockWidget::clear()
|
|||||||
ui->m_conductor_value_le->clear();
|
ui->m_conductor_value_le->clear();
|
||||||
ui->m_element_value_le->clear();
|
ui->m_element_value_le->clear();
|
||||||
ui->m_folio_value_le->clear();
|
ui->m_folio_value_le->clear();
|
||||||
|
ui->m_conductor_next_le->clear();
|
||||||
|
ui->m_element_next_le->clear();
|
||||||
|
ui->m_folio_next_le->clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief AutoNumberingDockWidget::rowFor
|
||||||
|
@return the combo/value/increase/next widgets that make up category's row.
|
||||||
|
*/
|
||||||
|
AutoNumberingDockWidget::Row AutoNumberingDockWidget::rowFor(AutoNumCategory category) const
|
||||||
|
{
|
||||||
|
switch (category) {
|
||||||
|
case AutoNumCategory::Conductor:
|
||||||
|
return {ui->m_conductor_cb, ui->m_conductor_value_le,
|
||||||
|
ui->m_conductor_increase_sb, ui->m_conductor_next_le};
|
||||||
|
case AutoNumCategory::Element:
|
||||||
|
return {ui->m_element_cb, ui->m_element_value_le,
|
||||||
|
ui->m_element_increase_sb, ui->m_element_next_le};
|
||||||
|
case AutoNumCategory::Folio:
|
||||||
|
return {ui->m_folio_cb, ui->m_folio_value_le,
|
||||||
|
ui->m_folio_increase_sb, ui->m_folio_next_le};
|
||||||
|
}
|
||||||
|
return {nullptr, nullptr, nullptr, nullptr};
|
||||||
}
|
}
|
||||||
|
|
||||||
void AutoNumberingDockWidget::projectClosed()
|
void AutoNumberingDockWidget::projectClosed()
|
||||||
@@ -177,9 +203,9 @@ void AutoNumberingDockWidget::setContext()
|
|||||||
|
|
||||||
//The combo boxes have just been repopulated, so the value fields next
|
//The combo boxes have just been repopulated, so the value fields next
|
||||||
//to them are showing whatever the previous project left there.
|
//to them are showing whatever the previous project left there.
|
||||||
refreshValueField(ui->m_conductor_cb, ui->m_conductor_value_le, AutoNumCategory::Conductor);
|
refreshRow(AutoNumCategory::Conductor);
|
||||||
refreshValueField(ui->m_element_cb, ui->m_element_value_le, AutoNumCategory::Element);
|
refreshRow(AutoNumCategory::Element);
|
||||||
refreshValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
|
refreshRow(AutoNumCategory::Folio);
|
||||||
|
|
||||||
this->setActive();
|
this->setActive();
|
||||||
}
|
}
|
||||||
@@ -255,7 +281,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);
|
refreshRow(AutoNumCategory::Conductor);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -284,7 +310,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);
|
refreshRow(AutoNumCategory::Element);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -324,7 +350,8 @@ void AutoNumberingDockWidget::on_m_folio_cb_activated(int) {
|
|||||||
if (m_project_view && m_project_view->currentDiagram()) {
|
if (m_project_view && m_project_view->currentDiagram()) {
|
||||||
m_project_view->currentDiagram()->diagram()->border_and_titleblock.importTitleBlock(ip);
|
m_project_view->currentDiagram()->diagram()->border_and_titleblock.importTitleBlock(ip);
|
||||||
}
|
}
|
||||||
refreshValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
|
emit(folioAutoNumChanged(current_autonum));
|
||||||
|
refreshRow(AutoNumCategory::Folio);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AutoNumberingDockWidget::on_m_configure_pb_clicked()
|
void AutoNumberingDockWidget::on_m_configure_pb_clicked()
|
||||||
@@ -367,6 +394,21 @@ void AutoNumberingDockWidget::on_m_folio_value_le_editingFinished()
|
|||||||
applyValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
|
applyValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AutoNumberingDockWidget::on_m_conductor_increase_sb_valueChanged(int)
|
||||||
|
{
|
||||||
|
applyIncreaseField(ui->m_conductor_cb, ui->m_conductor_increase_sb, AutoNumCategory::Conductor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoNumberingDockWidget::on_m_element_increase_sb_valueChanged(int)
|
||||||
|
{
|
||||||
|
applyIncreaseField(ui->m_element_cb, ui->m_element_increase_sb, AutoNumCategory::Element);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoNumberingDockWidget::on_m_folio_increase_sb_valueChanged(int)
|
||||||
|
{
|
||||||
|
applyIncreaseField(ui->m_folio_cb, ui->m_folio_increase_sb, AutoNumCategory::Folio);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief AutoNumberingDockWidget::contextFor
|
@brief AutoNumberingDockWidget::contextFor
|
||||||
@return the numerotation context named by combo_box, for category
|
@return the numerotation context named by combo_box, for category
|
||||||
@@ -433,17 +475,21 @@ int AutoNumberingDockWidget::counterIndex(const NumerotationContext &context)
|
|||||||
*/
|
*/
|
||||||
void AutoNumberingDockWidget::refreshValueFields()
|
void AutoNumberingDockWidget::refreshValueFields()
|
||||||
{
|
{
|
||||||
//Leave alone a field the user is typing in: numbering an element
|
//Leave alone a row the user is typing in: numbering an element
|
||||||
//refreshes all three, and overwriting a half-typed value under the
|
//refreshes all three rows, and overwriting a half-typed value or
|
||||||
//cursor is worse than showing it a moment out of date. Only this
|
//increment under the cursor is worse than showing it a moment out
|
||||||
//automatic path skips; an explicit refresh after a reset or an edit
|
//of date. Only this automatic path skips; an explicit refresh after
|
||||||
//still writes, so the field always ends up canonical.
|
//a reset or an edit still writes, so the row always ends up
|
||||||
if (!ui->m_conductor_value_le->hasFocus())
|
//canonical. The next-value preview has no such guard: it is
|
||||||
refreshValueField(ui->m_conductor_cb, ui->m_conductor_value_le, AutoNumCategory::Conductor);
|
//read-only, so there is nothing a refresh could clobber.
|
||||||
if (!ui->m_element_value_le->hasFocus())
|
for (AutoNumCategory category : {AutoNumCategory::Conductor,
|
||||||
refreshValueField(ui->m_element_cb, ui->m_element_value_le, AutoNumCategory::Element);
|
AutoNumCategory::Element,
|
||||||
if (!ui->m_folio_value_le->hasFocus())
|
AutoNumCategory::Folio})
|
||||||
refreshValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
|
{
|
||||||
|
const Row row = rowFor(category);
|
||||||
|
if (!row.value->hasFocus() && !row.increase->hasFocus())
|
||||||
|
refreshRow(category);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -485,13 +531,114 @@ void AutoNumberingDockWidget::applyValueField(QComboBox *combo_box, QLineEdit *l
|
|||||||
const QString typed = line_edit->text();
|
const QString typed = line_edit->text();
|
||||||
if (typed.isEmpty() || typed == context.itemAt(index).at(1))
|
if (typed.isEmpty() || typed == context.itemAt(index).at(1))
|
||||||
{
|
{
|
||||||
refreshValueField(combo_box, line_edit, category);
|
refreshRow(category);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.replaceValue(index, typed);
|
context.replaceValue(index, typed);
|
||||||
storeContext(combo_box, category, context);
|
storeContext(combo_box, category, context);
|
||||||
refreshValueField(combo_box, line_edit, category);
|
refreshRow(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief AutoNumberingDockWidget::refreshIncreaseField
|
||||||
|
Show the counter's current step size (bug #331: previously only
|
||||||
|
reachable from the full configuration dialog, via "Configurer").
|
||||||
|
*/
|
||||||
|
void AutoNumberingDockWidget::refreshIncreaseField(QComboBox *combo_box, QSpinBox *increase_sb, AutoNumCategory category)
|
||||||
|
{
|
||||||
|
//QSpinBox::setValue() emits valueChanged() even when called
|
||||||
|
//programmatically. Without blocking it, this refresh would
|
||||||
|
//immediately re-trigger on_..._increase_sb_valueChanged() ->
|
||||||
|
//applyIncreaseField() -> storeContext() -> the project's
|
||||||
|
//autoNumContextUpdated signal -> refreshValueFields() -> back here.
|
||||||
|
const QSignalBlocker blocker(increase_sb);
|
||||||
|
if (!m_project || combo_box->currentText().isEmpty())
|
||||||
|
{
|
||||||
|
increase_sb->setEnabled(false);
|
||||||
|
increase_sb->setValue(increase_sb->minimum());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NumerotationContext context = contextFor(combo_box, category);
|
||||||
|
const int index = counterIndex(context);
|
||||||
|
increase_sb->setEnabled(index >= 0);
|
||||||
|
increase_sb->setValue(index >= 0 ? context.itemAt(index).at(2).toInt()
|
||||||
|
: increase_sb->minimum());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief AutoNumberingDockWidget::applyIncreaseField
|
||||||
|
Write the spin box's step size to the counter it displays (bug #331).
|
||||||
|
*/
|
||||||
|
void AutoNumberingDockWidget::applyIncreaseField(QComboBox *combo_box, QSpinBox *increase_sb, 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;
|
||||||
|
|
||||||
|
if (increase_sb->value() == context.itemAt(index).at(2).toInt())
|
||||||
|
return;
|
||||||
|
|
||||||
|
context.replaceIncrease(index, increase_sb->value());
|
||||||
|
storeContext(combo_box, category, context);
|
||||||
|
refreshRow(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief AutoNumberingDockWidget::refreshNextField
|
||||||
|
Show what this counter will read after one more step (bug #331: "visualiser
|
||||||
|
la prochaine numérotation qui sera appliquée"). Advances a copy of the
|
||||||
|
whole context through NumerotationContextCommands -- the same engine the
|
||||||
|
Suivant button in the full configuration dialog uses to step a context --
|
||||||
|
so wrap-and-carry into this part from a following part, or out of it into
|
||||||
|
a preceding one, comes out identical to what will actually happen when the
|
||||||
|
number is next consumed.
|
||||||
|
*/
|
||||||
|
void AutoNumberingDockWidget::refreshNextField(QComboBox *combo_box, QLineEdit *next_edit, AutoNumCategory category)
|
||||||
|
{
|
||||||
|
if (!m_project || combo_box->currentText().isEmpty())
|
||||||
|
{
|
||||||
|
next_edit->clear();
|
||||||
|
next_edit->setEnabled(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NumerotationContext context = contextFor(combo_box, category);
|
||||||
|
const int index = counterIndex(context);
|
||||||
|
if (index < 0)
|
||||||
|
{
|
||||||
|
next_edit->clear();
|
||||||
|
next_edit->setEnabled(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Diagram *diagram = (m_project_view && m_project_view->currentDiagram())
|
||||||
|
? m_project_view->currentDiagram()->diagram()
|
||||||
|
: nullptr;
|
||||||
|
NumerotationContextCommands ncc(context, diagram);
|
||||||
|
const NumerotationContext next_context = ncc.next();
|
||||||
|
|
||||||
|
next_edit->setEnabled(true);
|
||||||
|
next_edit->setText(NumerotationContext::formatValue(next_context.itemAt(index)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief AutoNumberingDockWidget::refreshRow
|
||||||
|
Refresh a category's value, increment and next-value preview together --
|
||||||
|
every call site that used to refresh just the value field needs the
|
||||||
|
other two kept in step with it as well.
|
||||||
|
*/
|
||||||
|
void AutoNumberingDockWidget::refreshRow(AutoNumCategory category)
|
||||||
|
{
|
||||||
|
const Row row = rowFor(category);
|
||||||
|
refreshValueField(row.combo, row.value, category);
|
||||||
|
refreshIncreaseField(row.combo, row.increase, category);
|
||||||
|
refreshNextField(row.combo, row.next, category);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -535,10 +682,5 @@ void AutoNumberingDockWidget::resetAutoNum(QComboBox *combo_box, AutoNumCategory
|
|||||||
}
|
}
|
||||||
|
|
||||||
storeContext(combo_box, category, context);
|
storeContext(combo_box, category, context);
|
||||||
|
refreshRow(category);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
class QComboBox;
|
class QComboBox;
|
||||||
class QLineEdit;
|
class QLineEdit;
|
||||||
|
class QSpinBox;
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
class AutoNumberingDockWidget;
|
class AutoNumberingDockWidget;
|
||||||
@@ -66,10 +67,28 @@ class AutoNumberingDockWidget : public QDockWidget
|
|||||||
void on_m_element_value_le_editingFinished();
|
void on_m_element_value_le_editingFinished();
|
||||||
void on_m_folio_value_le_editingFinished();
|
void on_m_folio_value_le_editingFinished();
|
||||||
|
|
||||||
|
void on_m_conductor_increase_sb_valueChanged(int);
|
||||||
|
void on_m_element_increase_sb_valueChanged(int);
|
||||||
|
void on_m_folio_increase_sb_valueChanged(int);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void folioAutoNumChanged(QString);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class AutoNumCategory { Conductor, Element, Folio };
|
enum class AutoNumCategory { Conductor, Element, Folio };
|
||||||
|
|
||||||
|
/// The four widgets that make up one category's row, bundled so
|
||||||
|
/// refreshRow() can be called with just a category instead of
|
||||||
|
/// four pointers that must always be passed in matching sets.
|
||||||
|
struct Row
|
||||||
|
{
|
||||||
|
QComboBox *combo;
|
||||||
|
QLineEdit *value;
|
||||||
|
QSpinBox *increase;
|
||||||
|
QLineEdit *next;
|
||||||
|
};
|
||||||
|
Row rowFor(AutoNumCategory category) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief resetAutoNum
|
@brief resetAutoNum
|
||||||
Reset the numerotation context currently selected in combo_box
|
Reset the numerotation context currently selected in combo_box
|
||||||
@@ -91,6 +110,23 @@ class AutoNumberingDockWidget : public QDockWidget
|
|||||||
void refreshValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category);
|
void refreshValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category);
|
||||||
void applyValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category);
|
void applyValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category);
|
||||||
|
|
||||||
|
/// Refresh/apply the increment spin box the same way.
|
||||||
|
void refreshIncreaseField(QComboBox *combo_box, QSpinBox *increase_sb, AutoNumCategory category);
|
||||||
|
void applyIncreaseField(QComboBox *combo_box, QSpinBox *increase_sb, AutoNumCategory category);
|
||||||
|
|
||||||
|
/// Show what the counter will read after one more step, using
|
||||||
|
/// the same NumerotationContextCommands engine the Suivant
|
||||||
|
/// button in the full configuration dialog already advances
|
||||||
|
/// the whole context with -- so the preview can never disagree
|
||||||
|
/// with what actually happens when the number is next consumed.
|
||||||
|
void refreshNextField(QComboBox *combo_box, QLineEdit *next_edit, AutoNumCategory category);
|
||||||
|
|
||||||
|
/// Refresh a whole row -- value, increment and next-value
|
||||||
|
/// preview -- in one call. Every refresh call site needs the
|
||||||
|
/// increment and preview kept in step with the value now, so
|
||||||
|
/// this replaces refreshValueField() at each of them.
|
||||||
|
void refreshRow(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;
|
||||||
|
|||||||
@@ -15,6 +15,36 @@
|
|||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="dockWidgetContents">
|
<widget class="QWidget" name="dockWidgetContents">
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="3">
|
||||||
|
<widget class="QLabel" name="value_header_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Valeur</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="4">
|
||||||
|
<widget class="QLabel" name="increase_header_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Incrément</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="5">
|
||||||
|
<widget class="QLabel" name="next_header_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Suivant</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item row="3" column="1">
|
<item row="3" column="1">
|
||||||
<widget class="QComboBox" name="m_element_cb"/>
|
<widget class="QComboBox" name="m_element_cb"/>
|
||||||
</item>
|
</item>
|
||||||
@@ -61,6 +91,47 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="2" column="4">
|
||||||
|
<widget class="QSpinBox" name="m_conductor_increase_sb">
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>55</width>
|
||||||
|
<height>16777215</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Incrément : valeur ajoutée au compteur à chaque nouvelle numérotation</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="accelerated">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="5">
|
||||||
|
<widget class="QLineEdit" name="m_conductor_next_le">
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>70</width>
|
||||||
|
<height>16777215</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Prochaine valeur qui sera appliquée avec cet incrément</string>
|
||||||
|
</property>
|
||||||
|
<property name="readOnly">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignCenter</set>
|
||||||
|
</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">
|
||||||
@@ -108,6 +179,47 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="3" column="4">
|
||||||
|
<widget class="QSpinBox" name="m_element_increase_sb">
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>55</width>
|
||||||
|
<height>16777215</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Incrément : valeur ajoutée au compteur à chaque nouvelle numérotation</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="accelerated">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="5">
|
||||||
|
<widget class="QLineEdit" name="m_element_next_le">
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>70</width>
|
||||||
|
<height>16777215</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Prochaine valeur qui sera appliquée avec cet incrément</string>
|
||||||
|
</property>
|
||||||
|
<property name="readOnly">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignCenter</set>
|
||||||
|
</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>
|
||||||
@@ -144,6 +256,47 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="4" column="4">
|
||||||
|
<widget class="QSpinBox" name="m_folio_increase_sb">
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>55</width>
|
||||||
|
<height>16777215</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Incrément : valeur ajoutée au compteur à chaque nouvelle numérotation</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="accelerated">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="5">
|
||||||
|
<widget class="QLineEdit" name="m_folio_next_le">
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>70</width>
|
||||||
|
<height>16777215</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Prochaine valeur qui sera appliquée avec cet incrément</string>
|
||||||
|
</property>
|
||||||
|
<property name="readOnly">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignCenter</set>
|
||||||
|
</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">
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
+19
-1
@@ -67,6 +67,7 @@ Diagram::Diagram(QETProject *project) :
|
|||||||
m_project (project),
|
m_project (project),
|
||||||
use_border_ (true),
|
use_border_ (true),
|
||||||
draw_terminals_ (true),
|
draw_terminals_ (true),
|
||||||
|
draw_terminal_names_ (true),
|
||||||
draw_colored_conductors_ (true),
|
draw_colored_conductors_ (true),
|
||||||
m_event_interface (nullptr),
|
m_event_interface (nullptr),
|
||||||
m_freeze_new_elements (false),
|
m_freeze_new_elements (false),
|
||||||
@@ -2319,6 +2320,7 @@ ExportProperties Diagram::applyProperties(
|
|||||||
old_properties.draw_border = border_and_titleblock.borderIsDisplayed();
|
old_properties.draw_border = border_and_titleblock.borderIsDisplayed();
|
||||||
old_properties.draw_titleblock = border_and_titleblock.titleBlockIsDisplayed();
|
old_properties.draw_titleblock = border_and_titleblock.titleBlockIsDisplayed();
|
||||||
old_properties.draw_terminals = drawTerminals();
|
old_properties.draw_terminals = drawTerminals();
|
||||||
|
old_properties.draw_terminal_names = drawTerminalNames();
|
||||||
old_properties.draw_colored_conductors = drawColoredConductors();
|
old_properties.draw_colored_conductors = drawColoredConductors();
|
||||||
old_properties.exported_area = useBorder() ? QET::BorderArea
|
old_properties.exported_area = useBorder() ? QET::BorderArea
|
||||||
: QET::ElementsArea;
|
: QET::ElementsArea;
|
||||||
@@ -2327,6 +2329,7 @@ ExportProperties Diagram::applyProperties(
|
|||||||
// applique les nouvelles options de rendu
|
// applique les nouvelles options de rendu
|
||||||
setUseBorder (new_properties.exported_area == QET::BorderArea);
|
setUseBorder (new_properties.exported_area == QET::BorderArea);
|
||||||
setDrawTerminals (new_properties.draw_terminals);
|
setDrawTerminals (new_properties.draw_terminals);
|
||||||
|
setDrawTerminalNames (new_properties.draw_terminal_names);
|
||||||
setDrawColoredConductors (new_properties.draw_colored_conductors);
|
setDrawColoredConductors (new_properties.draw_colored_conductors);
|
||||||
setDisplayGrid (new_properties.draw_grid);
|
setDisplayGrid (new_properties.draw_grid);
|
||||||
setDisplayGuides (new_properties.draw_guides);
|
setDisplayGuides (new_properties.draw_guides);
|
||||||
@@ -2397,9 +2400,24 @@ QPointF Diagram::snapToGrid(const QPointF &p)
|
|||||||
\~French true pour afficher les bornes, false sinon
|
\~French true pour afficher les bornes, false sinon
|
||||||
*/
|
*/
|
||||||
void Diagram::setDrawTerminals(bool dt) {
|
void Diagram::setDrawTerminals(bool dt) {
|
||||||
|
draw_terminals_ = dt;
|
||||||
foreach(QGraphicsItem *qgi, items()) {
|
foreach(QGraphicsItem *qgi, items()) {
|
||||||
if (Terminal *t = qgraphicsitem_cast<Terminal *>(qgi)) {
|
if (Terminal *t = qgraphicsitem_cast<Terminal *>(qgi)) {
|
||||||
t -> setVisible(dt);
|
t -> update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Diagram::setDrawTerminalNames
|
||||||
|
Defines whether or not to display the terminal names/labels
|
||||||
|
@param dt : true to display the terminal names, false otherwise
|
||||||
|
*/
|
||||||
|
void Diagram::setDrawTerminalNames(bool dt) {
|
||||||
|
draw_terminal_names_ = dt;
|
||||||
|
foreach(QGraphicsItem *qgi, items()) {
|
||||||
|
if (Terminal *t = qgraphicsitem_cast<Terminal *>(qgi)) {
|
||||||
|
t -> update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ class Diagram : public QGraphicsScene
|
|||||||
bool draw_guides_;
|
bool draw_guides_;
|
||||||
QList<Diagram::Guide> m_guides_list;
|
QList<Diagram::Guide> m_guides_list;
|
||||||
bool draw_terminals_;
|
bool draw_terminals_;
|
||||||
|
bool draw_terminal_names_;
|
||||||
bool draw_colored_conductors_;
|
bool draw_colored_conductors_;
|
||||||
|
|
||||||
QString m_conductors_autonum_name;
|
QString m_conductors_autonum_name;
|
||||||
@@ -226,6 +227,8 @@ class Diagram : public QGraphicsScene
|
|||||||
|
|
||||||
bool drawTerminals() const;
|
bool drawTerminals() const;
|
||||||
void setDrawTerminals(bool);
|
void setDrawTerminals(bool);
|
||||||
|
bool drawTerminalNames() const;
|
||||||
|
void setDrawTerminalNames(bool);
|
||||||
bool drawColoredConductors() const;
|
bool drawColoredConductors() const;
|
||||||
void setDrawColoredConductors(bool);
|
void setDrawColoredConductors(bool);
|
||||||
|
|
||||||
@@ -426,6 +429,15 @@ inline bool Diagram::drawTerminals() const
|
|||||||
return(draw_terminals_);
|
return(draw_terminals_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Diagram::drawTerminalNames
|
||||||
|
@return true if terminal names are rendered, false otherwise
|
||||||
|
*/
|
||||||
|
inline bool Diagram::drawTerminalNames() const
|
||||||
|
{
|
||||||
|
return(draw_terminal_names_);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief Diagram::drawColoredConductors
|
@brief Diagram::drawColoredConductors
|
||||||
@return true if conductors colors are rendered, false otherwise.
|
@return true if conductors colors are rendered, false otherwise.
|
||||||
|
|||||||
@@ -20,11 +20,44 @@
|
|||||||
#include "../conductorautonumerotation.h"
|
#include "../conductorautonumerotation.h"
|
||||||
#include "../diagram.h"
|
#include "../diagram.h"
|
||||||
#include "../undocommand/addgraphicsobjectcommand.h"
|
#include "../undocommand/addgraphicsobjectcommand.h"
|
||||||
|
#include "../undocommand/deleteqgraphicsitemcommand.h"
|
||||||
#include "../factory/elementfactory.h"
|
#include "../factory/elementfactory.h"
|
||||||
#include "../qetapp.h"
|
#include "../qetapp.h"
|
||||||
#include "../qetdiagrameditor.h"
|
#include "../qetdiagrameditor.h"
|
||||||
#include "../qetgraphicsitem/element.h"
|
#include "../qetgraphicsitem/element.h"
|
||||||
#include "../qetgraphicsitem/conductor.h"
|
#include "../qetgraphicsitem/conductor.h"
|
||||||
|
#include "../qetgraphicsitem/terminal.h"
|
||||||
|
#include "../qet.h"
|
||||||
|
#include <QPainterPath>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
/**
|
||||||
|
@brief distanceToSegment
|
||||||
|
@param point : point to measure from
|
||||||
|
@param segment : the finite segment (not the infinite line through it)
|
||||||
|
@return the distance from @a point to the closest point actually on
|
||||||
|
@a segment. Unlike QET::orthogonalProjection(), which reports a hit
|
||||||
|
for any point on the segment's infinite extension, this clamps the
|
||||||
|
projection to the segment itself: a point collinear with a wire but
|
||||||
|
past its actual drawn end is correctly reported as far away, not "on"
|
||||||
|
the wire.
|
||||||
|
*/
|
||||||
|
qreal distanceToSegment(const QPointF &point, const QLineF &segment)
|
||||||
|
{
|
||||||
|
const QPointF a = segment.p1();
|
||||||
|
const QPointF b = segment.p2();
|
||||||
|
const QPointF ab = b - a;
|
||||||
|
const qreal len2 = QPointF::dotProduct(ab, ab);
|
||||||
|
|
||||||
|
if (len2 <= 0.0)
|
||||||
|
return QLineF(point, a).length();
|
||||||
|
|
||||||
|
qreal t = QPointF::dotProduct(point - a, ab) / len2;
|
||||||
|
t = qBound(0.0, t, 1.0);
|
||||||
|
return QLineF(point, a + t * ab).length();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief DiagramEventAddElement::DiagramEventAddElement
|
@brief DiagramEventAddElement::DiagramEventAddElement
|
||||||
@@ -255,9 +288,206 @@ void DiagramEventAddElement::addElement()
|
|||||||
//added to the scene so new conductor are created between terminal of the new element
|
//added to the scene so new conductor are created between terminal of the new element
|
||||||
//and the opposite terminal of m_element.
|
//and the opposite terminal of m_element.
|
||||||
m_diagram->removeItem(m_element);
|
m_diagram->removeItem(m_element);
|
||||||
while (!element -> AlignedFreeTerminals().isEmpty() && m_diagram -> project() -> autoConductor())
|
|
||||||
|
//Auto break conductor: if a terminal of the new element lies on an existing
|
||||||
|
//conductor, break the conductor and reconnect through the new element's terminal.
|
||||||
|
//Track the endpoints of broken conductors so auto-connect doesn't create duplicates.
|
||||||
|
QSet<Terminal *> broken_endpoints;
|
||||||
|
|
||||||
|
if (m_diagram->project()->autoBreakConductor())
|
||||||
{
|
{
|
||||||
QPair <Terminal *, Terminal *> pair = element -> AlignedFreeTerminals().takeFirst();
|
//Track which conductors we already handled for this element
|
||||||
|
QList<Conductor *> conductors_handled;
|
||||||
|
//Track which terminals of the new element are already used (by break or other-connect)
|
||||||
|
QSet<Terminal *> used_terminals;
|
||||||
|
|
||||||
|
foreach (Terminal *t, element->terminals())
|
||||||
|
{
|
||||||
|
//Skip terminals already used by a previous break+reconnect (other_terminal)
|
||||||
|
if (used_terminals.contains(t))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
QPointF t_dock = t->dockConductor();
|
||||||
|
|
||||||
|
//Collect all conductors that pass through this dock point.
|
||||||
|
struct ConductorMatch {
|
||||||
|
Conductor *conductor;
|
||||||
|
Terminal *connect_to; //endpoint "before" the dock point (based on orientation)
|
||||||
|
Terminal *other; //endpoint "after" the dock point
|
||||||
|
};
|
||||||
|
QList<ConductorMatch> all_matches;
|
||||||
|
|
||||||
|
foreach (Conductor *c, m_diagram->conductors())
|
||||||
|
{
|
||||||
|
//Skip conductors we already handled or connected to the new element
|
||||||
|
if (conductors_handled.contains(c) ||
|
||||||
|
c->terminal1->parentElement() == element ||
|
||||||
|
c->terminal2->parentElement() == element)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
//Check if dock point lies on the conductor path. Distance is
|
||||||
|
//measured to the segment itself (clamped), not the infinite
|
||||||
|
//line through it -- see distanceToSegment().
|
||||||
|
QPointF local_dock = c->mapFromScene(t_dock);
|
||||||
|
bool point_on_conductor = false;
|
||||||
|
QPainterPath path = c->path();
|
||||||
|
for (int i = 0; i < path.elementCount() - 1; ++i)
|
||||||
|
{
|
||||||
|
const QPainterPath::Element &e1 = path.elementAt(i);
|
||||||
|
const QPainterPath::Element &e2 = path.elementAt(i + 1);
|
||||||
|
QLineF segment(QPointF(e1.x, e1.y), QPointF(e2.x, e2.y));
|
||||||
|
if (distanceToSegment(local_dock, segment) < 5.0)
|
||||||
|
{
|
||||||
|
point_on_conductor = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!point_on_conductor)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Terminal *c1 = c->terminal1;
|
||||||
|
Terminal *c2 = c->terminal2;
|
||||||
|
QPointF c1_dock = c1->dockConductor();
|
||||||
|
QPointF c2_dock = c2->dockConductor();
|
||||||
|
|
||||||
|
Terminal *connect_to = nullptr;
|
||||||
|
Terminal *other = nullptr;
|
||||||
|
|
||||||
|
switch (t->orientation()) {
|
||||||
|
case Qet::North:
|
||||||
|
if (c1_dock.y() < t_dock.y()) { connect_to = c1; other = c2; }
|
||||||
|
else if (c2_dock.y() < t_dock.y()) { connect_to = c2; other = c1; }
|
||||||
|
break;
|
||||||
|
case Qet::South:
|
||||||
|
if (c1_dock.y() > t_dock.y()) { connect_to = c1; other = c2; }
|
||||||
|
else if (c2_dock.y() > t_dock.y()) { connect_to = c2; other = c1; }
|
||||||
|
break;
|
||||||
|
case Qet::East:
|
||||||
|
if (c1_dock.x() > t_dock.x()) { connect_to = c1; other = c2; }
|
||||||
|
else if (c2_dock.x() > t_dock.x()) { connect_to = c2; other = c1; }
|
||||||
|
break;
|
||||||
|
case Qet::West:
|
||||||
|
if (c1_dock.x() < t_dock.x()) { connect_to = c1; other = c2; }
|
||||||
|
else if (c2_dock.x() < t_dock.x()) { connect_to = c2; other = c1; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connect_to) {
|
||||||
|
qreal d1 = QLineF(t_dock, c1_dock).length();
|
||||||
|
qreal d2 = QLineF(t_dock, c2_dock).length();
|
||||||
|
if (d1 <= d2) { connect_to = c1; other = c2; }
|
||||||
|
else { connect_to = c2; other = c1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
all_matches.append({c, connect_to, other});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (all_matches.isEmpty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
//Group matches by their "other" endpoint and find the largest group.
|
||||||
|
//This ensures that when multiple independent circuits cross at the
|
||||||
|
//same dock point, the group with the most conductors gets priority,
|
||||||
|
//not whichever happens to have the nearest connect_to endpoint.
|
||||||
|
QMap<Terminal *, QList<ConductorMatch>> groups;
|
||||||
|
for (const auto &m : all_matches) {
|
||||||
|
groups[m.other].append(m);
|
||||||
|
}
|
||||||
|
Terminal *best_other = nullptr;
|
||||||
|
int best_count = 0;
|
||||||
|
for (auto it = groups.constBegin(); it != groups.constEnd(); ++it) {
|
||||||
|
if (it.value().size() > best_count) {
|
||||||
|
best_count = it.value().size();
|
||||||
|
best_other = it.key();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Only break conductors in the largest group (same "other" endpoint).
|
||||||
|
//This prevents bridging independent nets: if two unrelated conductors
|
||||||
|
//cross at a dock point, only the larger group gets broken.
|
||||||
|
const QList<ConductorMatch> &matches = groups[best_other];
|
||||||
|
|
||||||
|
//Mark terminal as used
|
||||||
|
used_terminals.insert(t);
|
||||||
|
|
||||||
|
//Delete all matched conductors in one batch
|
||||||
|
DiagramContent content;
|
||||||
|
for (const auto &m : matches) {
|
||||||
|
content.m_other_conductors.append(m.conductor);
|
||||||
|
conductors_handled.append(m.conductor);
|
||||||
|
}
|
||||||
|
new DeleteQGraphicsItemCommand(m_diagram, content, undo_object);
|
||||||
|
|
||||||
|
//Create new conductors from each connect_to endpoint to this terminal.
|
||||||
|
//This creates junctions at the terminal when multiple conductors
|
||||||
|
//from the same circuit pass through the dock point (e.g. left and
|
||||||
|
//right conductors going to the same terminal strip terminal).
|
||||||
|
for (const auto &m : matches) {
|
||||||
|
Conductor *new_c = new Conductor(m.connect_to, t);
|
||||||
|
new AddGraphicsObjectCommand(new_c, m_diagram, QPointF(), undo_object);
|
||||||
|
ConductorAutoNumerotation can(new_c, m_diagram, undo_object);
|
||||||
|
can.numerate();
|
||||||
|
if (m_diagram->freezeNewConductors() || m_diagram->project()->isFreezeNewConductors())
|
||||||
|
new_c->setFreezeLabel(true);
|
||||||
|
|
||||||
|
broken_endpoints.insert(m.connect_to);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Connect the shared "other" endpoint to the nearest free terminal
|
||||||
|
//with matching orientation.
|
||||||
|
QPointF other_dock = best_other->dockConductor();
|
||||||
|
Terminal *other_terminal = nullptr;
|
||||||
|
qreal best_dist = std::numeric_limits<qreal>::max();
|
||||||
|
foreach (Terminal *ot, element->terminals())
|
||||||
|
{
|
||||||
|
if (used_terminals.contains(ot))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
//Check that the other_dock approaches from the correct direction
|
||||||
|
//relative to the candidate terminal's orientation.
|
||||||
|
QPointF ot_dock = ot->dockConductor();
|
||||||
|
bool orientation_ok = false;
|
||||||
|
switch (ot->orientation()) {
|
||||||
|
case Qet::North: orientation_ok = other_dock.y() < ot_dock.y(); break;
|
||||||
|
case Qet::South: orientation_ok = other_dock.y() > ot_dock.y(); break;
|
||||||
|
case Qet::East: orientation_ok = other_dock.x() > ot_dock.x(); break;
|
||||||
|
case Qet::West: orientation_ok = other_dock.x() < ot_dock.x(); break;
|
||||||
|
}
|
||||||
|
if (!orientation_ok)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
qreal dist = QLineF(ot_dock, other_dock).length();
|
||||||
|
if (dist < best_dist) {
|
||||||
|
best_dist = dist;
|
||||||
|
other_terminal = ot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (other_terminal) {
|
||||||
|
Conductor *new_c2 = new Conductor(best_other, other_terminal);
|
||||||
|
new AddGraphicsObjectCommand(new_c2, m_diagram, QPointF(), undo_object);
|
||||||
|
ConductorAutoNumerotation can2(new_c2, m_diagram, undo_object);
|
||||||
|
can2.numerate();
|
||||||
|
if (m_diagram->freezeNewConductors() || m_diagram->project()->isFreezeNewConductors())
|
||||||
|
new_c2->setFreezeLabel(true);
|
||||||
|
|
||||||
|
broken_endpoints.insert(best_other);
|
||||||
|
used_terminals.insert(other_terminal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Auto-connect: collect all aligned pairs first, then filter and process.
|
||||||
|
QList<QPair<Terminal *, Terminal *>> aligned_pairs;
|
||||||
|
if (m_diagram->project()->autoConductor())
|
||||||
|
aligned_pairs = element->AlignedFreeTerminals();
|
||||||
|
|
||||||
|
for (const QPair<Terminal *, Terminal *> &pair : aligned_pairs)
|
||||||
|
{
|
||||||
|
//Skip if the other terminal was an endpoint of a broken conductor
|
||||||
|
if (broken_endpoints.contains(pair.second))
|
||||||
|
continue;
|
||||||
|
|
||||||
Conductor *conductor = new Conductor(pair.first, pair.second);
|
Conductor *conductor = new Conductor(pair.first, pair.second);
|
||||||
new AddGraphicsObjectCommand(conductor, m_diagram, QPointF(), undo_object);
|
new AddGraphicsObjectCommand(conductor, m_diagram, QPointF(), undo_object);
|
||||||
@@ -271,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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "diagrameventaddshape.h"
|
#include "diagrameventaddshape.h"
|
||||||
|
|
||||||
#include "../diagram.h"
|
#include "../diagram.h"
|
||||||
|
#include "../lastusedstyle.h"
|
||||||
#include "../undocommand/addgraphicsobjectcommand.h"
|
#include "../undocommand/addgraphicsobjectcommand.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,6 +78,14 @@ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
|||||||
if (!m_shape_item)
|
if (!m_shape_item)
|
||||||
{
|
{
|
||||||
m_shape_item = new QetShapeItem(pos, pos, m_shape_type);
|
m_shape_item = new QetShapeItem(pos, pos, m_shape_type);
|
||||||
|
//Start from whatever pen/brush was last applied this
|
||||||
|
//session, rather than always the hardcoded default.
|
||||||
|
if (LastUsedStyle::hasShapePen()) {
|
||||||
|
m_shape_item->setPen(LastUsedStyle::shapePen());
|
||||||
|
}
|
||||||
|
if (LastUsedStyle::hasShapeBrush()) {
|
||||||
|
m_shape_item->setBrush(LastUsedStyle::shapeBrush());
|
||||||
|
}
|
||||||
m_diagram->addItem (m_shape_item);
|
m_diagram->addItem (m_shape_item);
|
||||||
event->setAccepted(true);
|
event->setAccepted(true);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include "ElementsCollection/elementcollectionitem.h"
|
#include "ElementsCollection/elementcollectionitem.h"
|
||||||
#include "ElementsCollection/elementscollectionmodel.h"
|
#include "ElementsCollection/elementscollectionmodel.h"
|
||||||
|
#include "ElementsCollection/elementstreeview.h"
|
||||||
#include "qetapp.h"
|
#include "qetapp.h"
|
||||||
#include "qetmessagebox.h"
|
#include "qetmessagebox.h"
|
||||||
#include "qfilenameedit.h"
|
#include "qfilenameedit.h"
|
||||||
@@ -88,7 +89,7 @@ void ElementDialog::setUpWidget()
|
|||||||
layout->addWidget(new QLabel(label_));
|
layout->addWidget(new QLabel(label_));
|
||||||
|
|
||||||
|
|
||||||
m_tree_view = new QTreeView(this);
|
m_tree_view = new ElementsTreeView(this);
|
||||||
|
|
||||||
m_model = new ElementsCollectionModel(m_tree_view);
|
m_model = new ElementsCollectionModel(m_tree_view);
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ ExportProperties::ExportProperties() :
|
|||||||
draw_border(true),
|
draw_border(true),
|
||||||
draw_titleblock(true),
|
draw_titleblock(true),
|
||||||
draw_terminals(false),
|
draw_terminals(false),
|
||||||
|
draw_terminal_names(false),
|
||||||
draw_bg_transparent(false),
|
draw_bg_transparent(false),
|
||||||
draw_colored_conductors(true),
|
draw_colored_conductors(true),
|
||||||
exported_area(QET::BorderArea)
|
exported_area(QET::BorderArea)
|
||||||
@@ -70,6 +71,8 @@ void ExportProperties::toSettings(QSettings &settings,
|
|||||||
draw_titleblock);
|
draw_titleblock);
|
||||||
settings.setValue(prefix % "drawterminals",
|
settings.setValue(prefix % "drawterminals",
|
||||||
draw_terminals);
|
draw_terminals);
|
||||||
|
settings.setValue(prefix % "drawterminalnames",
|
||||||
|
draw_terminal_names);
|
||||||
settings.setValue(prefix % "drawbgtransparent",
|
settings.setValue(prefix % "drawbgtransparent",
|
||||||
draw_bg_transparent);
|
draw_bg_transparent);
|
||||||
settings.setValue(prefix % "drawcoloredconductors",
|
settings.setValue(prefix % "drawcoloredconductors",
|
||||||
@@ -105,6 +108,8 @@ void ExportProperties::fromSettings(QSettings &settings,
|
|||||||
true ).toBool();
|
true ).toBool();
|
||||||
draw_terminals = settings.value(prefix % "drawterminals",
|
draw_terminals = settings.value(prefix % "drawterminals",
|
||||||
false).toBool();
|
false).toBool();
|
||||||
|
draw_terminal_names = settings.value(prefix % "drawterminalnames",
|
||||||
|
false).toBool();
|
||||||
draw_bg_transparent = settings.value(prefix % "drawbgtransparent",
|
draw_bg_transparent = settings.value(prefix % "drawbgtransparent",
|
||||||
false).toBool();
|
false).toBool();
|
||||||
draw_colored_conductors = settings.value(
|
draw_colored_conductors = settings.value(
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class ExportProperties {
|
|||||||
bool draw_border; ///< Whether to render the border (along with rows/columns headers)
|
bool draw_border; ///< Whether to render the border (along with rows/columns headers)
|
||||||
bool draw_titleblock; ///< Whether to render the title block
|
bool draw_titleblock; ///< Whether to render the title block
|
||||||
bool draw_terminals; ///< Whether to render terminals
|
bool draw_terminals; ///< Whether to render terminals
|
||||||
|
bool draw_terminal_names; ///< Whether to render terminal names/labels
|
||||||
bool draw_bg_transparent; ///< Whether to use transparency for SVG-Export
|
bool draw_bg_transparent; ///< Whether to use transparency for SVG-Export
|
||||||
bool draw_colored_conductors; ///< Whether to render conductors colors
|
bool draw_colored_conductors; ///< Whether to render conductors colors
|
||||||
QET::DiagramArea exported_area; ///< Area of diagrams to be rendered
|
QET::DiagramArea exported_area; ///< Area of diagrams to be rendered
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ ExportProperties ExportPropertiesWidget::exportProperties() const
|
|||||||
export_properties.draw_border = draw_border -> isChecked();
|
export_properties.draw_border = draw_border -> isChecked();
|
||||||
export_properties.draw_titleblock = draw_titleblock -> isChecked();
|
export_properties.draw_titleblock = draw_titleblock -> isChecked();
|
||||||
export_properties.draw_terminals = draw_terminals -> isChecked();
|
export_properties.draw_terminals = draw_terminals -> isChecked();
|
||||||
|
export_properties.draw_terminal_names = draw_terminal_names -> isChecked();
|
||||||
export_properties.draw_bg_transparent = draw_bg_transparent -> isChecked();
|
export_properties.draw_bg_transparent = draw_bg_transparent -> isChecked();
|
||||||
export_properties.draw_colored_conductors = draw_colored_conductors -> isChecked();
|
export_properties.draw_colored_conductors = draw_colored_conductors -> isChecked();
|
||||||
export_properties.exported_area = export_border -> isChecked() ? QET::BorderArea : QET::ElementsArea;
|
export_properties.exported_area = export_border -> isChecked() ? QET::BorderArea : QET::ElementsArea;
|
||||||
@@ -85,6 +86,7 @@ void ExportPropertiesWidget::setExportProperties(const ExportProperties &export_
|
|||||||
draw_border -> setChecked(export_properties.draw_border);
|
draw_border -> setChecked(export_properties.draw_border);
|
||||||
draw_titleblock -> setChecked(export_properties.draw_titleblock);
|
draw_titleblock -> setChecked(export_properties.draw_titleblock);
|
||||||
draw_terminals -> setChecked(export_properties.draw_terminals);
|
draw_terminals -> setChecked(export_properties.draw_terminals);
|
||||||
|
draw_terminal_names -> setChecked(export_properties.draw_terminal_names);
|
||||||
draw_bg_transparent -> setChecked(export_properties.draw_bg_transparent);
|
draw_bg_transparent -> setChecked(export_properties.draw_bg_transparent);
|
||||||
draw_colored_conductors -> setChecked(export_properties.draw_colored_conductors);
|
draw_colored_conductors -> setChecked(export_properties.draw_colored_conductors);
|
||||||
|
|
||||||
@@ -206,13 +208,17 @@ void ExportPropertiesWidget::build()
|
|||||||
draw_terminals = new QCheckBox(tr("Dessiner les bornes"), groupbox_options);
|
draw_terminals = new QCheckBox(tr("Dessiner les bornes"), groupbox_options);
|
||||||
optionshlayout -> addWidget(draw_terminals, 2, 1);
|
optionshlayout -> addWidget(draw_terminals, 2, 1);
|
||||||
|
|
||||||
|
// dessiner les noms des bornes
|
||||||
|
draw_terminal_names = new QCheckBox(tr("Dessiner les noms des bornes"), groupbox_options);
|
||||||
|
optionshlayout -> addWidget(draw_terminal_names, 3, 0);
|
||||||
|
|
||||||
// conserver les couleurs des conducteurs
|
// conserver les couleurs des conducteurs
|
||||||
draw_colored_conductors = new QCheckBox(tr("Conserver les couleurs des conducteurs"), groupbox_options);
|
draw_colored_conductors = new QCheckBox(tr("Conserver les couleurs des conducteurs"), groupbox_options);
|
||||||
optionshlayout -> addWidget(draw_colored_conductors, 3, 0);
|
optionshlayout -> addWidget(draw_colored_conductors, 3, 1);
|
||||||
|
|
||||||
// use transparent background for SVG-Export
|
// use transparent background for SVG-Export
|
||||||
draw_bg_transparent = new QCheckBox(tr("SVG: fond transparent"), groupbox_options);
|
draw_bg_transparent = new QCheckBox(tr("SVG: fond transparent"), groupbox_options);
|
||||||
optionshlayout -> addWidget(draw_bg_transparent, 3, 1);
|
optionshlayout -> addWidget(draw_bg_transparent, 4, 0);
|
||||||
|
|
||||||
vboxLayout -> addWidget(groupbox_options);
|
vboxLayout -> addWidget(groupbox_options);
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class ExportPropertiesWidget : public QWidget {
|
|||||||
QCheckBox *draw_border;
|
QCheckBox *draw_border;
|
||||||
QCheckBox *draw_titleblock;
|
QCheckBox *draw_titleblock;
|
||||||
QCheckBox *draw_terminals;
|
QCheckBox *draw_terminals;
|
||||||
|
QCheckBox *draw_terminal_names;
|
||||||
QCheckBox *draw_bg_transparent;
|
QCheckBox *draw_bg_transparent;
|
||||||
QCheckBox *draw_colored_conductors;
|
QCheckBox *draw_colored_conductors;
|
||||||
QRadioButton *export_border;
|
QRadioButton *export_border;
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
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 "lastusedstyle.h"
|
||||||
|
|
||||||
|
QPen LastUsedStyle::m_shape_pen;
|
||||||
|
bool LastUsedStyle::m_has_shape_pen = false;
|
||||||
|
QBrush LastUsedStyle::m_shape_brush;
|
||||||
|
bool LastUsedStyle::m_has_shape_brush = false;
|
||||||
|
QFont LastUsedStyle::m_text_font;
|
||||||
|
bool LastUsedStyle::m_has_text_font = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
@return true if a shape pen was set this session
|
||||||
|
*/
|
||||||
|
bool LastUsedStyle::hasShapePen()
|
||||||
|
{
|
||||||
|
return m_has_shape_pen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@return the last pen applied to a shape this session
|
||||||
|
*/
|
||||||
|
QPen LastUsedStyle::shapePen()
|
||||||
|
{
|
||||||
|
return m_shape_pen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief LastUsedStyle::setShapePen
|
||||||
|
Record @a pen as the last-used shape pen for this session
|
||||||
|
@param pen
|
||||||
|
*/
|
||||||
|
void LastUsedStyle::setShapePen(const QPen &pen)
|
||||||
|
{
|
||||||
|
m_shape_pen = pen;
|
||||||
|
m_has_shape_pen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@return true if a shape brush was set this session
|
||||||
|
*/
|
||||||
|
bool LastUsedStyle::hasShapeBrush()
|
||||||
|
{
|
||||||
|
return m_has_shape_brush;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@return the last brush applied to a shape this session
|
||||||
|
*/
|
||||||
|
QBrush LastUsedStyle::shapeBrush()
|
||||||
|
{
|
||||||
|
return m_shape_brush;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief LastUsedStyle::setShapeBrush
|
||||||
|
Record @a brush as the last-used shape brush for this session
|
||||||
|
@param brush
|
||||||
|
*/
|
||||||
|
void LastUsedStyle::setShapeBrush(const QBrush &brush)
|
||||||
|
{
|
||||||
|
m_shape_brush = brush;
|
||||||
|
m_has_shape_brush = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@return true if a text font was set this session
|
||||||
|
*/
|
||||||
|
bool LastUsedStyle::hasTextFont()
|
||||||
|
{
|
||||||
|
return m_has_text_font;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@return the last font applied to a free text item this session
|
||||||
|
*/
|
||||||
|
QFont LastUsedStyle::textFont()
|
||||||
|
{
|
||||||
|
return m_text_font;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief LastUsedStyle::setTextFont
|
||||||
|
Record @a font as the last-used free text font for this session
|
||||||
|
@param font
|
||||||
|
*/
|
||||||
|
void LastUsedStyle::setTextFont(const QFont &font)
|
||||||
|
{
|
||||||
|
m_text_font = font;
|
||||||
|
m_has_text_font = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
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 LAST_USED_STYLE_H
|
||||||
|
#define LAST_USED_STYLE_H
|
||||||
|
|
||||||
|
#include <QBrush>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QPen>
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief The LastUsedStyle class
|
||||||
|
Session-scoped "last used" style for new shapes and free text created
|
||||||
|
on the diagram canvas: whatever pen/brush/font was last applied through
|
||||||
|
the properties editors becomes the starting point for the next new
|
||||||
|
item of that type, the way most drawing tools behave.
|
||||||
|
|
||||||
|
Deliberately in-memory only, not QSettings-backed: this is a live
|
||||||
|
"what did I just use" value for the current editing session, not an
|
||||||
|
app-wide default (that's already covered by the Preferences dialog's
|
||||||
|
font setting, read as the fallback when nothing has been set yet).
|
||||||
|
*/
|
||||||
|
class LastUsedStyle
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static bool hasShapePen();
|
||||||
|
static QPen shapePen();
|
||||||
|
static void setShapePen(const QPen &pen);
|
||||||
|
|
||||||
|
static bool hasShapeBrush();
|
||||||
|
static QBrush shapeBrush();
|
||||||
|
static void setShapeBrush(const QBrush &brush);
|
||||||
|
|
||||||
|
static bool hasTextFont();
|
||||||
|
static QFont textFont();
|
||||||
|
static void setTextFont(const QFont &font);
|
||||||
|
|
||||||
|
private:
|
||||||
|
LastUsedStyle() = delete;
|
||||||
|
|
||||||
|
static QPen m_shape_pen;
|
||||||
|
static bool m_has_shape_pen;
|
||||||
|
static QBrush m_shape_brush;
|
||||||
|
static bool m_has_shape_brush;
|
||||||
|
static QFont m_text_font;
|
||||||
|
static bool m_has_text_font;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // LAST_USED_STYLE_H
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include "ElementsCollection/elementcollectionitem.h"
|
#include "ElementsCollection/elementcollectionitem.h"
|
||||||
#include "ElementsCollection/elementscollectionmodel.h"
|
#include "ElementsCollection/elementscollectionmodel.h"
|
||||||
|
#include "ElementsCollection/elementstreeview.h"
|
||||||
#include "NameList/ui/namelistwidget.h"
|
#include "NameList/ui/namelistwidget.h"
|
||||||
#include "editor/ui/qetelementeditor.h"
|
#include "editor/ui/qetelementeditor.h"
|
||||||
#include "qetmessagebox.h"
|
#include "qetmessagebox.h"
|
||||||
@@ -85,7 +86,7 @@ QWizardPage *NewElementWizard::buildStep1()
|
|||||||
page -> setSubTitle(tr("Sélectionnez une catégorie dans laquelle enregistrer le nouvel élément.", "wizard page subtitle"));
|
page -> setSubTitle(tr("Sélectionnez une catégorie dans laquelle enregistrer le nouvel élément.", "wizard page subtitle"));
|
||||||
QVBoxLayout *layout = new QVBoxLayout();
|
QVBoxLayout *layout = new QVBoxLayout();
|
||||||
|
|
||||||
m_tree_view = new QTreeView(this);
|
m_tree_view = new ElementsTreeView(this);
|
||||||
|
|
||||||
m_model = new ElementsCollectionModel(m_tree_view);
|
m_model = new ElementsCollectionModel(m_tree_view);
|
||||||
m_model->hideElement();
|
m_model->hideElement();
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ ProjectPrintWindow::ProjectPrintWindow(QETProject *project, QPrinter *printer, Q
|
|||||||
ui->m_draw_border_cb->setChecked(exp.draw_border);
|
ui->m_draw_border_cb->setChecked(exp.draw_border);
|
||||||
ui->m_draw_titleblock_cb->setChecked(exp.draw_titleblock);
|
ui->m_draw_titleblock_cb->setChecked(exp.draw_titleblock);
|
||||||
ui->m_draw_terminal_cb->setChecked(exp.draw_terminals);
|
ui->m_draw_terminal_cb->setChecked(exp.draw_terminals);
|
||||||
|
ui->m_draw_terminal_names_cb->setChecked(exp.draw_terminal_names);
|
||||||
ui->m_keep_conductor_color_cb->setChecked(exp.draw_colored_conductors);
|
ui->m_keep_conductor_color_cb->setChecked(exp.draw_colored_conductors);
|
||||||
|
|
||||||
ui->m_date_cb->blockSignals(true);
|
ui->m_date_cb->blockSignals(true);
|
||||||
@@ -523,6 +524,7 @@ ExportProperties ProjectPrintWindow::exportProperties() const
|
|||||||
exp.draw_border = ui->m_draw_border_cb->isChecked();
|
exp.draw_border = ui->m_draw_border_cb->isChecked();
|
||||||
exp.draw_titleblock = ui->m_draw_titleblock_cb->isChecked();
|
exp.draw_titleblock = ui->m_draw_titleblock_cb->isChecked();
|
||||||
exp.draw_terminals = ui->m_draw_terminal_cb->isChecked();
|
exp.draw_terminals = ui->m_draw_terminal_cb->isChecked();
|
||||||
|
exp.draw_terminal_names = ui->m_draw_terminal_names_cb->isChecked();
|
||||||
exp.draw_colored_conductors = ui->m_keep_conductor_color_cb->isChecked();
|
exp.draw_colored_conductors = ui->m_keep_conductor_color_cb->isChecked();
|
||||||
exp.draw_grid = false;
|
exp.draw_grid = false;
|
||||||
exp.draw_guides = false;
|
exp.draw_guides = false;
|
||||||
@@ -796,6 +798,7 @@ void ProjectPrintWindow::on_m_draw_border_cb_clicked() { m_preview->upd
|
|||||||
void ProjectPrintWindow::on_m_draw_titleblock_cb_clicked() { m_preview->updatePreview(); }
|
void ProjectPrintWindow::on_m_draw_titleblock_cb_clicked() { m_preview->updatePreview(); }
|
||||||
void ProjectPrintWindow::on_m_keep_conductor_color_cb_clicked() { m_preview->updatePreview(); }
|
void ProjectPrintWindow::on_m_keep_conductor_color_cb_clicked() { m_preview->updatePreview(); }
|
||||||
void ProjectPrintWindow::on_m_draw_terminal_cb_clicked() { m_preview->updatePreview(); }
|
void ProjectPrintWindow::on_m_draw_terminal_cb_clicked() { m_preview->updatePreview(); }
|
||||||
|
void ProjectPrintWindow::on_m_draw_terminal_names_cb_clicked() { m_preview->updatePreview(); }
|
||||||
void ProjectPrintWindow::on_m_fit_in_page_cb_clicked() { m_preview->updatePreview(); }
|
void ProjectPrintWindow::on_m_fit_in_page_cb_clicked() { m_preview->updatePreview(); }
|
||||||
void ProjectPrintWindow::on_m_use_full_page_cb_clicked()
|
void ProjectPrintWindow::on_m_use_full_page_cb_clicked()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ class ProjectPrintWindow : public QMainWindow
|
|||||||
void on_m_draw_titleblock_cb_clicked();
|
void on_m_draw_titleblock_cb_clicked();
|
||||||
void on_m_keep_conductor_color_cb_clicked();
|
void on_m_keep_conductor_color_cb_clicked();
|
||||||
void on_m_draw_terminal_cb_clicked();
|
void on_m_draw_terminal_cb_clicked();
|
||||||
|
void on_m_draw_terminal_names_cb_clicked();
|
||||||
void on_m_fit_in_page_cb_clicked();
|
void on_m_fit_in_page_cb_clicked();
|
||||||
void on_m_use_full_page_cb_clicked();
|
void on_m_use_full_page_cb_clicked();
|
||||||
void on_m_zoom_out_action_triggered();
|
void on_m_zoom_out_action_triggered();
|
||||||
|
|||||||
@@ -183,6 +183,16 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="m_draw_terminal_names_cb">
|
||||||
|
<property name="text">
|
||||||
|
<string>Dessiner les noms des bornes</string>
|
||||||
|
</property>
|
||||||
|
<property name="checked">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -2577,6 +2579,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2630,6 +2636,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 *> &,
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ void QETDiagramEditor::setUpElementsPanel()
|
|||||||
connect(pa, &ElementsPanelWidget::requestForProjectClosing, this, qOverload<QETProject*>(&QETDiagramEditor::closeProject));
|
connect(pa, &ElementsPanelWidget::requestForProjectClosing, this, qOverload<QETProject*>(&QETDiagramEditor::closeProject));
|
||||||
connect(pa, SIGNAL(requestForProjectPropertiesEdition (QETProject *)), this, SLOT(editProjectProperties(QETProject *)));
|
connect(pa, SIGNAL(requestForProjectPropertiesEdition (QETProject *)), this, SLOT(editProjectProperties(QETProject *)));
|
||||||
connect(pa, &ElementsPanelWidget::requestForNewDiagram, this, &QETDiagramEditor::addDiagramToProject);
|
connect(pa, &ElementsPanelWidget::requestForNewDiagram, this, &QETDiagramEditor::addDiagramToProject);
|
||||||
|
connect(pa, &ElementsPanelWidget::requestForNewDiagram, this, &QETDiagramEditor::addDiagramToProjectAt);
|
||||||
connect(pa, SIGNAL(requestForDiagramPropertiesEdition (Diagram *)), this, SLOT(editDiagramProperties(Diagram *)));
|
connect(pa, SIGNAL(requestForDiagramPropertiesEdition (Diagram *)), this, SLOT(editDiagramProperties(Diagram *)));
|
||||||
connect(pa, &ElementsPanelWidget::requestForDiagramsDeletion, this, &QETDiagramEditor::removeDiagrams);
|
connect(pa, &ElementsPanelWidget::requestForDiagramsDeletion, this, &QETDiagramEditor::removeDiagrams);
|
||||||
connect(pa, &ElementsPanelWidget::requestForDiagramMoveUp, this, &QETDiagramEditor::moveDiagramUp);
|
connect(pa, &ElementsPanelWidget::requestForDiagramMoveUp, this, &QETDiagramEditor::moveDiagramUp);
|
||||||
@@ -367,6 +368,21 @@ void QETDiagramEditor::setUpActions()
|
|||||||
pv->project()->setAutoConductor(ac);
|
pv->project()->setAutoConductor(ac);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//AutoBreakConductor
|
||||||
|
m_auto_break_conductor = new QAction (QET::Icons::Conductor, tr("Coupure automatique de conducteur(s)","Tool tip of auto break conductor"), this);
|
||||||
|
m_auto_break_conductor->setStatusTip (tr("Couper automatiquement les conducteurs existants lors du placement d'un élément", "Status tip of auto break conductor"));
|
||||||
|
m_auto_break_conductor->setCheckable (true);
|
||||||
|
{
|
||||||
|
QSettings settings;
|
||||||
|
m_auto_break_conductor->setChecked(settings.value("diagrameditor/auto_break_conductor", false).toBool());
|
||||||
|
}
|
||||||
|
connect(m_auto_break_conductor, &QAction::triggered, [this](bool abc) {
|
||||||
|
QSettings settings;
|
||||||
|
settings.setValue("diagrameditor/auto_break_conductor", abc);
|
||||||
|
if (ProjectView *pv = currentProjectView())
|
||||||
|
pv->project()->setAutoBreakConductor(abc);
|
||||||
|
});
|
||||||
|
|
||||||
//Switch background color
|
//Switch background color
|
||||||
m_grey_background = new QAction (QET::Icons::DiagramBg, tr("Couleur de fond blanc/gris","Tool tip of white/grey background button"), this);
|
m_grey_background = new QAction (QET::Icons::DiagramBg, tr("Couleur de fond blanc/gris","Tool tip of white/grey background button"), this);
|
||||||
m_grey_background -> setStatusTip (tr("Affiche la couleur de fond du folio en blanc ou en gris", "Status tip of white/grey background button"));
|
m_grey_background -> setStatusTip (tr("Affiche la couleur de fond du folio en blanc ou en gris", "Status tip of white/grey background button"));
|
||||||
@@ -806,6 +822,7 @@ void QETDiagramEditor::setUpToolBar()
|
|||||||
diagram_tool_bar -> addAction (m_edit_diagram_properties);
|
diagram_tool_bar -> addAction (m_edit_diagram_properties);
|
||||||
diagram_tool_bar -> addAction (m_conductor_reset);
|
diagram_tool_bar -> addAction (m_conductor_reset);
|
||||||
diagram_tool_bar -> addAction (m_auto_conductor);
|
diagram_tool_bar -> addAction (m_auto_conductor);
|
||||||
|
diagram_tool_bar -> addAction (m_auto_break_conductor);
|
||||||
|
|
||||||
m_add_item_tool_bar = new QToolBar(tr("Ajouter"), this);
|
m_add_item_tool_bar = new QToolBar(tr("Ajouter"), this);
|
||||||
m_add_item_tool_bar->setObjectName("adding");
|
m_add_item_tool_bar->setObjectName("adding");
|
||||||
@@ -878,6 +895,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);
|
||||||
@@ -913,6 +932,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());
|
||||||
@@ -1280,6 +1300,12 @@ bool QETDiagramEditor::addProject(QETProject *project, bool update_panel)
|
|||||||
|
|
||||||
undo_group.addStack(project -> undoStack());
|
undo_group.addStack(project -> undoStack());
|
||||||
|
|
||||||
|
connect(project, &QETProject::projectModified, this, [this](QETProject *modified_project, bool) {
|
||||||
|
if (modified_project == currentProject()) {
|
||||||
|
updateWindowModifiedState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
m_element_collection_widget->addProject(project);
|
m_element_collection_widget->addProject(project);
|
||||||
|
|
||||||
// met a jour le panel d'elements
|
// met a jour le panel d'elements
|
||||||
@@ -1889,9 +1915,14 @@ void QETDiagramEditor::slot_updateModeActions()
|
|||||||
{
|
{
|
||||||
m_auto_conductor -> setEnabled (true);
|
m_auto_conductor -> setEnabled (true);
|
||||||
m_auto_conductor -> setChecked (pv -> project() -> autoConductor());
|
m_auto_conductor -> setChecked (pv -> project() -> autoConductor());
|
||||||
|
m_auto_break_conductor -> setEnabled (true);
|
||||||
|
m_auto_break_conductor -> setChecked (pv -> project() -> autoBreakConductor());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
m_auto_conductor -> setDisabled(true);
|
m_auto_conductor -> setDisabled(true);
|
||||||
|
m_auto_break_conductor -> setDisabled(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2267,6 +2298,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.
|
||||||
@@ -2543,6 +2593,7 @@ void QETDiagramEditor::subWindowActivated(QMdiSubWindow *subWindows)
|
|||||||
slot_updateWindowsMenu();
|
slot_updateWindowsMenu();
|
||||||
emit syncElementsPanel();
|
emit syncElementsPanel();
|
||||||
updateUsageTrackersActiveState();
|
updateUsageTrackersActiveState();
|
||||||
|
updateWindowModifiedState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2568,6 +2619,32 @@ void QETDiagramEditor::updateUsageTrackersActiveState()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QETDiagramEditor::updateWindowModifiedState
|
||||||
|
Reflect the currently active project's unsaved-changes state in the
|
||||||
|
main window's title and native "document modified" indicator (e.g.
|
||||||
|
the dot in the close button on macOS). Called whenever the active
|
||||||
|
project changes, or whenever the active project's own modified state
|
||||||
|
changes.
|
||||||
|
|
||||||
|
The window title's "[*]" placeholder is Qt's own convention: combined
|
||||||
|
with setWindowModified(), it lets each platform render the modified
|
||||||
|
indicator its own way (or not at all, on platforms without one)
|
||||||
|
without QET having to draw anything itself.
|
||||||
|
*/
|
||||||
|
void QETDiagramEditor::updateWindowModifiedState()
|
||||||
|
{
|
||||||
|
if (QETProject *project = currentProject()) {
|
||||||
|
setWindowTitle(QString("%1[*] - %2").arg(
|
||||||
|
project->pathNameTitle(),
|
||||||
|
tr("QElectroTech", "window title")));
|
||||||
|
setWindowModified(project->projectOptionsWereModified());
|
||||||
|
} else {
|
||||||
|
setWindowTitle(tr("QElectroTech", "window title"));
|
||||||
|
setWindowModified(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief QETDiagramEditor::selectionChanged
|
@brief QETDiagramEditor::selectionChanged
|
||||||
This slot is called when a diagram selection was changed.
|
This slot is called when a diagram selection was changed.
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ class QETDiagramEditor : public QETMainWindow
|
|||||||
ProjectView *findProject(const QString &) const;
|
ProjectView *findProject(const QString &) const;
|
||||||
QMdiSubWindow *subWindowForWidget(QWidget *) const;
|
QMdiSubWindow *subWindowForWidget(QWidget *) const;
|
||||||
void updateUsageTrackersActiveState();
|
void updateUsageTrackersActiveState();
|
||||||
|
void updateWindowModifiedState();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void syncElementsPanel();
|
void syncElementsPanel();
|
||||||
@@ -137,6 +138,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();
|
||||||
@@ -191,7 +193,7 @@ class QETDiagramEditor : public QETMainWindow
|
|||||||
*redo, ///< Redo the latest cancelled operation
|
*redo, ///< Redo the latest cancelled operation
|
||||||
*m_paste, ///< Paste clipboard content on the current diagram
|
*m_paste, ///< Paste clipboard content on the current diagram
|
||||||
*m_auto_conductor, ///< Enable/Disable the use of auto conductor
|
*m_auto_conductor, ///< Enable/Disable the use of auto conductor
|
||||||
*conductor_default, ///< Show a dialog to edit default conductor properties
|
*m_auto_break_conductor, ///< Enable/Disable the use of auto break conductor
|
||||||
*m_grey_background, ///< Switch the background color in white or grey
|
*m_grey_background, ///< Switch the background color in white or grey
|
||||||
*m_draw_grid, ///< Switch the background grid display or not
|
*m_draw_grid, ///< Switch the background grid display or not
|
||||||
*m_draw_guides = nullptr, ///< Switch the custom guides display or not
|
*m_draw_guides = nullptr, ///< Switch the custom guides display or not
|
||||||
@@ -199,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);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include "../diagram.h"
|
#include "../diagram.h"
|
||||||
#include "../diagramcommands.h"
|
#include "../diagramcommands.h"
|
||||||
|
#include "../lastusedstyle.h"
|
||||||
#include "../qet.h"
|
#include "../qet.h"
|
||||||
#include "../qetapp.h"
|
#include "../qetapp.h"
|
||||||
#include "../utils/qetutils.h"
|
#include "../utils/qetutils.h"
|
||||||
@@ -33,7 +34,10 @@
|
|||||||
IndependentTextItem::IndependentTextItem() :
|
IndependentTextItem::IndependentTextItem() :
|
||||||
DiagramTextItem(nullptr)
|
DiagramTextItem(nullptr)
|
||||||
{
|
{
|
||||||
setFont(QETApp::indiTextsItemFont());
|
//Start from the font last applied to a text item this session,
|
||||||
|
//falling back to the app-wide Preferences default otherwise.
|
||||||
|
setFont(LastUsedStyle::hasTextFont() ? LastUsedStyle::textFont()
|
||||||
|
: QETApp::indiTextsItemFont());
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
setRotation(settings.value("diagrameditor/independent_text_rotation", 0).toInt());
|
setRotation(settings.value("diagrameditor/independent_text_rotation", 0).toInt());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ void Terminal::paint(
|
|||||||
|
|
||||||
// dessin de la borne en rouge
|
// dessin de la borne en rouge
|
||||||
// draw the terminal in red
|
// draw the terminal in red
|
||||||
|
if (!diagram() || diagram()->drawTerminals()) {
|
||||||
t.setColor(Qt::red);
|
t.setColor(Qt::red);
|
||||||
painter -> setPen(t);
|
painter -> setPen(t);
|
||||||
painter -> drawLine(c, e);
|
painter -> drawLine(c, e);
|
||||||
@@ -212,6 +213,7 @@ void Terminal::paint(
|
|||||||
painter -> setRenderHint(QPainter::Antialiasing, true);
|
painter -> setRenderHint(QPainter::Antialiasing, true);
|
||||||
painter -> drawEllipse(QRectF(c.x() - 2.5, c.y() - 2.5, 5.0, 5.0));
|
painter -> drawEllipse(QRectF(c.x() - 2.5, c.y() - 2.5, 5.0, 5.0));
|
||||||
} else painter -> drawPoint(c);
|
} else painter -> drawPoint(c);
|
||||||
|
}
|
||||||
|
|
||||||
//Draw help line if needed,
|
//Draw help line if needed,
|
||||||
if (diagram() && m_draw_help_line)
|
if (diagram() && m_draw_help_line)
|
||||||
@@ -273,9 +275,10 @@ void Terminal::paint(
|
|||||||
m_help_line_a -> setLine(line);
|
m_help_line_a -> setLine(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw label if show_name is enabled
|
// Draw label if show_name is enabled and terminal names are allowed
|
||||||
const QString display_name = name();
|
const QString display_name = name();
|
||||||
if (d->m_show_name && !display_name.isEmpty()) {
|
if (d->m_show_name && !display_name.isEmpty()
|
||||||
|
&& (!diagram() || diagram()->drawTerminalNames())) {
|
||||||
painter->setRenderHint(QPainter::Antialiasing, true);
|
painter->setRenderHint(QPainter::Antialiasing, true);
|
||||||
painter->setRenderHint(QPainter::TextAntialiasing, true);
|
painter->setRenderHint(QPainter::TextAntialiasing, true);
|
||||||
painter->setFont(d->m_label_font);
|
painter->setFont(d->m_label_font);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ m_project_properties_handler{this}
|
|||||||
init();
|
init();
|
||||||
|
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
|
|
||||||
|
//Read auto break conductor default from global settings
|
||||||
|
m_auto_break_conductor = settings.value(QStringLiteral("diagrameditor/auto_break_conductor"), false).toBool();
|
||||||
|
|
||||||
int size = settings.beginReadArray(QStringLiteral("diagrameditor/defaultguides"));
|
int size = settings.beginReadArray(QStringLiteral("diagrameditor/defaultguides"));
|
||||||
for (int i = 0; i < size; ++i) {
|
for (int i = 0; i < size; ++i) {
|
||||||
settings.setArrayIndex(i);
|
settings.setArrayIndex(i);
|
||||||
@@ -927,6 +931,28 @@ void QETProject::setAutoConductor(bool ac)
|
|||||||
m_auto_conductor = ac;
|
m_auto_conductor = ac;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QETProject::autoBreakConductor
|
||||||
|
@return true if use of auto break conductor is authorized.
|
||||||
|
See also Q_PROPERTY autoBreakConductor
|
||||||
|
*/
|
||||||
|
bool QETProject::autoBreakConductor() const
|
||||||
|
{
|
||||||
|
return m_auto_break_conductor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QETProject::setAutoBreakConductor
|
||||||
|
@param abc
|
||||||
|
Enable the use of auto break conductor if true
|
||||||
|
See also Q_PROPERTY autoBreakConductor
|
||||||
|
*/
|
||||||
|
void QETProject::setAutoBreakConductor(bool abc)
|
||||||
|
{
|
||||||
|
if (abc != m_auto_break_conductor)
|
||||||
|
m_auto_break_conductor = abc;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief QETProject::autoFolioNumberingNewFolios
|
@brief QETProject::autoFolioNumberingNewFolios
|
||||||
emit Signal to add new Diagram with autonum
|
emit Signal to add new Diagram with autonum
|
||||||
@@ -1735,6 +1761,7 @@ void QETProject::readDefaultPropertiesXml(QDomDocument &xml_project)
|
|||||||
{
|
{
|
||||||
m_current_conductor_autonum = conds_autonums.attribute(QStringLiteral("current_autonum"));
|
m_current_conductor_autonum = conds_autonums.attribute(QStringLiteral("current_autonum"));
|
||||||
m_freeze_new_conductors = conds_autonums.attribute(QStringLiteral("freeze_new_conductors")) == QLatin1String("true");
|
m_freeze_new_conductors = conds_autonums.attribute(QStringLiteral("freeze_new_conductors")) == QLatin1String("true");
|
||||||
|
m_auto_break_conductor = conds_autonums.attribute(QStringLiteral("auto_break_conductors")) == QLatin1String("true");
|
||||||
for (auto elmt : QET::findInDomElement(conds_autonums, QStringLiteral("conductor_autonum")))
|
for (auto elmt : QET::findInDomElement(conds_autonums, QStringLiteral("conductor_autonum")))
|
||||||
{
|
{
|
||||||
NumerotationContext nc;
|
NumerotationContext nc;
|
||||||
@@ -1861,6 +1888,7 @@ void QETProject::writeDefaultPropertiesXml(QDomElement &xml_element)
|
|||||||
QDomElement conductor_autonums = xml_document.createElement("conductors_autonums");
|
QDomElement conductor_autonums = xml_document.createElement("conductors_autonums");
|
||||||
conductor_autonums.setAttribute("current_autonum", m_current_conductor_autonum);
|
conductor_autonums.setAttribute("current_autonum", m_current_conductor_autonum);
|
||||||
conductor_autonums.setAttribute("freeze_new_conductors", m_freeze_new_conductors ? "true" : "false");
|
conductor_autonums.setAttribute("freeze_new_conductors", m_freeze_new_conductors ? "true" : "false");
|
||||||
|
conductor_autonums.setAttribute("auto_break_conductors", m_auto_break_conductor ? "true" : "false");
|
||||||
foreach (QString key, conductorAutoNum().keys()) {
|
foreach (QString key, conductorAutoNum().keys()) {
|
||||||
QDomElement conductor_autonum = conductorAutoNum(key).toXml(xml_document, "conductor_autonum");
|
QDomElement conductor_autonum = conductorAutoNum(key).toXml(xml_document, "conductor_autonum");
|
||||||
if (key != "" && conductorAutoNumFormula(key) != "") {
|
if (key != "" && conductorAutoNumFormula(key) != "") {
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ class QETProject : public QObject
|
|||||||
};
|
};
|
||||||
|
|
||||||
Q_PROPERTY(bool autoConductor READ autoConductor WRITE setAutoConductor)
|
Q_PROPERTY(bool autoConductor READ autoConductor WRITE setAutoConductor)
|
||||||
|
Q_PROPERTY(bool autoBreakConductor READ autoBreakConductor WRITE setAutoBreakConductor)
|
||||||
|
|
||||||
// constructors, destructor
|
// constructors, destructor
|
||||||
public:
|
public:
|
||||||
@@ -181,9 +182,11 @@ class QETProject : public QObject
|
|||||||
void setFreezeNewConductors(bool);
|
void setFreezeNewConductors(bool);
|
||||||
|
|
||||||
bool autoConductor () const;
|
bool autoConductor () const;
|
||||||
|
bool autoBreakConductor () const;
|
||||||
bool autoElement () const;
|
bool autoElement () const;
|
||||||
bool autoFolio () const;
|
bool autoFolio () const;
|
||||||
void setAutoConductor (bool ac);
|
void setAutoConductor (bool ac);
|
||||||
|
void setAutoBreakConductor (bool abc);
|
||||||
void setAutoElement (bool ae);
|
void setAutoElement (bool ae);
|
||||||
void autoFolioNumberingNewFolios ();
|
void autoFolioNumberingNewFolios ();
|
||||||
void autoFolioNumberingSelectedFolios(int, int, const QString&);
|
void autoFolioNumberingSelectedFolios(int, int, const QString&);
|
||||||
@@ -318,6 +321,7 @@ class QETProject : public QObject
|
|||||||
QHash <QString, NumerotationContext> m_element_autonum; //Title and NumContext hash
|
QHash <QString, NumerotationContext> m_element_autonum; //Title and NumContext hash
|
||||||
QString m_current_element_autonum;
|
QString m_current_element_autonum;
|
||||||
bool m_auto_conductor = true;
|
bool m_auto_conductor = true;
|
||||||
|
bool m_auto_break_conductor = false;
|
||||||
XmlElementCollection *m_elements_collection = nullptr;
|
XmlElementCollection *m_elements_collection = nullptr;
|
||||||
bool m_freeze_new_elements = false;
|
bool m_freeze_new_elements = false;
|
||||||
bool m_freeze_new_conductors = false;
|
bool m_freeze_new_conductors = false;
|
||||||
|
|||||||
@@ -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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user