Merge branch 'master' into master-modernize-signal-slot

This commit is contained in:
Andre Rummler
2026-08-08 23:13:42 +02:00
109 changed files with 35451 additions and 23666 deletions
-1
View File
@@ -11,7 +11,6 @@ jobs:
permissions: permissions:
contents: write contents: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
+28
View File
@@ -145,6 +145,34 @@ else()
) )
endif() endif()
# Optional precompiled headers -- see QET_ENABLE_PCH in
# cmake/developer_options.cmake for what this trades away.
#
# target_precompile_headers() needs CMake 3.16; the project still declares a
# 3.5 minimum, so guard rather than raise it for an opt-in developer feature.
#
# The generator expressions are load-bearing, not decoration: this target also
# compiles the 18 C files of the bundled LZMA decoder
# (sources/import/edz/lzma/*.c), and an unguarded list applies to every
# language in the target, so the Qt headers would reach the C compiler and fail
# with "unknown type name 'namespace'". $<ANGLE-R> is required because a
# literal '>' would terminate the generator expression.
if(QET_ENABLE_PCH)
if(CMAKE_VERSION VERSION_LESS 3.16)
message(WARNING
"QET_ENABLE_PCH needs CMake 3.16 or newer (found ${CMAKE_VERSION}); "
"building without precompiled headers.")
else()
target_precompile_headers(${PROJECT_NAME} PRIVATE
"$<$<COMPILE_LANGUAGE:CXX>:<QtCore/QtCore$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<QtGui/QtGui$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<QtWidgets/QtWidgets$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<QtXml/QtXml$<ANGLE-R>>"
)
message(STATUS "QET_ENABLE_PCH: precompiled headers enabled")
endif()
endif()
target_link_libraries( target_link_libraries(
${PROJECT_NAME} ${PROJECT_NAME}
PUBLIC PUBLIC
View File
+14
View File
@@ -33,3 +33,17 @@ add_definitions(-DQT_MESSAGELOGCONTEXT)
# Build with KF5 # Build with KF5
option(BUILD_WITH_KF5 "Build with KF5" ON) option(BUILD_WITH_KF5 "Build with KF5" ON)
# Precompiled headers for the Qt umbrella headers.
#
# Off by default and intended for local development only. Building QET is
# dominated by re-parsing Qt's headers: a 214-line .cpp expands to ~198,000
# preprocessed lines, and compiling one translation unit costs ~4.1 s of which
# only ~0.35 s is optimisation (-O0 instead of -O3 saves 8%). A PCH caches the
# parsed header state and takes that ~4.1 s down to ~1.2 s.
#
# It is deliberately NOT on by default: a PCH satisfies includes that a source
# file forgot to make itself, so code written with it enabled can fail to
# compile for everyone else. Leaving it off keeps CI and contributors on the
# strict behaviour, and only developers who opt in trade that for the speed.
option(QET_ENABLE_PCH "Use precompiled headers (developer build speed; may mask missing #includes)" OFF)
+16 -1
View File
@@ -116,6 +116,16 @@ set(QET_RES_FILES
set(QET_SRC_FILES set(QET_SRC_FILES
${QET_DIR}/sources/cli_export.cpp ${QET_DIR}/sources/cli_export.cpp
${QET_DIR}/sources/cli_export.h ${QET_DIR}/sources/cli_export.h
${QET_DIR}/sources/logging/crashhandler.cpp
${QET_DIR}/sources/logging/crashhandler.h
${QET_DIR}/sources/logging/eventloopwatchdog.cpp
${QET_DIR}/sources/logging/eventloopwatchdog.h
${QET_DIR}/sources/logging/logring.cpp
${QET_DIR}/sources/logging/logring.h
${QET_DIR}/sources/logging/qetlogger.cpp
${QET_DIR}/sources/logging/qetlogger.h
${QET_DIR}/sources/logging/ui/diagnosticsreportdialog.cpp
${QET_DIR}/sources/logging/ui/diagnosticsreportdialog.h
${QET_DIR}/sources/pdf_links.cpp ${QET_DIR}/sources/pdf_links.cpp
${QET_DIR}/sources/pdf_links.h ${QET_DIR}/sources/pdf_links.h
${QET_DIR}/sources/import/edz/edzarchive.cpp ${QET_DIR}/sources/import/edz/edzarchive.cpp
@@ -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
View File
File diff suppressed because it is too large Load Diff
+1019 -743
View File
File diff suppressed because it is too large Load Diff
+1021 -743
View File
File diff suppressed because it is too large Load Diff
+1019 -743
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1020 -743
View File
File diff suppressed because it is too large Load Diff
+1019 -743
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1021 -743
View File
File diff suppressed because it is too large Load Diff
+1019 -743
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1044 -768
View File
File diff suppressed because it is too large Load Diff
+1026 -748
View File
File diff suppressed because it is too large Load Diff
+1017 -743
View File
File diff suppressed because it is too large Load Diff
+1019 -743
View File
File diff suppressed because it is too large Load Diff
+1017 -743
View File
File diff suppressed because it is too large Load Diff
+1017 -743
View File
File diff suppressed because it is too large Load Diff
+1019 -743
View File
File diff suppressed because it is too large Load Diff
+1019 -743
View File
File diff suppressed because it is too large Load Diff
+1019 -743
View File
File diff suppressed because it is too large Load Diff
+1047 -771
View File
File diff suppressed because it is too large Load Diff
+1021 -743
View File
File diff suppressed because it is too large Load Diff
+1047 -771
View File
File diff suppressed because it is too large Load Diff
+1019 -743
View File
File diff suppressed because it is too large Load Diff
+1050 -772
View File
File diff suppressed because it is too large Load Diff
+1049 -771
View File
File diff suppressed because it is too large Load Diff
+1021 -743
View File
File diff suppressed because it is too large Load Diff
+1049 -771
View File
File diff suppressed because it is too large Load Diff
+1051 -771
View File
File diff suppressed because it is too large Load Diff
+1049 -771
View File
File diff suppressed because it is too large Load Diff
+1019 -743
View File
File diff suppressed because it is too large Load Diff
+1017 -743
View File
File diff suppressed because it is too large Load Diff
+1021 -743
View File
File diff suppressed because it is too large Load Diff
+1017 -743
View File
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -173,7 +173,9 @@ HEADERS += $$files(sources/*.h) \
$$files(sources/qet_elementscaler/*.h) \ $$files(sources/qet_elementscaler/*.h) \
$$files(sources/svg/*.h) \ $$files(sources/svg/*.h) \
$$files(sources/import/edz/*.h) \ $$files(sources/import/edz/*.h) \
$$files(sources/import/edz/lzma/*.h) $$files(sources/import/edz/lzma/*.h) \
$$files(sources/logging/*.h) \
$$files(sources/logging/ui/*.h)
SOURCES += $$files(sources/*.cpp) \ SOURCES += $$files(sources/*.cpp) \
$$files(sources/editor/*.cpp) \ $$files(sources/editor/*.cpp) \
@@ -219,7 +221,10 @@ SOURCES += $$files(sources/*.cpp) \
$$files(sources/qet_elementscaler/*.cpp) \ $$files(sources/qet_elementscaler/*.cpp) \
$$files(sources/svg/*.cpp) \ $$files(sources/svg/*.cpp) \
$$files(sources/import/edz/*.cpp) \ $$files(sources/import/edz/*.cpp) \
$$files(sources/import/edz/lzma/*.c) $$files(sources/import/edz/lzma/*.c) \
$$files(sources/logging/*.cpp) \
$$files(sources/logging/ui/*.cpp)
# Needed for use promote QTreeWidget in terminalstripeditor.ui # Needed for use promote QTreeWidget in terminalstripeditor.ui
INCLUDEPATH += sources/TerminalStrip/ui INCLUDEPATH += sources/TerminalStrip/ui
@@ -22,7 +22,39 @@
#include "../qeticons.h" #include "../qeticons.h"
#include "elementslocation.h" #include "elementslocation.h"
#include <QApplication>
#include <QDir> #include <QDir>
#include <QPainter>
#include <QPixmap>
#include <QStyle>
namespace {
/**
@return the folder icon overlaid with a small warning badge in the
bottom-right corner. Used for a directory whose qet_directory could
not be read (@see FileElementCollectionItem::m_qet_directory_unreadable),
so the problem is visible in the tree itself and not only on hover
via the tooltip. Built once: same folder icon, same badge, every time.
*/
const QIcon &unreadableFolderIcon()
{
static const QIcon icon = []() {
QPixmap pixmap = QET::Icons::Folder.pixmap(16, 16);
const QPixmap badge = QApplication::style()
->standardIcon(QStyle::SP_MessageBoxWarning)
.pixmap(9, 9);
QPainter painter(&pixmap);
painter.drawPixmap(pixmap.width() - badge.width(),
pixmap.height() - badge.height(),
badge);
painter.end();
return QIcon(pixmap);
}();
return icon;
}
}
/** /**
@brief FileElementCollectionItem::FileElementCollectionItem @brief FileElementCollectionItem::FileElementCollectionItem
@@ -136,18 +168,41 @@ 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()))
{ {
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
+45
View File
@@ -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());
}
+5
View File
@@ -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_;
+167 -25
View File
@@ -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">
+26 -2
View File
@@ -24,6 +24,7 @@
#include "qet.h" #include "qet.h"
#include "qetdiagrameditor.h" #include "qetdiagrameditor.h"
#include "ui/potentialselectordialog.h" #include "ui/potentialselectordialog.h"
#include "undocommand/setautonumcontextcommand.h"
/** /**
@brief ConductorAutoNumerotation::ConductorAutoNumerotation @brief ConductorAutoNumerotation::ConductorAutoNumerotation
@@ -156,7 +157,16 @@ void ConductorAutoNumerotation::newProperties(
autonum::setSequential(formula, seq, context, diagram, autoNum_name); autonum::setSequential(formula, seq, context, diagram, autoNum_name);
NumerotationContextCommands ncc (context, diagram); NumerotationContextCommands ncc (context, diagram);
diagram->project()->addConductorAutoNum(autoNum_name, ncc.next()); NumerotationContext new_context = ncc.next();
QETProject *project = diagram->project();
auto *undo = new SetAutoNumContextCommand(
[project](const QString &k, const NumerotationContext &c) {project->addConductorAutoNum(k, c);},
autoNum_name,
context,
new_context);
undo->setText(QObject::tr("Numéroter automatiquement un conducteur", "undo caption"));
diagram->undoStack().push(undo);
} }
/** /**
@@ -245,7 +255,21 @@ void ConductorAutoNumerotation::numerateNewConductor()
autoNum_name); autoNum_name);
NumerotationContextCommands ncc (context, m_diagram); NumerotationContextCommands ncc (context, m_diagram);
m_diagram->project()->addConductorAutoNum(autoNum_name, ncc.next()); NumerotationContext new_context = ncc.next();
QETProject *project = m_diagram->project();
auto setter = [project](const QString &k, const NumerotationContext &c) {project->addConductorAutoNum(k, c);};
if (m_parent_undo)
{
new SetAutoNumContextCommand(setter, autoNum_name, context, new_context, m_parent_undo);
}
else
{
auto *undo = new SetAutoNumContextCommand(setter, autoNum_name, context, new_context);
undo->setText(QObject::tr("Numéroter automatiquement un conducteur", "undo caption"));
m_diagram->undoStack().push(undo);
}
} }
applyText(autonum::AssignVariables::formulaToLabel( applyText(autonum::AssignVariables::formulaToLabel(
+19 -1
View File
@@ -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();
} }
} }
} }
+12
View File
@@ -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.
+244 -9
View File
@@ -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
@@ -248,16 +281,213 @@ void DiagramEventAddElement::addElement()
QUndoCommand *undo_object = new QUndoCommand(tr("Ajouter %1").arg(element->name())); QUndoCommand *undo_object = new QUndoCommand(tr("Ajouter %1").arg(element->name()));
new AddGraphicsObjectCommand(element, m_diagram, m_element -> pos(), undo_object); new AddGraphicsObjectCommand(element, m_diagram, m_element -> pos(), undo_object);
//When we search for free aligned terminal we temporally remove m_element to //When we search for free aligned terminal we temporally remove m_element to
//avoid any interaction with the function Element::AlignedFreeTerminals //avoid any interaction with the function Element::AlignedFreeTerminals
//This is useful when an element has two (or more) terminals on opposite sides, //This is useful when an element has two (or more) terminals on opposite sides,
//because m_element is exactly at the same pos of the new element //because m_element is exactly at the same pos of the new element
//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;
+3 -2
View File
@@ -18,6 +18,7 @@
#include "partplctable.h" #include "partplctable.h"
#include "../../QPropertyUndoCommand/qpropertyundocommand.h" #include "../../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../../qetapp.h"
#include "../../QetGraphicsItemModeler/qetgraphicshandleritem.h" #include "../../QetGraphicsItemModeler/qetgraphicshandleritem.h"
#include "../../QetGraphicsItemModeler/qetgraphicshandlerutility.h" #include "../../QetGraphicsItemModeler/qetgraphicshandlerutility.h"
#include "../../properties/elementdata.h" #include "../../properties/elementdata.h"
@@ -302,7 +303,7 @@ void PartPlcTable::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti
// Draw column headers // Draw column headers
QFont header_font = plc_data.headerFont.family().isEmpty() QFont header_font = plc_data.headerFont.family().isEmpty()
? painter->font() : plc_data.headerFont; ? QETApp::diagramTextsFont() : plc_data.headerFont;
header_font.setBold(true); header_font.setBold(true);
painter->setFont(header_font); painter->setFont(header_font);
@@ -318,7 +319,7 @@ void PartPlcTable::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti
// Draw IO rows // Draw IO rows
QFont cell_font = plc_data.cellFont.family().isEmpty() QFont cell_font = plc_data.cellFont.family().isEmpty()
? painter->font() : plc_data.cellFont; ? QETApp::diagramTextsFont() : plc_data.cellFont;
painter->setFont(cell_font); painter->setFont(cell_font);
int start_idx = block_starts.at(block); int start_idx = block_starts.at(block);
int end_idx = (block + 1 < block_starts.size()) int end_idx = (block + 1 < block_starts.size())
@@ -28,6 +28,8 @@
#include <QSignalBlocker> #include <QSignalBlocker>
#include <QTableWidgetItem> #include <QTableWidgetItem>
#include <QHeaderView> #include <QHeaderView>
#include <QScrollBar>
#include <QWheelEvent>
#include <QTableWidget> #include <QTableWidget>
#include <QCheckBox> #include <QCheckBox>
#include <QGroupBox> #include <QGroupBox>
@@ -41,6 +43,8 @@
#include <QFont> #include <QFont>
#include <QLineEdit> #include <QLineEdit>
#include <QSplitter> #include <QSplitter>
#include <QShortcut>
#include <QMenu>
/** /**
@brief The EditorDelegate class @brief The EditorDelegate class
@@ -666,10 +670,15 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
plc_layout->addLayout(toolbar); plc_layout->addLayout(toolbar);
// Tables side by side: IO table (left) + Terminal table (right) // Tables side by side: IO table (left) + Terminal table (right)
auto *tables_splitter = new QSplitter(Qt::Horizontal, m_plc_gb); // Both share a single vertical scrollbar on the right
auto *tables_container = new QWidget(m_plc_gb);
auto *tables_layout = new QHBoxLayout(tables_container);
tables_layout->setContentsMargins(0, 0, 0, 0);
auto *splitter = new QSplitter(Qt::Horizontal, tables_container);
// IO Table // IO Table
m_plc_table = new QTableWidget(tables_splitter); m_plc_table = new QTableWidget(splitter);
m_plc_table->setColumnCount(5); m_plc_table->setColumnCount(5);
m_plc_table->setHorizontalHeaderLabels({ m_plc_table->setHorizontalHeaderLabels({
tr("Type"), tr("Adresse"), tr("Fonction"), tr("Type"), tr("Adresse"), tr("Fonction"),
@@ -686,10 +695,10 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems); m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems);
m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection); m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_plc_table->setMinimumHeight(200); m_plc_table->setMinimumHeight(200);
tables_splitter->addWidget(m_plc_table); m_plc_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
// Terminal table (per IO: Nb + T1-T4) // Terminal table (per IO: Nb + T1-T4)
m_plc_terminal_table = new QTableWidget(tables_splitter); m_plc_terminal_table = new QTableWidget(splitter);
m_plc_terminal_table->setColumnCount(2); m_plc_terminal_table->setColumnCount(2);
m_plc_terminal_table->setHorizontalHeaderLabels({ m_plc_terminal_table->setHorizontalHeaderLabels({
tr("Nb."), tr("T1") tr("Nb."), tr("T1")
@@ -700,13 +709,61 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
m_plc_terminal_table->setSelectionBehavior(QAbstractItemView::SelectItems); m_plc_terminal_table->setSelectionBehavior(QAbstractItemView::SelectItems);
m_plc_terminal_table->setSelectionMode(QAbstractItemView::ExtendedSelection); m_plc_terminal_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_plc_terminal_table->setMinimumHeight(200); m_plc_terminal_table->setMinimumHeight(200);
tables_splitter->addWidget(m_plc_terminal_table); m_plc_terminal_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
tables_splitter->setStretchFactor(0, 3); // Shared vertical scrollbar
tables_splitter->setStretchFactor(1, 1); m_plc_shared_scrollbar = new QScrollBar(Qt::Vertical, tables_container);
tables_splitter->setSizes({500, 150}); m_plc_shared_scrollbar->setMinimum(0);
plc_layout->addWidget(tables_splitter); splitter->addWidget(m_plc_table);
splitter->addWidget(m_plc_terminal_table);
splitter->setStretchFactor(0, 3);
splitter->setStretchFactor(1, 1);
tables_layout->addWidget(splitter);
tables_layout->addWidget(m_plc_shared_scrollbar);
// Bidirectional sync between tables and shared scrollbar
// Use flag to prevent infinite loops
connect(m_plc_shared_scrollbar, &QScrollBar::valueChanged,
this, [this](int value) {
if (m_plc_scroll_sync) return;
m_plc_scroll_sync = true;
m_plc_table->verticalScrollBar()->setValue(value);
m_plc_terminal_table->verticalScrollBar()->setValue(value);
m_plc_scroll_sync = false;
});
connect(m_plc_table->verticalScrollBar(), &QScrollBar::valueChanged,
this, [this](int value) {
if (m_plc_scroll_sync) return;
m_plc_scroll_sync = true;
m_plc_shared_scrollbar->setValue(value);
m_plc_terminal_table->verticalScrollBar()->setValue(value);
m_plc_scroll_sync = false;
});
connect(m_plc_terminal_table->verticalScrollBar(), &QScrollBar::valueChanged,
this, [this](int value) {
if (m_plc_scroll_sync) return;
m_plc_scroll_sync = true;
m_plc_shared_scrollbar->setValue(value);
m_plc_table->verticalScrollBar()->setValue(value);
m_plc_scroll_sync = false;
});
// Update shared scrollbar range from both tables
auto syncRange = [this]() {
int max = qMax(m_plc_table->verticalScrollBar()->maximum(),
m_plc_terminal_table->verticalScrollBar()->maximum());
m_plc_shared_scrollbar->blockSignals(true);
m_plc_shared_scrollbar->setMaximum(max);
m_plc_shared_scrollbar->blockSignals(false);
};
connect(m_plc_table->verticalScrollBar(), &QScrollBar::rangeChanged,
this, syncRange);
connect(m_plc_terminal_table->verticalScrollBar(), &QScrollBar::rangeChanged,
this, syncRange);
plc_layout->addWidget(tables_container);
// Font settings // Font settings
auto *font_layout = new QHBoxLayout(); auto *font_layout = new QHBoxLayout();
@@ -790,12 +847,25 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
} }
plc_layout->addLayout(col_layout); plc_layout->addLayout(col_layout);
// Add to master group box // Add to master group box - row 4, full width
ui->m_master_gb->layout()->addWidget(m_plc_gb); auto *gl = qobject_cast<QGridLayout*>(ui->m_master_gb->layout());
gl->addWidget(m_plc_gb, 4, 0, 1, 2);
// Connect signals // Connect signals
connect(add_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcAddRow); connect(add_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcAddRow);
connect(remove_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcRemoveRow); connect(remove_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcRemoveRow);
// Ctrl+V shortcut for paste
auto *paste_shortcut = new QShortcut(QKeySequence::Paste, m_plc_table);
connect(paste_shortcut, &QShortcut::activated, this, &ElementPropertiesEditorWidget::plcPasteFromClipboard);
// Context menu for the PLC table
m_plc_table->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_plc_table, &QTableWidget::customContextMenuRequested, this, [this](const QPoint &pos) {
QMenu menu;
menu.addAction(tr("Coller depuis le presse-papiers"), this, &ElementPropertiesEditorWidget::plcPasteFromClipboard);
menu.exec(m_plc_table->mapToGlobal(pos));
});
} }
/** /**
@@ -877,12 +947,12 @@ void ElementPropertiesEditorWidget::populatePlcTable()
m_plc_header_font = plc_data.headerFont; m_plc_header_font = plc_data.headerFont;
m_plc_cell_font = plc_data.cellFont; m_plc_cell_font = plc_data.cellFont;
if (m_plc_header_font.family().isEmpty()) { if (m_plc_header_font.family().isEmpty()) {
m_plc_header_font = QFont(m_plc_table->font()); m_plc_header_font = QETApp::diagramTextsFont();
m_plc_header_font.setBold(true); m_plc_header_font.setBold(true);
m_plc_header_font.setPointSize(8); m_plc_header_font.setPointSize(8);
} }
if (m_plc_cell_font.family().isEmpty()) { if (m_plc_cell_font.family().isEmpty()) {
m_plc_cell_font = QFont(m_plc_table->font()); m_plc_cell_font = QETApp::diagramTextsFont();
m_plc_cell_font.setPointSize(8); m_plc_cell_font.setPointSize(8);
} }
m_plc_header_font_btn->setText(tr("Police des en-têtes: %1 %2pt") m_plc_header_font_btn->setText(tr("Police des en-têtes: %1 %2pt")
@@ -920,6 +990,21 @@ void ElementPropertiesEditorWidget::populatePlcTable()
hdr->moveSection(hdr->visualIndex(logical), visual); hdr->moveSection(hdr->visualIndex(logical), visual);
} }
} }
// Ensure scrollbars stay hidden and sync shared scrollbar range
if (m_plc_table) {
m_plc_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_plc_table->verticalScrollBar()->setVisible(false);
}
if (m_plc_terminal_table) {
m_plc_terminal_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_plc_terminal_table->verticalScrollBar()->setVisible(false);
}
if (m_plc_shared_scrollbar && m_plc_table) {
int max = qMax(m_plc_table->verticalScrollBar()->maximum(),
m_plc_terminal_table->verticalScrollBar()->maximum());
m_plc_shared_scrollbar->setMaximum(max);
}
} }
/** /**
@@ -1158,53 +1243,80 @@ void ElementPropertiesEditorWidget::plcPasteFromClipboard()
return; return;
QStringList lines = clipboard_text.split('\n', Qt::SkipEmptyParts); QStringList lines = clipboard_text.split('\n', Qt::SkipEmptyParts);
if (lines.isEmpty())
return;
int start_row = m_plc_table->rowCount(); bool has_tabs = false;
m_plc_table->setRowCount(start_row + lines.size()); for (const QString &line : lines) {
if (line.contains('\t')) {
for (int i = 0; i < lines.size(); ++i) { has_tabs = true;
QStringList cells = lines.at(i).split('\t'); break;
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 (!has_tabs) {
if (!cells.isEmpty()) { // Vertical paste: values go down the same column
QString type_str = cells.at(0).trimmed(); int target_col = m_plc_table->currentColumn();
int type_idx = -1; if (target_col < 0) target_col = 0;
int target_row = m_plc_table->currentRow();
if (target_row < 0) target_row = 0;
int max_rows = m_plc_table->rowCount();
for (int i = 0; i < lines.size(); ++i) {
int row = target_row + i;
if (row >= max_rows) break;
plcSetCellFromValue(row, target_col, lines.at(i).trimmed());
}
} else {
// Horizontal paste: each line is a separate IO row
int target_row = m_plc_table->currentRow();
if (target_row < 0) target_row = 0;
int max_rows = m_plc_table->rowCount();
for (int i = 0; i < lines.size(); ++i) {
int row = target_row + i;
if (row >= max_rows) break;
QStringList cells = lines.at(i).split('\t');
for (int c = 0; c < cells.size(); ++c) {
if (c > 4) break;
plcSetCellFromValue(row, c, cells.at(c).trimmed());
}
}
}
}
/**
* @brief ElementPropertiesEditorWidget::plcSetCellFromValue
* Set a single table cell value, respecting the column widget type.
*/
void ElementPropertiesEditorWidget::plcSetCellFromValue(int row, int col, const QString &val)
{
if (!m_plc_table || row < 0 || col < 0 || col > 4)
return;
if (col == 0) {
auto *type_cb = qobject_cast<QComboBox*>(m_plc_table->cellWidget(row, col));
if (!type_cb)
return;
if (!val.isEmpty()) {
QStringList plc_types = ElementData::plcIOTypeList();
for (int t = 0; t < plc_types.size(); ++t) { for (int t = 0; t < plc_types.size(); ++t) {
if (plc_types.at(t).compare(type_str, Qt::CaseInsensitive) == 0) { if (plc_types.at(t).compare(val, Qt::CaseInsensitive) == 0) {
type_idx = t; type_cb->setCurrentIndex(t);
break; return;
} }
} }
if (type_idx >= 0)
type_cb->setCurrentIndex(type_idx);
} }
m_plc_table->setCellWidget(row, 0, type_cb); }
else if (col == 4) {
// Address // CrossRef - read-only
m_plc_table->setItem(row, 1, new QTableWidgetItem( auto *item = new QTableWidgetItem(val);
cells.size() > 1 ? cells.at(1).trimmed() : QString())); item->setFlags(item->flags() & ~Qt::ItemIsEditable);
m_plc_table->setItem(row, col, item);
// Function text }
m_plc_table->setItem(row, 2, new QTableWidgetItem( else {
cells.size() > 2 ? cells.at(2).trimmed() : QString())); // Text columns: Address, Function, Comment
m_plc_table->setItem(row, col, new QTableWidgetItem(val));
// Comment
m_plc_table->setItem(row, 3, new QTableWidgetItem(
cells.size() > 3 ? cells.at(3).trimmed() : QString()));
// CrossRef (read-only)
auto *crossref_item = new QTableWidgetItem(
cells.size() > 4 ? cells.at(4).trimmed() : QString());
crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable);
m_plc_table->setItem(row, 4, crossref_item);
} }
} }
@@ -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:
@@ -92,9 +94,11 @@ class ElementPropertiesEditorWidget : public QDialog
QCheckBox *m_plc_show_headers_cb = nullptr; QCheckBox *m_plc_show_headers_cb = nullptr;
QFont m_plc_header_font; QFont m_plc_header_font;
QFont m_plc_cell_font; QFont m_plc_cell_font;
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,7 +39,20 @@
<item> <item>
<widget class="QComboBox" name="m_base_type_cb"/> <widget class="QComboBox" name="m_base_type_cb"/>
</item> </item>
</layout> <item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item> </item>
<item> <item>
<widget class="QGroupBox" name="m_slave_gb"> <widget class="QGroupBox" name="m_slave_gb">
@@ -93,17 +106,34 @@
<property name="title"> <property name="title">
<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">
<widget class="QLabel" name="label_5"> <layout class="QHBoxLayout" name="type_concret_layout">
<property name="text"> <item>
<string>Type concret</string> <widget class="QLabel" name="label_5">
</property> <property name="text">
</widget> <string>Type concret</string>
</item> </property>
<item row="0" column="1"> </widget>
<widget class="QComboBox" name="m_master_type_cb"/> </item>
</item> <item>
<spacer name="type_concret_spacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QComboBox" name="m_master_type_cb"/>
</item>
</layout>
</item>
<item row="1" column="0"> <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,20 +232,7 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item> </layout>
<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>
</widget> </widget>
<widget class="QWidget" name="Informations"> <widget class="QWidget" name="Informations">
<attribute name="title"> <attribute name="title">
+2 -1
View File
@@ -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);
+34
View File
@@ -61,6 +61,8 @@ ElementsPanelWidget::ElementsPanelWidget(QWidget *parent) : QWidget(parent) {
prj_edit_prop = new QAction(QET::Icons::DialogInformation, tr("Propriétés du projet"), this); prj_edit_prop = new QAction(QET::Icons::DialogInformation, tr("Propriétés du projet"), this);
prj_prop_diagram = new QAction(QET::Icons::DialogInformation, tr("Propriétés du folio"), this); prj_prop_diagram = new QAction(QET::Icons::DialogInformation, tr("Propriétés du folio"), this);
prj_add_diagram = new QAction(QET::Icons::DiagramAdd, tr("Ajouter un folio"), this); prj_add_diagram = new QAction(QET::Icons::DiagramAdd, tr("Ajouter un folio"), this);
prj_insert_diagram_above = new QAction(QET::Icons::DiagramAdd, tr("Insérer un folio au-dessus"), this);
prj_insert_diagram_below = new QAction(QET::Icons::DiagramAdd, tr("Insérer un folio en dessous"), this);
prj_duplicate_diagram = new QAction(QET::Icons::IC_CopyFile, tr("Copier et coller"), this); prj_duplicate_diagram = new QAction(QET::Icons::IC_CopyFile, tr("Copier et coller"), this);
prj_del_diagram = new QAction(QET::Icons::DiagramDelete, tr("Supprimer ce folio"), this); prj_del_diagram = new QAction(QET::Icons::DiagramDelete, tr("Supprimer ce folio"), this);
prj_move_diagram_up = new QAction(QET::Icons::GoUp, tr("Remonter ce folio"), this); prj_move_diagram_up = new QAction(QET::Icons::GoUp, tr("Remonter ce folio"), this);
@@ -101,6 +103,8 @@ ElementsPanelWidget::ElementsPanelWidget(QWidget *parent) : QWidget(parent) {
connect(prj_edit_prop, SIGNAL(triggered()), this, SLOT(editProjectProperties())); connect(prj_edit_prop, SIGNAL(triggered()), this, SLOT(editProjectProperties()));
connect(prj_prop_diagram, SIGNAL(triggered()), this, SLOT(editDiagramProperties())); connect(prj_prop_diagram, SIGNAL(triggered()), this, SLOT(editDiagramProperties()));
connect(prj_add_diagram, SIGNAL(triggered()), this, SLOT(newDiagram())); connect(prj_add_diagram, SIGNAL(triggered()), this, SLOT(newDiagram()));
connect(prj_insert_diagram_above, SIGNAL(triggered()), this, SLOT(insertDiagramAbove()));
connect(prj_insert_diagram_below, SIGNAL(triggered()), this, SLOT(insertDiagramBelow()));
connect(prj_del_diagram, SIGNAL(triggered()), this, SLOT(deleteDiagram())); connect(prj_del_diagram, SIGNAL(triggered()), this, SLOT(deleteDiagram()));
connect(prj_duplicate_diagram, SIGNAL(triggered()), this, SLOT(duplicateDiagram())); connect(prj_duplicate_diagram, SIGNAL(triggered()), this, SLOT(duplicateDiagram()));
connect(prj_move_diagram_up, SIGNAL(triggered()), this, SLOT(moveDiagramUp())); connect(prj_move_diagram_up, SIGNAL(triggered()), this, SLOT(moveDiagramUp()));
@@ -245,6 +249,32 @@ void ElementsPanelWidget::newDiagram()
} }
} }
/**
@brief ElementsPanelWidget::insertDiagramAbove
Emit requestForNewDiagramAt with the position of the currently
selected diagram, inserting the new folio right before it.
*/
void ElementsPanelWidget::insertDiagramAbove()
{
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
QETProject *project = selected_diagram->project();
emit(requestForNewDiagramAt(project, project->folioIndex(selected_diagram)));
}
}
/**
@brief ElementsPanelWidget::insertDiagramBelow
Emit requestForNewDiagramAt with the position right after the
currently selected diagram, inserting the new folio right after it.
*/
void ElementsPanelWidget::insertDiagramBelow()
{
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
QETProject *project = selected_diagram->project();
emit(requestForNewDiagramAt(project, project->folioIndex(selected_diagram) + 1));
}
}
/** /**
* Emet le signal requestForDiagramsDeletion avec les schemas selectionnes * Emet le signal requestForDiagramsDeletion avec les schemas selectionnes
*/ */
@@ -451,6 +481,8 @@ void ElementsPanelWidget::updateButtons()
prj_del_diagram -> setEnabled(is_writable); prj_del_diagram -> setEnabled(is_writable);
prj_duplicate_diagram -> setEnabled(is_writable); prj_duplicate_diagram -> setEnabled(is_writable);
prj_insert_diagram_above -> setEnabled(is_writable);
prj_insert_diagram_below -> setEnabled(is_writable);
prj_move_diagram_up -> setEnabled(is_writable && min_position > 0); prj_move_diagram_up -> setEnabled(is_writable && min_position > 0);
prj_move_diagram_down -> setEnabled(is_writable && max_position < project_diagrams_count - 1); prj_move_diagram_down -> setEnabled(is_writable && max_position < project_diagrams_count - 1);
prj_move_diagram_top -> setEnabled(is_writable && min_position > 0); prj_move_diagram_top -> setEnabled(is_writable && min_position > 0);
@@ -504,6 +536,8 @@ void ElementsPanelWidget::handleContextMenu(const QPoint &pos) {
break; break;
case QET::Diagram: case QET::Diagram:
context_menu -> addAction(prj_prop_diagram); context_menu -> addAction(prj_prop_diagram);
context_menu -> addAction(prj_insert_diagram_above);
context_menu -> addAction(prj_insert_diagram_below);
context_menu -> addAction(prj_del_diagram); context_menu -> addAction(prj_del_diagram);
context_menu -> addAction(prj_duplicate_diagram); context_menu -> addAction(prj_duplicate_diagram);
context_menu -> addAction(prj_move_diagram_top); context_menu -> addAction(prj_move_diagram_top);
+5
View File
@@ -46,6 +46,8 @@ class ElementsPanelWidget : public QWidget {
*prj_edit_prop, *prj_edit_prop,
*prj_prop_diagram, *prj_prop_diagram,
*prj_add_diagram, *prj_add_diagram,
*prj_insert_diagram_above,
*prj_insert_diagram_below,
*prj_del_diagram, *prj_del_diagram,
*prj_duplicate_diagram, *prj_duplicate_diagram,
*prj_move_diagram_up, *prj_move_diagram_up,
@@ -66,6 +68,7 @@ class ElementsPanelWidget : public QWidget {
signals: signals:
void requestForProject(QETProject *); void requestForProject(QETProject *);
void requestForNewDiagram(QETProject *); void requestForNewDiagram(QETProject *);
void requestForNewDiagramAt(QETProject *, int);
void requestForProjectClosing(QETProject *); void requestForProjectClosing(QETProject *);
void requestForProjectPropertiesEdition(QETProject *); void requestForProjectPropertiesEdition(QETProject *);
void requestForDiagramPropertiesEdition(Diagram *); void requestForDiagramPropertiesEdition(Diagram *);
@@ -88,6 +91,8 @@ class ElementsPanelWidget : public QWidget {
void editProjectProperties(); void editProjectProperties();
void editDiagramProperties(); void editDiagramProperties();
void newDiagram(); void newDiagram();
void insertDiagramAbove();
void insertDiagramBelow();
void deleteDiagram(); void deleteDiagram();
void duplicateDiagram(); void duplicateDiagram();
void moveDiagramUp(); void moveDiagramUp();
+5
View File
@@ -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(
+1
View File
@@ -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
+8 -2
View File
@@ -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);
+1
View File
@@ -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;
+106
View File
@@ -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;
}
+63
View File
@@ -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
+171
View File
@@ -0,0 +1,171 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "crashhandler.h"
#include "logring.h"
#include "../qetversion.h"
#include <QByteArray>
#include <QSysInfo>
#include <atomic>
#include <cstring>
#ifdef Q_OS_WIN
#include <fcntl.h>
#include <io.h>
#include <share.h>
#include <sys/stat.h>
#include <windows.h>
#else
#include <cerrno>
#include <csignal>
#include <fcntl.h>
#include <unistd.h>
#endif
namespace {
// Everything the handler touches is preallocated here and filled in by
// install() (normal context, runs once at startup) -- nothing under the
// actual signal/exception path may allocate or touch QString/Qt.
const LogRing *g_ring = nullptr;
char g_dump_path[1024] = {};
char g_header[1024] = {};
int g_header_len = 0;
// Guards against two threads crashing at once, or the handler itself
// faulting while dumping: only the first crash writes a dump. See
// crashhandler.h invariant 4.
std::atomic<bool> g_already_dumped{false};
#ifndef Q_OS_WIN
// A stack-overflow SIGSEGV leaves no usable stack for a handler to run
// on at all, hence the alternate signal stack (invariant: sized well
// above any known SIGSTKSZ so this doesn't depend on
// sysconf(_SC_SIGSTKSZ), which some libc versions require at runtime
// rather than offering as a compile-time constant).
char g_altstack[65536];
const int kHandledSignals[] = {SIGSEGV, SIGABRT, SIGBUS, SIGFPE, SIGILL};
void restoreDefaultAndReraise(int sig)
{
struct sigaction sa {};
sa.sa_handler = SIG_DFL;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(sig, &sa, nullptr);
raise(sig);
}
void signalHandler(int sig)
{
if (g_already_dumped.exchange(true, std::memory_order_acq_rel)) {
// Not the first crash (concurrent fault on another thread, or
// this handler faulting while dumping): skip straight to
// restore-and-re-raise rather than risk a second, interleaved
// write to the same file.
restoreDefaultAndReraise(sig);
return;
}
// open/write/close are all on the POSIX async-signal-safe function
// list; nothing else is called here.
const int fd = ::open(g_dump_path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (fd >= 0) {
if (g_header_len > 0) {
::write(fd, g_header, static_cast<size_t>(g_header_len));
}
if (g_ring) {
g_ring->dumpToFd(fd);
}
::close(fd);
}
restoreDefaultAndReraise(sig);
}
#else // Q_OS_WIN
LONG WINAPI windowsExceptionFilter(EXCEPTION_POINTERS *)
{
bool expected = false;
if (!g_already_dumped.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
return EXCEPTION_CONTINUE_SEARCH;
}
int fd = -1;
errno_t err = _sopen_s(&fd, g_dump_path,
_O_WRONLY | _O_CREAT | _O_TRUNC | _O_BINARY,
_SH_DENYWR, _S_IREAD | _S_IWRITE);
if (err == 0 && fd >= 0) {
if (g_header_len > 0) {
_write(fd, g_header, g_header_len);
}
if (g_ring) {
g_ring->dumpToFd(fd);
}
_close(fd);
}
// Do not suppress Windows Error Reporting / an attached debugger --
// same invariant as re-raising on POSIX (see crashhandler.h,
// invariant 3).
return EXCEPTION_CONTINUE_SEARCH;
}
#endif
} // namespace
void CrashHandler::install(const LogRing *ring, const QString &dump_path)
{
g_ring = ring;
const QByteArray path_utf8 = dump_path.toUtf8();
std::strncpy(g_dump_path, path_utf8.constData(), sizeof(g_dump_path) - 1);
const QByteArray header = QByteArray("QET crash dump\n")
+ "Version: " + QetVersion::displayedVersion().toUtf8() + "\n"
+ "Git: " GIT_COMMIT_SHA "\n"
+ "OS: " + QSysInfo::prettyProductName().toUtf8() + " (" + QSysInfo::currentCpuArchitecture().toUtf8() + ")\n"
+ "Qt: " QT_VERSION_STR "\n"
+ "---\n";
g_header_len = qMin<int>(header.size(), static_cast<int>(sizeof(g_header)) - 1);
std::memcpy(g_header, header.constData(), static_cast<size_t>(g_header_len));
#ifdef Q_OS_WIN
SetUnhandledExceptionFilter(windowsExceptionFilter);
#else
stack_t ss;
ss.ss_sp = g_altstack;
ss.ss_size = sizeof(g_altstack);
ss.ss_flags = 0;
sigaltstack(&ss, nullptr);
struct sigaction sa {};
sa.sa_handler = signalHandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_ONSTACK;
for (int sig : kHandledSignals) {
sigaction(sig, &sa, nullptr);
}
#endif
}
+85
View File
@@ -0,0 +1,85 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CRASHHANDLER_H
#define CRASHHANDLER_H
#include <QString>
class LogRing;
/**
@brief The CrashHandler class
Discussion #644, step 4: on a fatal crash, flush the in-memory
LogRing to a fixed file before the process dies, so the last N log
lines leading up to the crash survive it -- today they only exist in
memory and are lost with the process.
This is the highest-risk piece of the whole logging rework (the
discussion's own words: "lands last, behind its own switch"), so its
invariants are worth restating plainly:
1. The handler must never block. It takes no locks -- LogRing itself
is lock-free for exactly this reason (see logring.h). A handler
that can hang is worse than no handler: it turns a clean crash
(which at least produces a core dump) into a hung process that has
to be force-killed, producing neither a core dump nor a ring dump.
2. The handler must never allocate. Under heap corruption -- a
plausible *cause* of the very crash being handled -- malloc may
itself deadlock or fault. Every buffer this code touches at crash
time (the dump path, the header, the ring's own storage) is
preallocated by install(), which runs once at startup in normal
(non-signal) context.
3. The handler must not swallow the crash. After writing the dump it
restores the default disposition for the signal and re-raises, so
the OS still produces a core dump (POSIX) / Windows Error
Reporting still sees the exception. A handler that "fixed" the
crash by not re-raising would destroy the post-mortem evidence a
core dump provides.
4. Only the *first* crash writes a dump. An atomic test-and-set
guards against two threads faulting simultaneously (or the handler
itself faulting while dumping) producing an interleaved or
truncated file; every crash after the first goes straight to
restore-and-re-raise.
Tested in this environment: POSIX/Linux only (sigaction, sigaltstack,
SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL). The Windows path
(SetUnhandledExceptionFilter) and macOS-specific behaviour (signal
handling itself is POSIX and shares the Linux code path, but sandbox
profiles can affect where the dump file may be written) are
implemented per the discussion's guidance but could not be exercised
here -- there is no Windows or macOS build available in this sandbox.
Please sanity-check both before relying on them in the field.
*/
class CrashHandler
{
public:
/// Installs the crash handler. Must be called from normal
/// (non-signal) startup code, after the LogRing it will dump
/// exists, and only once. `ring` must outlive the process (in
/// practice: the LogRing owned by QetLogger's function-local
/// static instance, which is never destroyed before exit).
/// `dump_path` is resolved and copied into a fixed-size internal
/// buffer here; nothing under the actual signal/exception path
/// touches QString.
static void install(const LogRing *ring, const QString &dump_path);
private:
CrashHandler() = delete;
};
#endif // CRASHHANDLER_H
+58
View File
@@ -0,0 +1,58 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "eventloopwatchdog.h"
#include <QDebug>
#include <QProcessEnvironment>
EventLoopWatchdog::EventLoopWatchdog(QObject *parent) :
QObject(parent)
{
m_disabled = QProcessEnvironment::systemEnvironment()
.value(QStringLiteral("QET_WATCHDOG_DISABLE")) == QStringLiteral("1");
// Precise, not the default Coarse: Coarse explicitly trades timing
// accuracy for power/scheduling efficiency (platform-dependent, but
// commonly +/- a double-digit percentage), which would show up as
// noise indistinguishable from a real stall in exactly the
// measurement this class exists to make trustworthy.
m_timer.setTimerType(Qt::PreciseTimer);
connect(&m_timer, &QTimer::timeout, this, &EventLoopWatchdog::tick);
}
void EventLoopWatchdog::start()
{
if (m_disabled)
return;
m_elapsed.start();
m_timer.start(kTickIntervalMs);
}
void EventLoopWatchdog::tick()
{
// restart() returns the elapsed time and resets the clock in one
// call, so this tick's own cost is never counted against the next.
const qint64 actual_ms = m_elapsed.restart();
if (actual_ms > kStallThresholdMs) {
qWarning() << "EventLoopWatchdog: main thread stalled for"
<< actual_ms << "ms (expected a tick every"
<< kTickIntervalMs << "ms)";
}
}
+92
View File
@@ -0,0 +1,92 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EVENTLOOPWATCHDOG_H
#define EVENTLOOPWATCHDOG_H
#include <QElapsedTimer>
#include <QObject>
#include <QTimer>
/**
@brief The EventLoopWatchdog class
Detects when the main (GUI) thread's event loop goes unresponsive --
QetLogger (discussion #644) can only see what an explicit qDebug()/
qInfo()/qWarning() call already decided to report, and most of a
session (painting, dragging, a slow synchronous operation) produces
no log output at all, so a silent multi-second gap in the log is
indistinguishable from the user simply not doing anything.
This closes that gap the direct way: a repeating QTimer::PreciseTimer
ticks on a short, fixed interval; each tick measures the *actual*
wall-clock time elapsed since the previous one via QElapsedTimer
(monotonic -- unaffected by system clock/NTP adjustments, unlike
QDateTime). Qt does not queue up missed fires for a normal repeating
timer, so if the event loop is blocked for 600ms, the timer fires
once as soon as the loop frees up, with ~600ms measured since the
last tick -- that gap *is* the stall, measured at its source rather
than inferred from log silence.
Only fires a qWarning() (and so only touches the log at all) when a
tick is late by more than kStallThresholdMs, to stay within the
spirit of QetLogger's bounded-log design (see its class comment) --
a healthy session should produce zero output from this class. This
tells you *that* a stall happened and *how long* it was, not what
caused it; pair a reported timestamp with `docker exec`+gdb the way
the CLI hang (PR #661) was diagnosed to go from "it lagged" to a
root cause.
Escape hatch: if QET_WATCHDOG_DISABLE=1 is set in the environment at
construction time, start() does nothing.
*/
class EventLoopWatchdog : public QObject
{
Q_OBJECT
public:
/// How often the watchdog checks in. Small enough to bound the
/// measurement's own granularity, large enough that the tick
/// itself is negligible overhead on the event loop it's watching.
static constexpr int kTickIntervalMs = 50;
/// A tick arriving later than this many ms after the previous one
/// is logged as a stall. Comfortably above kTickIntervalMs so
/// ordinary OS scheduling noise doesn't produce a warning on every
/// tick, and in the range a user would actually notice as lag.
static constexpr int kStallThresholdMs = 200;
explicit EventLoopWatchdog(QObject *parent = nullptr);
/// Starts ticking. Must be called from the main thread, after the
/// event loop it watches is about to run (i.e. immediately before
/// QApplication::exec()) -- constructing this class earlier is
/// harmless, but start() before there is an event loop to tick
/// against would just measure the time until app.exec() is
/// reached. No-op if QET_WATCHDOG_DISABLE=1 was set at
/// construction time.
void start();
private slots:
void tick();
private:
QTimer m_timer;
QElapsedTimer m_elapsed;
bool m_disabled = false;
};
#endif // EVENTLOOPWATCHDOG_H
+152
View File
@@ -0,0 +1,152 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "logring.h"
#include <cstring>
#ifdef Q_OS_WIN
#include <io.h>
#else
#include <cerrno>
#include <unistd.h>
#endif
static_assert(std::atomic<int>::is_always_lock_free,
"LogRing::Entry::length must be a lock-free atomic<int> -- "
"dumpToFd() reads it from a signal handler and must never block.");
namespace {
/**
@brief writeAllSignalSafe
Loops until length bytes have been written to fd or an unrecoverable
error occurs. write(2) may write fewer bytes than requested and may
return EINTR -- both are *more* likely from inside a signal handler
than in normal code, so a single write() call is not enough here.
Async-signal-safe: only calls write(2)/errno, nothing else.
*/
void writeAllSignalSafe(int fd, const char *data, int length) noexcept
{
int remaining = length;
const char *p = data;
while (remaining > 0) {
#ifdef Q_OS_WIN
const int n = _write(fd, p, static_cast<unsigned int>(remaining));
if (n <= 0) {
return;
}
#else
const ssize_t n = ::write(fd, p, static_cast<size_t>(remaining));
if (n < 0) {
if (errno == EINTR) {
continue;
}
return; // unrecoverable -- give up silently, never block/throw
}
if (n == 0) {
return;
}
#endif
p += n;
remaining -= static_cast<int>(n);
}
}
} // namespace
LogRing::LogRing() :
// The sized constructor value-initialises each Entry in place; unlike
// resize(), it doesn't require Entry to be move/copy-constructible,
// which std::atomic<int> deliberately never is. The only allocation
// this class ever does.
m_entries(kCapacityEntries)
{
}
void LogRing::append(const QByteArray &line) noexcept
{
static const char kMarker[] = "...[ring-truncated]\n";
const int marker_len = static_cast<int>(sizeof(kMarker)) - 1;
const quint64 idx = m_write_cursor.fetch_add(1, std::memory_order_relaxed);
Entry &slot = m_entries[static_cast<size_t>(idx % static_cast<quint64>(kCapacityEntries))];
// Zero the length first so a concurrent reader landing on this exact
// slot mid-copy sees "not ready" rather than the previous lap's
// (now-being-overwritten) content at a stale length.
slot.length.store(0, std::memory_order_relaxed);
int len;
if (line.size() < kEntryBytes) {
std::memcpy(slot.data, line.constData(), static_cast<size_t>(line.size()));
len = line.size();
} else {
const int keep = kEntryBytes - marker_len;
std::memcpy(slot.data, line.constData(), static_cast<size_t>(keep));
std::memcpy(slot.data + keep, kMarker, static_cast<size_t>(marker_len));
len = kEntryBytes;
}
slot.length.store(len, std::memory_order_release);
}
QVector<QByteArray> LogRing::snapshot() const
{
const quint64 cursor = m_write_cursor.load(std::memory_order_acquire);
const quint64 cap = static_cast<quint64>(kCapacityEntries);
const quint64 count = (cursor < cap) ? cursor : cap;
const quint64 start = (cursor < cap) ? 0 : (cursor - cap);
QVector<QByteArray> result;
result.reserve(static_cast<int>(count));
for (quint64 i = 0; i < count; ++i) {
const Entry &slot = m_entries[static_cast<size_t>((start + i) % cap)];
const int len = slot.length.load(std::memory_order_acquire);
if (len > 0) {
result.append(QByteArray(slot.data, len));
}
}
return result;
}
void LogRing::dumpToFd(int fd) const noexcept
{
const quint64 cursor = m_write_cursor.load(std::memory_order_acquire);
const quint64 cap = static_cast<quint64>(kCapacityEntries);
const quint64 count = (cursor < cap) ? cursor : cap;
const quint64 start = (cursor < cap) ? 0 : (cursor - cap);
for (quint64 i = 0; i < count; ++i) {
const Entry &slot = m_entries[static_cast<size_t>((start + i) % cap)];
const int len = slot.length.load(std::memory_order_acquire);
if (len > 0) {
writeAllSignalSafe(fd, slot.data, len);
}
}
}
void LogRing::clear()
{
m_write_cursor.store(0, std::memory_order_relaxed);
for (auto &entry : m_entries) {
entry.length.store(0, std::memory_order_relaxed);
}
}
+85
View File
@@ -0,0 +1,85 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef LOGRING_H
#define LOGRING_H
#include <QByteArray>
#include <QVector>
#include <atomic>
#include <vector>
/**
@brief The LogRing class
Fixed-capacity, always-on in-memory ring of the most recent log
lines, preallocated once at construction -- append() never
allocates.
Lock-free by construction, not just "thread-safe": step 4 (see
crashhandler.h) reads this ring from inside a POSIX signal handler,
where taking any lock is unsafe -- if the crashing thread happens to
be the one that already holds it (or any other thread does and never
gets scheduled again), the handler hangs forever, and you lose both
the ring dump *and* the core dump. So there is no mutex here at all:
append() claims a slot with a single atomic fetch-add, and
dumpToFd()/snapshot() read the preallocated entries directly.
Accepted tradeoff: if dumpToFd() runs while another thread is
mid-append into the exact slot being read (only possible in the
crash-handler case, and only for at most one slot), that one entry
may be read torn -- part old content, part new. Every other entry is
unaffected. This is deliberate: the alternative (a seqlock or similar
to detect and retry torn reads) adds real complexity for a window
that, per discussion #644, is not worth trading "the handler must
never block" against.
*/
class LogRing
{
public:
static constexpr int kCapacityEntries = 4096;
static constexpr int kEntryBytes = 512; // 4096 * 512 = 2 MiB total
LogRing();
/// Append one already-formatted, already-truncated log line.
/// Bytes beyond kEntryBytes are dropped with a truncation marker.
/// Never allocates, never blocks. Safe to call from any normal
/// (non-signal) thread concurrently.
void append(const QByteArray &line) noexcept;
/// Snapshot of the entries currently held, oldest first. Normal
/// (non-signal) context only.
QVector<QByteArray> snapshot() const;
/// Async-signal-safe: writes every entry currently held to fd via
/// write(2) only -- no allocation, no Qt, no locks. May write a
/// torn entry under the rare race described above; never blocks.
void dumpToFd(int fd) const noexcept;
void clear();
private:
struct Entry {
char data[kEntryBytes];
std::atomic<int> length{0}; // 0 = not yet written this lap
};
std::vector<Entry> m_entries; // preallocated once, capacity fixed
std::atomic<quint64> m_write_cursor{0}; // monotonically increasing
};
#endif // LOGRING_H
+431
View File
@@ -0,0 +1,431 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "qetlogger.h"
#include "crashhandler.h"
#include "../qetapp.h"
#include "../qetversion.h"
#include <QDateTime>
#include <QDir>
#include <QFileInfo>
#include <QSysInfo>
#include <cstdio>
namespace {
/**
@brief legacyStderrOutput
The QET_LOG_DISABLE=1 escape hatch. Deliberately independent of
every other function in this file -- including sanitize()/
formatLine(), which are exactly the new code a problem might be in
-- so this path stays usable even if the rest of the rework
misbehaves. No ring, no file, no rotation, no mutex.
*/
void legacyStderrOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
const QByteArray local_msg = msg.toLocal8Bit();
const char *file = context.file ? context.file : "";
const char *function = context.function ? context.function : "";
const char *level = "Unknown";
switch (type) {
case QtDebugMsg: level = "Debug"; break;
case QtInfoMsg: level = "Info"; break;
case QtWarningMsg: level = "Warning"; break;
case QtCriticalMsg: level = "Critical"; break;
case QtFatalMsg: level = "Fatal"; break;
}
fprintf(stderr, "%s: %s (%s:%u, %s)\n",
level, local_msg.constData(), file, context.line, function);
}
/**
@brief ReentrancyGuard
Sets the referenced flag on construction, clears it on destruction
(including via early return / exception unwinding). Used as the
per-thread guard against the logger recursing into itself.
*/
struct ReentrancyGuard
{
bool &flag;
explicit ReentrancyGuard(bool &f) : flag(f) {flag = true;}
~ReentrancyGuard() {flag = false;}
};
} // namespace
/**
@brief QetLogger::instance
Function-local static: guaranteed constructed exactly once, in a
thread-safe way, on first use -- but the *meaningful* initialisation
(log path resolution, opening the file) happens in init(), called
explicitly from main() at a defined point, not implicitly on
whichever thread happens to log first.
*/
QetLogger &QetLogger::instance()
{
static QetLogger logger;
return logger;
}
void QetLogger::init()
{
m_disabled = (qgetenv("QET_LOG_DISABLE") == "1");
if (m_disabled) {
return;
}
m_log_dir = QETApp::dataDir();
m_base_name = QDate::currentDate().toString(QStringLiteral("yyyyMMdd"));
QMutexLocker locker(&m_file_mutex);
m_file_output_ok = ensureFileOpenLocked();
}
void QetLogger::installCrashHandler()
{
if (m_disabled) {
return;
}
CrashHandler::install(&m_ring, crashDumpPath());
}
QString QetLogger::crashDumpPath() const
{
return m_log_dir % QStringLiteral("/crash_dump.log");
}
QString QetLogger::currentLogFilePath() const
{
return m_log_dir % QStringLiteral("/") % m_base_name % QStringLiteral(".log");
}
/**
@brief QetLogger::ensureFileOpenLocked
Caller must hold m_file_mutex. Opens the current session's log file
if not already open. Refuses to follow a pre-existing symlink at
that path, and creates the file owner-read/write only.
*/
bool QetLogger::ensureFileOpenLocked()
{
if (m_file.isOpen()) {
return true;
}
QDir().mkpath(m_log_dir);
const QString path = currentLogFilePath();
const QFileInfo info(path);
if (info.exists() && info.isSymLink()) {
// Filesystem hardening: refuse a pre-planted symlink rather than
// silently appending to whatever it points at.
return false;
}
m_file.setFileName(path);
if (!m_file.open(QIODevice::WriteOnly | QIODevice::Append)) {
return false;
}
m_file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
m_bytes_written_current_file = m_file.size();
return true;
}
QString QetLogger::rotatedPath(int index) const
{
return m_log_dir % QStringLiteral("/") % m_base_name % QStringLiteral(".") % QString::number(index) % QStringLiteral(".log");
}
/**
@brief QetLogger::rotateLocked
Caller must hold m_file_mutex. Shifts .3.log -> .4.log (dropping the
previous .4.log), .2.log -> .3.log, .1.log -> .2.log, .log -> .1.log,
then opens a fresh, empty current file.
*/
void QetLogger::rotateLocked()
{
m_file.close();
const QString base_path = currentLogFilePath();
for (int i = kRotationKeep; i >= 1; --i) {
const QString from = (i == 1) ? base_path : rotatedPath(i - 1);
const QString to = rotatedPath(i);
if (QFile::exists(to)) {
QFile::remove(to);
}
if (QFile::exists(from)) {
QFile::rename(from, to);
}
}
m_bytes_written_current_file = 0;
m_file_output_ok = ensureFileOpenLocked();
}
void QetLogger::writeToFile(const QByteArray &line, QtMsgType type)
{
QMutexLocker locker(&m_file_mutex);
if (!m_file_output_ok) {
// Write-failure policy: once file output has failed, stop
// attempting it rather than spin-retrying every message. The
// ring keeps running regardless.
return;
}
const qint64 written = m_file.write(line);
if (written != line.size()) {
m_file_output_ok = false;
m_file.close();
return;
}
m_bytes_written_current_file += written;
if (type >= QtWarningMsg) {
m_file.flush();
}
if (m_bytes_written_current_file >= kMaxFileBytes) {
rotateLocked();
}
}
/**
@brief QetLogger::sanitize
Escapes newlines, carriage returns and other control characters.
Much of what QET logs is externally controlled (file paths, element
names, font strings read out of a .qet file); left unescaped, a
crafted string containing '\n' can forge additional log lines.
Operates on already-UTF-8-encoded bytes: this is safe because UTF-8
continuation bytes are always >= 0x80, so any byte < 0x20 found here
is a genuine ASCII control character, never part of a multi-byte
sequence.
*/
QByteArray QetLogger::sanitize(const QByteArray &input)
{
QByteArray out;
out.reserve(input.size());
for (unsigned char c : input) {
if (c == '\n') {
out += "\\n";
} else if (c == '\r') {
out += "\\r";
} else if (c == '\t') {
out += static_cast<char>(c);
} else if (c < 0x20 || c == 0x7F) {
out += "\\x";
out += QByteArray::number(c, 16).rightJustified(2, '0');
} else {
out += static_cast<char>(c);
}
}
return out;
}
/**
@brief QetLogger::truncateMessage
Caps a single message at max_bytes, appending a marker stating how
many bytes were dropped, so one pathological caller (e.g. dumping an
entire XML document to qDebug()) can't consume an unbounded amount
of the ring's or file's byte budget.
*/
QByteArray QetLogger::truncateMessage(const QByteArray &input, int max_bytes)
{
if (input.size() <= max_bytes) {
return input;
}
const int dropped = input.size() - max_bytes;
QByteArray out = input.left(max_bytes);
out += " ...[truncated ";
out += QByteArray::number(dropped);
out += " bytes]";
return out;
}
QByteArray QetLogger::formatLine(QtMsgType type, const QMessageLogContext &context, const QByteArray &sanitized_msg)
{
// Includes the date (not just the time) so that a session crossing
// midnight -- now kept in a single file -- doesn't read as ambiguous.
const QByteArray timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss.zzz")).toUtf8();
const char *level = "Unknown";
switch (type) {
case QtDebugMsg: level = "Debug"; break;
case QtInfoMsg: level = "Info"; break;
case QtWarningMsg: level = "Warning"; break;
case QtCriticalMsg: level = "Critical"; break;
case QtFatalMsg: level = "Fatal"; break;
}
const char *file = context.file ? context.file : "";
const char *function = context.function ? context.function : "";
QByteArray line = timestamp;
line += ' ';
line += level;
line += ": ";
line += sanitized_msg;
if (type == QtInfoMsg) {
line += " \n";
} else {
line += " (";
line += file;
line += ":";
line += QByteArray::number(context.line ? context.line : 0);
line += ", ";
line += function;
line += ")\n";
}
return line;
}
void QetLogger::handleMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
if (m_disabled) {
legacyStderrOutput(type, context, msg);
return;
}
static thread_local bool in_handler = false;
if (in_handler) {
// The logger itself triggered a message (e.g. from inside a Qt
// call it made) -- drop it rather than recurse.
return;
}
ReentrancyGuard guard(in_handler);
const QByteArray sanitized = truncateMessage(sanitize(msg.toUtf8()), kMaxMessageBytes);
const QByteArray line = formatLine(type, context, sanitized);
fwrite(line.constData(), 1, static_cast<size_t>(line.size()), stderr);
m_ring.append(line);
writeToFile(line, type);
}
void QetLogger::pruneOldLogFiles(int days)
{
if (m_disabled) {
return;
}
const QDate today = QDate::currentDate();
const QStringList filters = {
QStringLiteral("????????.log"), // base files, e.g. 20260803.log
QStringLiteral("????????.?.log"), // rotated files, e.g. 20260803.1.log
};
const QDir dir(m_log_dir);
const auto entries = dir.entryInfoList(filters, QDir::Files);
for (const QFileInfo &file_info : entries) {
if (!file_info.isFile()) {
continue;
}
// lastModified(), not lastRead(): reading the log (opening it to
// attach to a bug report, a backup job, an indexer) must not
// reset the retention clock and keep it alive indefinitely.
if (file_info.lastModified().date().daysTo(today) > days) {
QFile::remove(file_info.absoluteFilePath());
}
}
}
// --- Step 5: getting the data back out ----------------------------------
bool QetLogger::hasPendingCrashDump() const
{
if (m_disabled) {
return false;
}
const QFileInfo info(crashDumpPath());
return info.exists() && info.isFile() && info.size() > 0;
}
QByteArray QetLogger::pendingCrashDumpContents() const
{
QFile file(crashDumpPath());
if (!file.open(QIODevice::ReadOnly)) {
return QByteArray();
}
return redact(file.readAll());
}
void QetLogger::clearPendingCrashDump()
{
QFile::remove(crashDumpPath());
}
QByteArray QetLogger::buildDiagnosticsReport() const
{
QByteArray header;
header += "QElectroTech diagnostics report\n";
header += "Generated: " % QDateTime::currentDateTime().toString(Qt::ISODate) % "\n";
header += "Version: " % QetVersion::displayedVersion() % "\n";
header += "Git: " GIT_COMMIT_SHA "\n";
header += "OS: " % QSysInfo::prettyProductName() % " (" % QSysInfo::currentCpuArchitecture() % ")\n";
header += "Qt: " QT_VERSION_STR "\n";
header += "---\n";
QByteArray body;
QFile file(currentLogFilePath());
if (file.open(QIODevice::ReadOnly)) {
body = file.readAll();
} else {
// Fall back to the in-memory ring if the file itself can't be
// read (e.g. file output already failed this session).
for (const QByteArray &line : m_ring.snapshot()) {
body += line;
}
}
return redact(header + body);
}
/**
@brief QetLogger::redact
Replaces the user's home directory with "~" wherever it appears.
Applied before a crash dump or a diagnostics report is ever shown to
the user: both are destined to be attached to a public bug tracker,
and an absolute path under the home directory leaks the account name
(discussion #644's privacy section: "/home/laurent/... leaks a
username"). This is the one redaction implemented here; the
discussion's fancier "optionally redact project filenames too" is
not attempted -- reliably telling a project path apart from
arbitrary log text is a much fuzzier problem than a literal prefix
match against a known directory.
*/
QByteArray QetLogger::redact(const QByteArray &input)
{
const QByteArray home = QDir::homePath().toUtf8();
if (home.isEmpty()) {
return input;
}
QByteArray out = input;
out.replace(home, QByteArrayLiteral("~"));
return out;
}
+159
View File
@@ -0,0 +1,159 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef QETLOGGER_H
#define QETLOGGER_H
#include "logring.h"
#include <QFile>
#include <QMutex>
#include <QString>
#include <QtGlobal>
/**
@brief The QetLogger class
Rework of QET's diagnostic logging (discussion #644, steps 1-3):
- Step 1: one file handle held open for the session under a mutex
instead of opening/closing per message; the log path (including
the date-stamped filename) is resolved exactly once, at init(),
instead of being recomputed on every message -- a session that
crosses midnight now stays in one file; retention now uses
lastModified() instead of lastRead(); stderr and file output both
use UTF-8 explicitly (previously stderr used the local 8-bit
codec and the file's encoding silently differed between Qt5 and
Qt6).
- Step 2: the previously-unbounded daily file is now size-capped
and rotated (kMaxFileBytes per file, kRotationKeep old files kept
beyond the current one); each message is truncated to
kMaxMessageBytes and control characters are escaped before being
written, so one pathological caller can't blow the size budget or
forge log lines; the log file is refused if it already exists as
a symlink and is created owner-read/write only.
- Step 3: every formatted line is also appended to an in-memory
LogRing (see logring.h) -- always on, fixed capacity, allocation-
free on the hot path.
- Step 4: installCrashHandler() wires the ring up to CrashHandler
(see crashhandler.h), so a SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL (or,
on Windows, an unhandled structured exception) flushes the ring to
a fixed crash-dump file before the process dies.
- Step 5: hasPendingCrashDump()/pendingCrashDumpContents()/
clearPendingCrashDump() let startup code (see QETApp::checkBackupFiles())
notice and offer an unretrieved crash dump from the *previous* run;
buildDiagnosticsReport() is the equivalent for a manual "save a
report right now" action on the *current*, still-running session.
Both go through redact() before ever reaching the user, since both
are destined for a public bug tracker.
Deliberately NOT included: log categories, a full session header
beyond what the crash dump/report already carry, repeat collapsing,
rate limiting. Those are listed in discussion #644 under "best
practices worth building in", not part of the numbered steps.
Escape hatch: if QET_LOG_DISABLE=1 is set in the environment at
init() time, this class does nothing beyond a minimal, independent
stderr passthrough -- no ring, no file, no rotation -- so a problem
in this rework can be worked around without a rebuild.
*/
class QetLogger
{
public:
static constexpr qint64 kMaxFileBytes = 2 * 1024 * 1024; // 2 MiB per file
static constexpr int kRotationKeep = 4; // .1.log .. .4.log
static constexpr int kMaxMessageBytes = 4096; // per-message truncation
static QetLogger &instance();
/// Must be called exactly once, from main(), before
/// qInstallMessageHandler(). Resolves the log directory and the
/// session's log filename, and opens the file.
void init();
/// Step 4: installs the crash handler (see crashhandler.h). Must
/// be called after init() (the ring and the dump path must exist
/// first) and, like init(), only once.
void installCrashHandler();
/// The function installed via qInstallMessageHandler() forwards here.
void handleMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg);
/// Replaces the old delete_old_log_files(): same call shape, fixed
/// to use lastModified() (not lastRead()) and to also match rotated
/// file names.
void pruneOldLogFiles(int days);
/// Snapshot of the in-memory ring, oldest first.
QVector<QByteArray> ringSnapshot() const {return m_ring.snapshot();}
// --- Step 5: getting the data back out -------------------------
/// True if a previous run's crash handler left an unretrieved
/// dump behind.
bool hasPendingCrashDump() const;
/// Raw contents of the pending crash dump, or an empty array if
/// there isn't one. Does not delete it -- call
/// clearPendingCrashDump() once it has been offered to the user.
QByteArray pendingCrashDumpContents() const;
/// Deletes the pending crash dump file. Call after the user has
/// been offered it (whether they chose to save it or not) so it
/// is never offered a second time.
void clearPendingCrashDump();
/// Builds a redacted diagnostics bundle from the *current* session
/// (header + this session's log file so far) for the manual
/// "Save report" action -- as opposed to pendingCrashDumpContents(),
/// which is about a *previous*, already-terminated session.
QByteArray buildDiagnosticsReport() const;
/// Replaces occurrences of the user's home directory with "~".
/// Applied to both the crash dump and buildDiagnosticsReport()
/// before they are ever shown to the user, since both are
/// destined for a public bug tracker.
static QByteArray redact(const QByteArray &input);
private:
QetLogger() = default;
QetLogger(const QetLogger &) = delete;
bool ensureFileOpenLocked();
void rotateLocked();
void writeToFile(const QByteArray &line, QtMsgType type);
QString rotatedPath(int index) const;
QString crashDumpPath() const;
QString currentLogFilePath() const;
static QByteArray sanitize(const QByteArray &input);
static QByteArray truncateMessage(const QByteArray &input, int max_bytes);
static QByteArray formatLine(QtMsgType type, const QMessageLogContext &context, const QByteArray &sanitized_msg);
bool m_disabled = false;
QString m_log_dir;
QString m_base_name; // e.g. "20260803", resolved once in init()
QMutex m_file_mutex;
QFile m_file;
qint64 m_bytes_written_current_file = 0;
bool m_file_output_ok = false;
LogRing m_ring;
};
#endif // QETLOGGER_H
@@ -0,0 +1,91 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "diagnosticsreportdialog.h"
#include "../../qetmessagebox.h"
#include <QDialogButtonBox>
#include <QFile>
#include <QFileDialog>
#include <QFontDatabase>
#include <QLabel>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QVBoxLayout>
DiagnosticsReportDialog::DiagnosticsReportDialog(
const QString &title,
const QString &intro,
const QByteArray &content,
QWidget *parent) :
QDialog(parent)
{
setWindowTitle(title);
resize(700, 500);
auto *layout = new QVBoxLayout(this);
auto *intro_label = new QLabel(intro, this);
intro_label->setWordWrap(true);
layout->addWidget(intro_label);
auto *preview = new QPlainTextEdit(this);
preview->setReadOnly(true);
preview->setLineWrapMode(QPlainTextEdit::NoWrap);
preview->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
preview->setPlainText(QString::fromUtf8(content));
layout->addWidget(preview);
auto *buttons = new QDialogButtonBox(this);
QPushButton *save_button = buttons->addButton(tr("Enregistrer..."), QDialogButtonBox::ActionRole);
buttons->addButton(QDialogButtonBox::Close);
connect(save_button, &QPushButton::clicked, this, &DiagnosticsReportDialog::saveToFile);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(buttons->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &QDialog::accept);
layout->addWidget(buttons);
// Stash the content for saveToFile(); the preview widget already
// holds a QString copy but we save the original UTF-8 bytes to avoid
// any round-trip surprises.
setProperty("qet_report_content", content);
}
void DiagnosticsReportDialog::saveToFile()
{
const QString path = QFileDialog::getSaveFileName(
this,
tr("Enregistrer le rapport de diagnostic"),
QStringLiteral("qet-diagnostic-report.txt"),
tr("Fichiers texte (*.txt);;Tous les fichiers (*)"));
if (path.isEmpty()) {
return;
}
QFile file(path);
if (!file.open(QIODevice::WriteOnly)) {
QET::QetMessageBox::critical(
this,
tr("Erreur"),
tr("Impossible d'écrire dans le fichier « %1 ».").arg(path));
return;
}
file.write(property("qet_report_content").toByteArray());
file.close();
}
@@ -0,0 +1,51 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DIAGNOSTICSREPORTDIALOG_H
#define DIAGNOSTICSREPORTDIALOG_H
#include <QDialog>
/**
@brief The DiagnosticsReportDialog class
Discussion #644, step 5: "Show what's in it before saving -- the user
is about to attach this to a public tracker." Used for both the
after-a-crash offer (QETApp::checkBackupFiles()) and the manual
"Help > Diagnostics > Save report" action -- the only difference
between the two is the intro text and where the content comes from
(QetLogger::pendingCrashDumpContents() vs. buildDiagnosticsReport()).
The content passed in is expected to already be redacted
(QetLogger::redact()) -- this dialog just displays and optionally
saves whatever it's given.
*/
class DiagnosticsReportDialog : public QDialog
{
Q_OBJECT
public:
explicit DiagnosticsReportDialog(
const QString &title,
const QString &intro,
const QByteArray &content,
QWidget *parent = nullptr);
private slots:
void saveToFile();
};
#endif // DIAGNOSTICSREPORTDIALOG_H
+29 -123
View File
@@ -16,6 +16,8 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>. along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "cli_export.h" #include "cli_export.h"
#include "logging/eventloopwatchdog.h"
#include "logging/qetlogger.h"
#include "machine_info.h" #include "machine_info.h"
#include "qet.h" #include "qet.h"
#include "qetapp.h" #include "qetapp.h"
@@ -62,131 +64,16 @@ class EarlyFileOpenCatcher : public QObject
#endif #endif
/** /**
@brief myMessageOutput @brief qetLogMessageHandler
for debugging Installed via qInstallMessageHandler(); forwards to QetLogger, which
@param type : the messages that can be sent to a message handler holds all the actual formatting/ring/rotation state. See
@param context : were? wat? logging/qetlogger.h for the rationale (discussion #644).
@param msg : Message
*/ */
void myMessageOutput(QtMsgType type, void qetLogMessageHandler(QtMsgType type,
const QMessageLogContext &context, const QMessageLogContext &context,
const QString &msg) const QString &msg)
{ {
QetLogger::instance().handleMessage(type, context, msg);
QString txt=QTime::currentTime().toString("hh:mm:ss.zzz");
QByteArray dbs =txt.toLocal8Bit();
QByteArray localMsg = msg.toLocal8Bit();
const char *file = context.file ? context.file : "";
const char *function = context.function ? context.function : "";
switch (type) {
case QtDebugMsg:
fprintf(stderr,
"%s Debug: %s (%s:%u, %s)\n",
dbs.constData(),
localMsg.constData(),
file,
context.line,
function);
txt+=" Debug: ";
break;
case QtInfoMsg:
fprintf(stderr,
"%s Info: %s \n",
dbs.constData(),
localMsg.constData());
txt+=" Info: ";
break;
case QtWarningMsg:
fprintf(stderr,
"%s Warning: %s (%s:%u, %s)\n",
dbs.constData(),
localMsg.constData(),
file, context.line,
function);
txt+=" Warning: ";
break;
case QtCriticalMsg:
fprintf(stderr,
"%s Critical: %s (%s:%u, %s)\n",
dbs.constData(),
localMsg.constData(),
file,
context.line,
function);
txt+=" Critical: ";
break;
case QtFatalMsg:
fprintf(stderr,
"%s Fatal: %s (%s:%u, %s)\n",
dbs.constData(),
localMsg.constData(),
file,
context.line,
function);
txt+=" Fatal: ";
break;
default:
fprintf(stderr,
"%s Unknown: %s (%s:%u, %s)\n",
dbs.constData(),
localMsg.constData(),
file,
context.line,
function);
txt+=" Unknown: ";
}
txt+= msg;
if(type==QtInfoMsg){
txt+=" \n";
} else {
txt+= " (";
txt+= context.file ? context.file : "";
txt+= ":";
txt+=QString::number(context.line ? context.line :0);
txt+= ", ";
txt+= context.function ? context.function : "";
txt+=")\n";
}
QFile outFile(QETApp::dataDir()
+"/"
+QDate::currentDate().toString("yyyyMMdd")
+".log");
if(outFile.open(QIODevice::WriteOnly | QIODevice::Append))
{
QTextStream ts(&outFile);
ts << txt;
}
outFile.close();
}
/**
@brief delete_old_log_files
delete old log files
@param days : max days old
*/
void delete_old_log_files(int days)
{
const QDate today = QDate::currentDate();
const QString path = QETApp::dataDir() % "/";
QString filter("%1%1%1%1%1%1%1%1.log"); // pattern
filter = filter.arg("[0123456789]"); // valid characters
Q_FOREACH (auto fileInfo,
QDir(path).entryInfoList(
QStringList(filter),
QDir::Files))
{
if (fileInfo.lastRead().date().daysTo(today) > days)
{
QString filepath = fileInfo.absoluteFilePath();
QDir deletefile;
deletefile.setPath(filepath);
deletefile.remove(filepath);
qDebug() << "File " % filepath % " is deleted!";
}
}
} }
/** /**
@@ -253,13 +140,25 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
} }
} }
// Resolve the logger's state (log directory, session filename, open
// file handle) explicitly here, immediately before installing the
// handler -- not implicitly on whichever thread happens to log
// first. See QetLogger::init().
//
// Install the log-file message handler BEFORE the application starts: // Install the log-file message handler BEFORE the application starts:
// QETApp's constructor does the whole startup (collections, editor, // QETApp's constructor does the whole startup (collections, editor,
// opening the projects given on the command line), so installing the // opening the projects given on the command line), so installing the
// handler afterwards - as was done in the startup worker below - meant // handler afterwards - as was done in the startup worker below - meant
// exactly the interesting lines (collection and project load timers) // exactly the interesting lines (collection and project load timers)
// went to stderr, which is invisible in a Windows GUI session. // went to stderr, which is invisible in a Windows GUI session.
qInstallMessageHandler(myMessageOutput); QetLogger::instance().init();
qInstallMessageHandler(qetLogMessageHandler);
// Step 4 (discussion #644): flush the ring to a crash-dump file if
// the process dies from here on. Installed right after the ring
// exists (init() just constructed it) and as early as reasonably
// possible, so it also covers whatever runs between here and
// QETApp's own construction below.
QetLogger::instance().installCrashHandler();
SingleApplication app(argc, argv, true); SingleApplication app(argc, argv, true);
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
@@ -308,9 +207,16 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
{ {
qInfo("Start-up"); qInfo("Start-up");
// delete old log files of max 7 days old. // delete old log files of max 7 days old.
delete_old_log_files(7); QetLogger::instance().pruneOldLogFiles(7);
MachineInfo::instance()->send_info_to_debug(); MachineInfo::instance()->send_info_to_debug();
}); });
// Constructed here rather than earlier: start() measures ticks against
// the event loop app.exec() is about to run, so there is no point
// (and no accurate baseline) before this line.
EventLoopWatchdog watchdog;
watchdog.start();
return app.exec(); return app.exec();
} }
+2 -1
View File
@@ -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();
+3
View File
@@ -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()
{ {
+1
View File
@@ -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();
+10
View File
@@ -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>
+1 -1
View File
@@ -842,7 +842,7 @@ void ProjectView::initWidgets()
QHBoxLayout *TopRightCorner_Layout = new QHBoxLayout(); QHBoxLayout *TopRightCorner_Layout = new QHBoxLayout();
TopRightCorner_Layout->setContentsMargins(0,0,0,0); TopRightCorner_Layout->setContentsMargins(0,0,0,0);
// some place left to the 'next_right_view_button' button // some place left to the 'next_right_view_button' button
TopRightCorner_Layout->insertSpacing(1,10); TopRightCorner_Layout->addSpacing(10);
QHBoxLayout *TopLeftCorner_Layout = new QHBoxLayout(); QHBoxLayout *TopLeftCorner_Layout = new QHBoxLayout();
TopLeftCorner_Layout->setContentsMargins(0,0,0,0); TopLeftCorner_Layout->setContentsMargins(0,0,0,0);
+205
View File
@@ -267,6 +267,211 @@ QDomElement ElementData::kindInfoToXml(QDomDocument &document)
return returned_elmt; return returned_elmt;
} }
/**
* @brief ElementData::plcMasterDataToXml
* Serialize PLC master data to XML (used by Element::toXml for diagram instances)
*/
QDomElement ElementData::plcMasterDataToXml(QDomDocument &document) const
{
auto xml_plc = document.createElement(QStringLiteral("plcMasterData"));
xml_plc.setAttribute(QStringLiteral("rowHeight"),
QString::number(m_plc_master_data.rowHeight, 'f', 2));
// Save break positions
{
auto xml_breaks = document.createElement(QStringLiteral("breakPositions"));
for (int bp : m_plc_master_data.breakPositions) {
auto xml_bp = document.createElement(QStringLiteral("break"));
xml_bp.appendChild(document.createTextNode(QString::number(bp)));
xml_breaks.appendChild(xml_bp);
}
xml_plc.appendChild(xml_breaks);
}
// Save column widths
auto xml_col_widths = document.createElement(QStringLiteral("columnWidths"));
for (auto it = m_plc_master_data.colWidths.constBegin();
it != m_plc_master_data.colWidths.constEnd(); ++it) {
auto xml_col = document.createElement(QStringLiteral("column"));
xml_col.setAttribute(QStringLiteral("index"), it.key());
xml_col.setAttribute(QStringLiteral("width"), QString::number(it.value(), 'f', 2));
xml_col_widths.appendChild(xml_col);
}
xml_plc.appendChild(xml_col_widths);
// Save column visibility
auto xml_col_vis = document.createElement(QStringLiteral("columnVisibility"));
for (auto it = m_plc_master_data.colVisible.constBegin();
it != m_plc_master_data.colVisible.constEnd(); ++it) {
auto xml_col = document.createElement(QStringLiteral("column"));
xml_col.setAttribute(QStringLiteral("index"), it.key());
xml_col.setAttribute(QStringLiteral("visible"), it.value() ? "true" : "false");
xml_col_vis.appendChild(xml_col);
}
xml_plc.appendChild(xml_col_vis);
// Save fonts
if (!m_plc_master_data.headerFont.family().isEmpty()) {
auto xml_hfont = document.createElement(QStringLiteral("headerFont"));
xml_hfont.setAttribute(QStringLiteral("family"), m_plc_master_data.headerFont.family());
xml_hfont.setAttribute(QStringLiteral("size"), m_plc_master_data.headerFont.pointSize());
xml_hfont.setAttribute(QStringLiteral("bold"), m_plc_master_data.headerFont.bold() ? "true" : "false");
xml_plc.appendChild(xml_hfont);
}
if (!m_plc_master_data.cellFont.family().isEmpty()) {
auto xml_cfont = document.createElement(QStringLiteral("cellFont"));
xml_cfont.setAttribute(QStringLiteral("family"), m_plc_master_data.cellFont.family());
xml_cfont.setAttribute(QStringLiteral("size"), m_plc_master_data.cellFont.pointSize());
xml_cfont.setAttribute(QStringLiteral("bold"), m_plc_master_data.cellFont.bold() ? "true" : "false");
xml_plc.appendChild(xml_cfont);
}
// Save custom column names
if (!m_plc_master_data.columnNames.isEmpty()) {
auto xml_names = document.createElement(QStringLiteral("columnNames"));
for (int i = 0; i < m_plc_master_data.columnNames.size(); ++i) {
auto xml_name = document.createElement(QStringLiteral("column"));
xml_name.setAttribute(QStringLiteral("index"), i);
xml_name.appendChild(document.createTextNode(m_plc_master_data.columnNames.at(i)));
xml_names.appendChild(xml_name);
}
xml_plc.appendChild(xml_names);
}
// Save column order
if (!m_plc_master_data.columnOrder.isEmpty()) {
auto xml_order = document.createElement(QStringLiteral("columnOrder"));
QString order_str;
for (int i = 0; i < m_plc_master_data.columnOrder.size(); ++i) {
if (i > 0) order_str += QStringLiteral(",");
order_str += QString::number(m_plc_master_data.columnOrder.at(i));
}
xml_order.appendChild(document.createTextNode(order_str));
xml_plc.appendChild(xml_order);
}
// Save showHeaders
{
auto xml_sh = document.createElement(QStringLiteral("showHeaders"));
xml_sh.appendChild(document.createTextNode(
m_plc_master_data.showHeaders ? QStringLiteral("1") : QStringLiteral("0")));
xml_plc.appendChild(xml_sh);
}
// Save IO entries
auto xml_ios = document.createElement(QStringLiteral("plcIOs"));
for (const auto &io : m_plc_master_data.ios) {
auto xml_io = document.createElement(QStringLiteral("plcIO"));
xml_io.setAttribute(QStringLiteral("type"), plcIOTypeToString(io.type));
xml_io.setAttribute(QStringLiteral("address"), io.address);
xml_io.setAttribute(QStringLiteral("functionText"), io.functionText);
xml_io.setAttribute(QStringLiteral("comment"), io.comment);
xml_io.setAttribute(QStringLiteral("crossRef"), io.crossRef);
xml_io.setAttribute(QStringLiteral("terminalCount"), io.terminalCount);
for (const auto &t : io.terminals) {
auto xml_term = document.createElement(QStringLiteral("terminal"));
xml_term.appendChild(document.createTextNode(t));
xml_io.appendChild(xml_term);
}
xml_ios.appendChild(xml_io);
}
xml_plc.appendChild(xml_ios);
return xml_plc;
}
/**
* @brief ElementData::plcMasterDataFromXml
* Deserialize PLC master data from XML
*/
void ElementData::plcMasterDataFromXml(const QDomElement &xml_plc)
{
if (xml_plc.isNull())
return;
// Reset PLC data before loading to avoid appending to existing data
m_plc_master_data = PlcMasterData();
m_plc_master_data.rowHeight = xml_plc.attribute(
QStringLiteral("rowHeight"), QStringLiteral("8.0")).toDouble();
// Load break positions
auto xml_breaks = xml_plc.firstChildElement(QStringLiteral("breakPositions"));
for (const auto &xml_bp : QETXML::findInDomElement(xml_breaks, QStringLiteral("break"))) {
m_plc_master_data.breakPositions.append(xml_bp.text().toInt());
}
// Load column widths
auto xml_col_widths = xml_plc.firstChildElement(QStringLiteral("columnWidths"));
for (const auto &xml_col : QETXML::findInDomElement(xml_col_widths, QStringLiteral("column"))) {
int idx = xml_col.attribute(QStringLiteral("index")).toInt();
qreal w = xml_col.attribute(QStringLiteral("width")).toDouble();
m_plc_master_data.colWidths.insert(idx, w);
}
// Load column visibility
auto xml_col_vis = xml_plc.firstChildElement(QStringLiteral("columnVisibility"));
for (const auto &xml_col : QETXML::findInDomElement(xml_col_vis, QStringLiteral("column"))) {
int idx = xml_col.attribute(QStringLiteral("index")).toInt();
bool vis = xml_col.attribute(QStringLiteral("visible")) == QLatin1String("true");
m_plc_master_data.colVisible.insert(idx, vis);
}
// Load fonts
auto xml_hfont = xml_plc.firstChildElement(QStringLiteral("headerFont"));
if (!xml_hfont.isNull()) {
m_plc_master_data.headerFont.setFamily(xml_hfont.attribute(QStringLiteral("family")));
m_plc_master_data.headerFont.setPointSize(xml_hfont.attribute(QStringLiteral("size")).toInt());
m_plc_master_data.headerFont.setBold(xml_hfont.attribute(QStringLiteral("bold")) == QLatin1String("true"));
}
auto xml_cfont = xml_plc.firstChildElement(QStringLiteral("cellFont"));
if (!xml_cfont.isNull()) {
m_plc_master_data.cellFont.setFamily(xml_cfont.attribute(QStringLiteral("family")));
m_plc_master_data.cellFont.setPointSize(xml_cfont.attribute(QStringLiteral("size")).toInt());
m_plc_master_data.cellFont.setBold(xml_cfont.attribute(QStringLiteral("bold")) == QLatin1String("true"));
}
// Load custom column names
auto xml_names = xml_plc.firstChildElement(QStringLiteral("columnNames"));
for (const auto &xml_col : QETXML::findInDomElement(xml_names, QStringLiteral("column"))) {
int idx = xml_col.attribute(QStringLiteral("index")).toInt();
while (m_plc_master_data.columnNames.size() <= idx)
m_plc_master_data.columnNames.append(QString());
m_plc_master_data.columnNames.replace(idx, xml_col.text());
}
// Load column order
auto xml_order = xml_plc.firstChildElement(QStringLiteral("columnOrder"));
if (!xml_order.isNull()) {
QStringList order_str_list = xml_order.text().split(',');
for (const auto &s : order_str_list) {
m_plc_master_data.columnOrder.append(s.trimmed().toInt());
}
}
// Load showHeaders
auto xml_sh = xml_plc.firstChildElement(QStringLiteral("showHeaders"));
if (!xml_sh.isNull()) {
m_plc_master_data.showHeaders = xml_sh.text() == QLatin1String("1");
}
// Load IO entries
auto xml_ios = xml_plc.firstChildElement(QStringLiteral("plcIOs"));
for (const auto &xml_io : QETXML::findInDomElement(xml_ios, QStringLiteral("plcIO"))) {
PlcIO io;
io.type = plcIOTypeFromString(xml_io.attribute(QStringLiteral("type")));
io.address = xml_io.attribute(QStringLiteral("address"));
io.functionText = xml_io.attribute(QStringLiteral("functionText"));
io.comment = xml_io.attribute(QStringLiteral("comment"));
io.crossRef = xml_io.attribute(QStringLiteral("crossRef"));
io.terminalCount = xml_io.attribute(QStringLiteral("terminalCount")).toInt();
for (const auto &xml_term : QETXML::findInDomElement(xml_io, QStringLiteral("terminal"))) {
io.terminals.append(xml_term.text());
}
m_plc_master_data.ios.append(io);
}
}
/** /**
* @brief ElementData::setTerminalType * @brief ElementData::setTerminalType
* Override the terminal type by \p t_type * Override the terminal type by \p t_type
+7
View File
@@ -130,6 +130,11 @@ class ElementData : public PropertiesInterface
QList<int> columnOrder; ///< Column display order (logical indices) QList<int> columnOrder; ///< Column display order (logical indices)
bool showHeaders = true; ///< Show column headers on sheet bool showHeaders = true; ///< Show column headers on sheet
PlcMasterData() {
headerFont.setFamily(QString());
cellFont.setFamily(QString());
}
bool operator==(const PlcMasterData &other) const { bool operator==(const PlcMasterData &other) const {
return ios == other.ios return ios == other.ios
&& breakPositions == other.breakPositions && breakPositions == other.breakPositions
@@ -201,6 +206,8 @@ class ElementData : public PropertiesInterface
QDomElement toXml(QDomDocument &xml_element) const override; QDomElement toXml(QDomDocument &xml_element) const override;
bool fromXml(const QDomElement &xml_element) override; bool fromXml(const QDomElement &xml_element) override;
QDomElement kindInfoToXml(QDomDocument &document); QDomElement kindInfoToXml(QDomDocument &document);
QDomElement plcMasterDataToXml(QDomDocument &document) const;
void plcMasterDataFromXml(const QDomElement &xml_plc);
void setTerminalType(ElementData::TerminalType t_type); void setTerminalType(ElementData::TerminalType t_type);
ElementData::TerminalType terminalType() const; ElementData::TerminalType terminalType() const;
+2
View File
@@ -17,6 +17,7 @@
*/ */
#include "terminaldata.h" #include "terminaldata.h"
#include "../qetapp.h"
#include "../utils/qetutils.h" #include "../utils/qetutils.h"
#include <QGraphicsObject> #include <QGraphicsObject>
@@ -38,6 +39,7 @@ TerminalData::TerminalData(QGraphicsObject *parent):
void TerminalData::init() void TerminalData::init()
{ {
m_label_font = QETApp::diagramTextsFont();
} }
TerminalData::~TerminalData() TerminalData::~TerminalData()
+53
View File
@@ -40,6 +40,8 @@
#include "machine_info.h" #include "machine_info.h"
#include "TerminalStrip/ui/terminalstripeditorwindow.h" #include "TerminalStrip/ui/terminalstripeditorwindow.h"
#include "qetversion.h" #include "qetversion.h"
#include "logging/qetlogger.h"
#include "logging/ui/diagnosticsreportdialog.h"
#include <cstdlib> #include <cstdlib>
#include <iostream> #include <iostream>
@@ -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
+2
View File
@@ -271,6 +271,7 @@ class QETApp : public QObject
void openTitleBlockTemplateFiles(const QStringList &); void openTitleBlockTemplateFiles(const QStringList &);
void configureQET(); void configureQET();
void aboutQET(); void aboutQET();
void showDiagnosticsReport();
void receiveMessage(int instanceId, QByteArray message); void receiveMessage(int instanceId, QByteArray message);
private: private:
@@ -287,6 +288,7 @@ class QETApp : public QObject
void initSystemTray(); void initSystemTray();
void buildSystemTrayMenu(); void buildSystemTrayMenu();
void checkBackupFiles(); void checkBackupFiles();
void checkCrashDump();
void fetchWindowStats( void fetchWindowStats(
const QList<QETDiagramEditor *> &, const QList<QETDiagramEditor *> &,
const QList<QETElementEditor *> &, const QList<QETElementEditor *> &,
+77
View File
@@ -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.
+3 -2
View File
@@ -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
+38 -5
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>. along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "element.h" #include "element.h"
#include "../qetapp.h"
#include "../qetproject.h" #include "../qetproject.h"
#include "../PropertiesEditor/propertieseditordialog.h" #include "../PropertiesEditor/propertieseditordialog.h"
#include "../autoNum/assignvariables.h" #include "../autoNum/assignvariables.h"
@@ -33,6 +34,7 @@
#include "../qetgraphicsitem/terminal.h" #include "../qetgraphicsitem/terminal.h"
#include "../ui/elementpropertieswidget.h" #include "../ui/elementpropertieswidget.h"
#include "../undocommand/changeelementinformationcommand.h" #include "../undocommand/changeelementinformationcommand.h"
#include "../undocommand/setautonumcontextcommand.h"
#include "dynamicelementtextitem.h" #include "dynamicelementtextitem.h"
#include "elementtextitemgroup.h" #include "elementtextitemgroup.h"
#include "iostream" #include "iostream"
@@ -860,6 +862,15 @@ bool Element::fromXml(QDomElement &e,
} }
} }
//Load PLC master data override from diagram XML
if (m_data.m_type == ElementData::Master &&
m_data.m_master_type == ElementData::PLC)
{
auto xml_plc = e.firstChildElement(QStringLiteral("plcMasterData"));
if (!xml_plc.isNull())
m_data.plcMasterDataFromXml(xml_plc);
}
//We must block the update of the alignment when loading the information //We must block the update of the alignment when loading the information
//otherwise the pos of the text will not be the same as it was at save time. //otherwise the pos of the text will not be the same as it was at save time.
for(DynamicElementTextItem *deti : m_dynamic_text_list) for(DynamicElementTextItem *deti : m_dynamic_text_list)
@@ -993,6 +1004,15 @@ QDomElement Element::toXml(
element.appendChild(properties); element.appendChild(properties);
} }
//Save PLC master data override for elements on diagram
if (m_data.m_type == ElementData::Master &&
m_data.m_master_type == ElementData::PLC)
{
auto xml_plc = m_data.plcMasterDataToXml(document);
if (!xml_plc.isNull())
element.appendChild(xml_plc);
}
//Dynamic texts //Dynamic texts
QDomElement dyn_text = document.createElement(QStringLiteral("dynamic_texts")); QDomElement dyn_text = document.createElement(QStringLiteral("dynamic_texts"));
for (DynamicElementTextItem *deti : m_dynamic_text_list) for (DynamicElementTextItem *deti : m_dynamic_text_list)
@@ -1626,7 +1646,7 @@ void Element::hoverLeaveEvent(QGraphicsSceneHoverEvent *e)
(ex K for coil) with condition : (ex K for coil) with condition :
formula is empty, text tagged "label" is emptty or "_"; formula is empty, text tagged "label" is emptty or "_";
*/ */
void Element::setUpFormula(bool code_letter) void Element::setUpFormula(bool code_letter, QUndoCommand *parent_undo)
{ {
Q_UNUSED(code_letter) Q_UNUSED(code_letter)
@@ -1655,8 +1675,21 @@ void Element::setUpFormula(bool code_letter)
nc, nc,
diagram(), diagram(),
element_currentAutoNum); element_currentAutoNum);
diagram()->project()->addElementAutoNum(element_currentAutoNum,
ncc.next()); NumerotationContext new_context = ncc.next();
QETProject *project = diagram()->project();
auto setter = [project](const QString &k, const NumerotationContext &c) {project->addElementAutoNum(k, c);};
if (parent_undo)
{
new SetAutoNumContextCommand(setter, element_currentAutoNum, nc, new_context, parent_undo);
}
else
{
auto *undo = new SetAutoNumContextCommand(setter, element_currentAutoNum, nc, new_context);
undo->setText(tr("Numéroter automatiquement un élément", "undo caption"));
diagram()->undoStack().push(undo);
}
if(!m_freeze_label && !formula.isEmpty()) if(!m_freeze_label && !formula.isEmpty())
{ {
@@ -1876,12 +1909,12 @@ void Element::drawPlcTable(QPainter *painter)
// Fonts // Fonts
QFont header_font = plc_data.headerFont; QFont header_font = plc_data.headerFont;
if (header_font.family().isEmpty()) { if (header_font.family().isEmpty()) {
header_font = painter->font(); header_font = QETApp::diagramTextsFont();
header_font.setBold(true); header_font.setBold(true);
} }
QFont cell_font = plc_data.cellFont; QFont cell_font = plc_data.cellFont;
if (cell_font.family().isEmpty()) { if (cell_font.family().isEmpty()) {
cell_font = painter->font(); cell_font = QETApp::diagramTextsFont();
} }
for (const QPointF &pos : positions) { for (const QPointF &pos : positions) {
+2 -1
View File
@@ -35,6 +35,7 @@ class Terminal;
class Conductor; class Conductor;
class DynamicElementTextItem; class DynamicElementTextItem;
class ElementTextItemGroup; class ElementTextItemGroup;
class QUndoCommand;
/** /**
This is the base class for electrical elements. This is the base class for electrical elements.
@@ -142,7 +143,7 @@ class Element : public QetGraphicsItem
{return m_autoNum_seq;} {return m_autoNum_seq;}
autonum::sequentialNumbers& rSequenceStruct() autonum::sequentialNumbers& rSequenceStruct()
{return m_autoNum_seq;} {return m_autoNum_seq;}
void setUpFormula(bool code_letter = true); void setUpFormula(bool code_letter = true, QUndoCommand *parent_undo = nullptr);
void setPrefix(QString); void setPrefix(QString);
QString getPrefix() const; QString getPrefix() const;
void freezeLabel(bool freeze); void freezeLabel(bool freeze);
@@ -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());
} }
+17 -14
View File
@@ -199,19 +199,21 @@ 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
t.setColor(Qt::red); if (!diagram() || diagram()->drawTerminals()) {
painter -> setPen(t); t.setColor(Qt::red);
painter -> drawLine(c, e); painter -> setPen(t);
painter -> drawLine(c, e);
// dessin du point d'amarrage au conducteur en bleu // dessin du point d'amarrage au conducteur en bleu
// draw the docking point to the conductor in blue // draw the docking point to the conductor in blue
t.setColor(m_hovered_color); t.setColor(m_hovered_color);
painter -> setPen(t); painter -> setPen(t);
painter -> setBrush(m_hovered_color); painter -> setBrush(m_hovered_color);
if (m_hovered) { if (m_hovered) {
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);
+8
View File
@@ -136,6 +136,12 @@ void QETMainWindow::initCommonActions()
about_qt_ = new QAction(QET::Icons::QtLogo, tr("À propos de &Qt"), this); about_qt_ = new QAction(QET::Icons::QtLogo, tr("À propos de &Qt"), this);
about_qt_ -> setStatusTip(tr("Affiche des informations sur la bibliothèque Qt", "status bar tip")); about_qt_ -> setStatusTip(tr("Affiche des informations sur la bibliothèque Qt", "status bar tip"));
connect(about_qt_, SIGNAL(triggered()), qApp, SLOT(aboutQt())); connect(about_qt_, SIGNAL(triggered()), qApp, SLOT(aboutQt()));
diagnostics_action_ = new QAction(QET::Icons::DialogInformation, tr("Enregistrer un rapport de diagnostic..."), this);
diagnostics_action_ -> setStatusTip(tr("Génère un rapport avec les derniers messages de journalisation, pour l'inclure dans un rapport de bug", "status bar tip"));
connect(diagnostics_action_, &QAction::triggered, this, []() {
QETApp::instance()->showDiagnosticsReport();
});
} }
/** /**
@@ -158,6 +164,8 @@ void QETMainWindow::initCommonMenus()
help_menu_ -> addAction(donate_); help_menu_ -> addAction(donate_);
help_menu_ -> addAction(about_qt_); help_menu_ -> addAction(about_qt_);
help_menu_ -> addAction(about_qet_); help_menu_ -> addAction(about_qet_);
help_menu_ -> addSeparator();
help_menu_ -> addAction(diagnostics_action_);
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
upgrade_ -> setVisible(true); upgrade_ -> setVisible(true);
+1
View File
@@ -62,6 +62,7 @@ class QETMainWindow : public QMainWindow {
QAction *upgrade_M; ///< Launch browser on QElectroTech MAC_OS_X builds QAction *upgrade_M; ///< Launch browser on QElectroTech MAC_OS_X builds
QAction *donate_; ///< Launch browser to donate link QAction *donate_; ///< Launch browser to donate link
QAction *about_qt_; ///< launch the "About Qt" dialog QAction *about_qt_; ///< launch the "About Qt" dialog
QAction *diagnostics_action_; ///< Open the diagnostics report dialog (discussion #644, step 5)
QMenu *settings_menu_; ///< Settings menu QMenu *settings_menu_; ///< Settings menu
QMenu *help_menu_; ///< Help menu QMenu *help_menu_; ///< Help menu
QMenu *display_toolbars_; ///< Show/hide toolbars/docks QMenu *display_toolbars_; ///< Show/hide toolbars/docks
+28
View File
@@ -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) != "") {
+4
View File
@@ -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;
-117
View File
@@ -1,117 +0,0 @@
/********************************************************************************
** Form generated from reading UI file 'addlinkdialog.ui'
**
** Created: Thu 4. Apr 17:13:59 2013
** by: Qt User Interface Compiler version 4.8.4
**
** WARNING! All changes made in this file will be lost when recompiling UI file!
********************************************************************************/
#ifndef UI_ADDLINKDIALOG_H
#define UI_ADDLINKDIALOG_H
#include <QtCore/QVariant>
#include <QAction>
#include <QApplication>
#include <QButtonGroup>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QFrame>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QSpacerItem>
#include <QVBoxLayout>
QT_BEGIN_NAMESPACE
class Ui_AddLinkDialog
{
public:
QVBoxLayout *verticalLayout;
QFormLayout *formLayout;
QLabel *label;
QLineEdit *titleInput;
QLabel *label_2;
QLineEdit *urlInput;
QSpacerItem *verticalSpacer;
QFrame *line;
QDialogButtonBox *buttonBox;
void setupUi(QDialog *AddLinkDialog)
{
if (AddLinkDialog->objectName().isEmpty())
AddLinkDialog->setObjectName(QString::fromUtf8("AddLinkDialog"));
AddLinkDialog->setSizeGripEnabled(false);
AddLinkDialog->setModal(true);
verticalLayout = new QVBoxLayout(AddLinkDialog);
verticalLayout->setObjectName(QString::fromUtf8("verticalLayout"));
formLayout = new QFormLayout();
formLayout->setObjectName(QString::fromUtf8("formLayout"));
label = new QLabel(AddLinkDialog);
label->setObjectName(QString::fromUtf8("label"));
formLayout->setWidget(0, QFormLayout::LabelRole, label);
titleInput = new QLineEdit(AddLinkDialog);
titleInput->setObjectName(QString::fromUtf8("titleInput"));
titleInput->setMinimumSize(QSize(337, 0));
formLayout->setWidget(0, QFormLayout::FieldRole, titleInput);
label_2 = new QLabel(AddLinkDialog);
label_2->setObjectName(QString::fromUtf8("label_2"));
formLayout->setWidget(1, QFormLayout::LabelRole, label_2);
urlInput = new QLineEdit(AddLinkDialog);
urlInput->setObjectName(QString::fromUtf8("urlInput"));
formLayout->setWidget(1, QFormLayout::FieldRole, urlInput);
verticalLayout->addLayout(formLayout);
verticalSpacer = new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding);
verticalLayout->addItem(verticalSpacer);
line = new QFrame(AddLinkDialog);
line->setObjectName(QString::fromUtf8("line"));
line->setFrameShape(QFrame::HLine);
line->setFrameShadow(QFrame::Sunken);
verticalLayout->addWidget(line);
buttonBox = new QDialogButtonBox(AddLinkDialog);
buttonBox->setObjectName(QString::fromUtf8("buttonBox"));
buttonBox->setOrientation(Qt::Horizontal);
buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok);
verticalLayout->addWidget(buttonBox);
retranslateUi(AddLinkDialog);
QObject::connect(buttonBox, SIGNAL(accepted()), AddLinkDialog, SLOT(accept()));
QObject::connect(buttonBox, SIGNAL(rejected()), AddLinkDialog, SLOT(reject()));
QMetaObject::connectSlotsByName(AddLinkDialog);
} // setupUi
void retranslateUi(QDialog *AddLinkDialog)
{
AddLinkDialog->setWindowTitle(QApplication::translate("AddLinkDialog", "Insert Link", nullptr));
label->setText(QApplication::translate("AddLinkDialog", "Title:", nullptr));
label_2->setText(QApplication::translate("AddLinkDialog", "URL:", nullptr));
} // retranslateUi
};
namespace Ui {
class AddLinkDialog: public Ui_AddLinkDialog {};
} // namespace Ui
QT_END_NAMESPACE
#endif // UI_ADDLINKDIALOG_H
+5 -1
View File
@@ -214,7 +214,11 @@ void NewDiagramPage::applyConf()
rpw->toSettings(settings, "diagrameditor/defaultreport"); rpw->toSettings(settings, "diagrameditor/defaultreport");
// default xref properties // default xref properties
QHash <QString, XRefProperties> hash_xrp = xrefpw -> properties(); const QHash<QString, XRefProperties> hash_xrp = xrefpw->properties();
for (auto it = hash_xrp.constBegin() ; it != hash_xrp.constEnd() ; ++it) {
it.value().toSettings(settings,
QStringLiteral("diagrameditor/defaultxref") % it.key());
}
// Global in QSettings speichern // Global in QSettings speichern
QList<Diagram::Guide> current_guides = m_gpw->guides(); QList<Diagram::Guide> current_guides = m_gpw->guides();
@@ -393,7 +393,7 @@ void GeneralConfigurationPage::fillLang()
ui->m_lang_cb->addItem(QET::Icons::hr, tr("Croate"), "hr"); ui->m_lang_cb->addItem(QET::Icons::hr, tr("Croate"), "hr");
ui->m_lang_cb->addItem(QET::Icons::it, tr("Italien"), "it"); ui->m_lang_cb->addItem(QET::Icons::it, tr("Italien"), "it");
ui->m_lang_cb->addItem(QET::Icons::jp, tr("Japonais"), "ja"); ui->m_lang_cb->addItem(QET::Icons::jp, tr("Japonais"), "ja");
ui->m_lang_cb->addItem(QET::Icons::ko, tr("Coréen"), "ko"); ui->m_lang_cb->addItem(QET::Icons::ko, tr("Coréen"), "ko");
ui->m_lang_cb->addItem(QET::Icons::pl, tr("Polonais"), "pl"); ui->m_lang_cb->addItem(QET::Icons::pl, tr("Polonais"), "pl");
ui->m_lang_cb->addItem(QET::Icons::pt, tr("Portugais"), "pt"); ui->m_lang_cb->addItem(QET::Icons::pt, tr("Portugais"), "pt");
ui->m_lang_cb->addItem(QET::Icons::ro, tr("Roumains"), "ro"); ui->m_lang_cb->addItem(QET::Icons::ro, tr("Roumains"), "ro");
@@ -405,9 +405,9 @@ void GeneralConfigurationPage::fillLang()
ui->m_lang_cb->addItem(QET::Icons::tr, tr("Turc"), "tr"); ui->m_lang_cb->addItem(QET::Icons::tr, tr("Turc"), "tr");
ui->m_lang_cb->addItem(QET::Icons::hu, tr("Hongrois"), "hu"); ui->m_lang_cb->addItem(QET::Icons::hu, tr("Hongrois"), "hu");
ui->m_lang_cb->addItem(QET::Icons::mn, tr("Mongol"), "mn"); ui->m_lang_cb->addItem(QET::Icons::mn, tr("Mongol"), "mn");
ui->m_lang_cb->addItem(QET::Icons::uk, tr("Ukrainien"), "uk"); ui->m_lang_cb->addItem(QET::Icons::uk, tr("Ukrainien"), "uk");
ui->m_lang_cb->addItem(QET::Icons::zh, tr("Chinois"), "zh"); ui->m_lang_cb->addItem(QET::Icons::zh, tr("Chinois"), "zh");
ui->m_lang_cb->addItem(QET::Icons::se, tr("Suédois"), "sv"); ui->m_lang_cb->addItem(QET::Icons::se, tr("Suédois"), "sv");
//set current index to the lang found in setting file //set current index to the lang found in setting file
//if lang doesn't exist set to system //if lang doesn't exist set to system
QSettings settings; QSettings settings;
+113
View File
@@ -0,0 +1,113 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "customelementinfopartwidget.h"
#include "../diagramcontext.h"
#include "../qeticons.h"
#include <QGridLayout>
#include <QLineEdit>
#include <QToolButton>
/**
@brief CustomElementInfoPartWidget::CustomElementInfoPartWidget
Constructor
@param key initial key name (empty for a freshly added row)
@param value initial value
@param parent parent widget
*/
CustomElementInfoPartWidget::CustomElementInfoPartWidget(
const QString &key,
const QString &value,
QWidget *parent) :
QWidget(parent),
m_key_edit(new QLineEdit(key, this)),
m_value_edit(new QLineEdit(value, this)),
m_remove_button(new QToolButton(this))
{
m_key_edit->setPlaceholderText(tr("nom_de_la_propriete"));
m_key_edit->setToolTip(tr("Lettres minuscules, chiffres, tiret et underscore uniquement"));
m_value_edit->setClearButtonEnabled(true);
m_remove_button->setIcon(QET::Icons::Remove);
m_remove_button->setToolTip(tr("Supprimer cette propriété"));
m_remove_button->setAutoRaise(true);
auto *layout = new QGridLayout(this);
layout->setContentsMargins(0, 2, 0, 2);
layout->setVerticalSpacing(2);
layout->setHorizontalSpacing(0);
layout->addWidget(m_key_edit, 0, 0);
layout->addWidget(m_value_edit, 1, 0);
layout->addWidget(m_remove_button, 0, 1, 2, 1);
connect(m_key_edit, &QLineEdit::textChanged, this, &CustomElementInfoPartWidget::validateKey);
connect(m_key_edit, &QLineEdit::textChanged, this, &CustomElementInfoPartWidget::changed);
connect(m_value_edit, &QLineEdit::textChanged, this, &CustomElementInfoPartWidget::changed);
connect(m_remove_button, &QToolButton::clicked, this, [this]() {
emit removeRequested(this);
});
setFocusProxy(m_key_edit);
validateKey();
}
CustomElementInfoPartWidget::~CustomElementInfoPartWidget()
{
}
/**
@return the key name currently typed in this row
*/
QString CustomElementInfoPartWidget::key() const
{
return m_key_edit->text().trimmed();
}
/**
@return the value currently typed in this row
*/
QString CustomElementInfoPartWidget::value() const
{
return m_value_edit->text();
}
/**
@return true if the typed key is non-empty and matches
DiagramContext::isKeyAcceptable()
*/
bool CustomElementInfoPartWidget::hasValidKey() const
{
const QString k = key();
return !k.isEmpty() && DiagramContext::isKeyAcceptable(k);
}
/**
@brief CustomElementInfoPartWidget::validateKey
Flag the key field when it doesn't match the accepted format,
instead of silently dropping it later.
*/
void CustomElementInfoPartWidget::validateKey()
{
const QString k = key();
if (k.isEmpty() || DiagramContext::isKeyAcceptable(k)) {
m_key_edit->setStyleSheet(QString());
} else {
m_key_edit->setStyleSheet(QStringLiteral("border: 1px solid red;"));
}
}

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