mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-08-13 10:04:13 +02:00
Merge branch 'master' into master-modernize-signal-slot
This commit is contained in:
@@ -11,7 +11,6 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/master'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
@@ -145,6 +145,34 @@ else()
|
||||
)
|
||||
endif()
|
||||
|
||||
# Optional precompiled headers -- see QET_ENABLE_PCH in
|
||||
# cmake/developer_options.cmake for what this trades away.
|
||||
#
|
||||
# target_precompile_headers() needs CMake 3.16; the project still declares a
|
||||
# 3.5 minimum, so guard rather than raise it for an opt-in developer feature.
|
||||
#
|
||||
# The generator expressions are load-bearing, not decoration: this target also
|
||||
# compiles the 18 C files of the bundled LZMA decoder
|
||||
# (sources/import/edz/lzma/*.c), and an unguarded list applies to every
|
||||
# language in the target, so the Qt headers would reach the C compiler and fail
|
||||
# with "unknown type name 'namespace'". $<ANGLE-R> is required because a
|
||||
# literal '>' would terminate the generator expression.
|
||||
if(QET_ENABLE_PCH)
|
||||
if(CMAKE_VERSION VERSION_LESS 3.16)
|
||||
message(WARNING
|
||||
"QET_ENABLE_PCH needs CMake 3.16 or newer (found ${CMAKE_VERSION}); "
|
||||
"building without precompiled headers.")
|
||||
else()
|
||||
target_precompile_headers(${PROJECT_NAME} PRIVATE
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<QtCore/QtCore$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<QtGui/QtGui$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<QtWidgets/QtWidgets$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<QtXml/QtXml$<ANGLE-R>>"
|
||||
)
|
||||
message(STATUS "QET_ENABLE_PCH: precompiled headers enabled")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
target_link_libraries(
|
||||
${PROJECT_NAME}
|
||||
PUBLIC
|
||||
|
||||
@@ -33,3 +33,17 @@ add_definitions(-DQT_MESSAGELOGCONTEXT)
|
||||
|
||||
# Build with KF5
|
||||
option(BUILD_WITH_KF5 "Build with KF5" ON)
|
||||
|
||||
# Precompiled headers for the Qt umbrella headers.
|
||||
#
|
||||
# Off by default and intended for local development only. Building QET is
|
||||
# dominated by re-parsing Qt's headers: a 214-line .cpp expands to ~198,000
|
||||
# preprocessed lines, and compiling one translation unit costs ~4.1 s of which
|
||||
# only ~0.35 s is optimisation (-O0 instead of -O3 saves 8%). A PCH caches the
|
||||
# parsed header state and takes that ~4.1 s down to ~1.2 s.
|
||||
#
|
||||
# It is deliberately NOT on by default: a PCH satisfies includes that a source
|
||||
# file forgot to make itself, so code written with it enabled can fail to
|
||||
# compile for everyone else. Leaving it off keeps CI and contributors on the
|
||||
# strict behaviour, and only developers who opt in trade that for the speed.
|
||||
option(QET_ENABLE_PCH "Use precompiled headers (developer build speed; may mask missing #includes)" OFF)
|
||||
|
||||
@@ -116,6 +116,16 @@ set(QET_RES_FILES
|
||||
set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/cli_export.cpp
|
||||
${QET_DIR}/sources/cli_export.h
|
||||
${QET_DIR}/sources/logging/crashhandler.cpp
|
||||
${QET_DIR}/sources/logging/crashhandler.h
|
||||
${QET_DIR}/sources/logging/eventloopwatchdog.cpp
|
||||
${QET_DIR}/sources/logging/eventloopwatchdog.h
|
||||
${QET_DIR}/sources/logging/logring.cpp
|
||||
${QET_DIR}/sources/logging/logring.h
|
||||
${QET_DIR}/sources/logging/qetlogger.cpp
|
||||
${QET_DIR}/sources/logging/qetlogger.h
|
||||
${QET_DIR}/sources/logging/ui/diagnosticsreportdialog.cpp
|
||||
${QET_DIR}/sources/logging/ui/diagnosticsreportdialog.h
|
||||
${QET_DIR}/sources/pdf_links.cpp
|
||||
${QET_DIR}/sources/pdf_links.h
|
||||
${QET_DIR}/sources/import/edz/edzarchive.cpp
|
||||
@@ -205,6 +215,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/exportpropertieswidget.h
|
||||
${QET_DIR}/sources/genericpanel.cpp
|
||||
${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.h
|
||||
${QET_DIR}/sources/main.cpp
|
||||
@@ -530,7 +542,6 @@ set(QET_SRC_FILES
|
||||
|
||||
${QET_DIR}/sources/richtext/richtexteditor.cpp
|
||||
${QET_DIR}/sources/richtext/richtexteditor_p.h
|
||||
${QET_DIR}/sources/richtext/ui_addlinkdialog.h
|
||||
|
||||
${QET_DIR}/sources/SearchAndReplace/searchandreplaceworker.cpp
|
||||
${QET_DIR}/sources/SearchAndReplace/searchandreplaceworker.h
|
||||
@@ -683,6 +694,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/ui/dynamicelementtextitemeditor.h
|
||||
${QET_DIR}/sources/ui/dynamicelementtextmodel.cpp
|
||||
${QET_DIR}/sources/ui/dynamicelementtextmodel.h
|
||||
${QET_DIR}/sources/ui/customelementinfopartwidget.cpp
|
||||
${QET_DIR}/sources/ui/customelementinfopartwidget.h
|
||||
${QET_DIR}/sources/ui/elementinfopartwidget.cpp
|
||||
${QET_DIR}/sources/ui/elementinfopartwidget.h
|
||||
${QET_DIR}/sources/ui/elementinfowidget.cpp
|
||||
@@ -761,6 +774,8 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/undocommand/movediagramcommand.h
|
||||
${QET_DIR}/sources/undocommand/removediagramcommand.cpp
|
||||
${QET_DIR}/sources/undocommand/removediagramcommand.h
|
||||
${QET_DIR}/sources/undocommand/setautonumcontextcommand.cpp
|
||||
${QET_DIR}/sources/undocommand/setautonumcontextcommand.h
|
||||
${QET_DIR}/sources/undocommand/rotateselectioncommand.cpp
|
||||
${QET_DIR}/sources/undocommand/rotateselectioncommand.h
|
||||
${QET_DIR}/sources/undocommand/rotatetextscommand.cpp
|
||||
|
||||
+1027
-743
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
+1021
-743
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+1020
-743
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+1021
-743
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+1044
-768
File diff suppressed because it is too large
Load Diff
+1026
-748
File diff suppressed because it is too large
Load Diff
+1017
-743
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
+1017
-743
File diff suppressed because it is too large
Load Diff
+1017
-743
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
+1047
-771
File diff suppressed because it is too large
Load Diff
+1021
-743
File diff suppressed because it is too large
Load Diff
+1047
-771
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
+1050
-772
File diff suppressed because it is too large
Load Diff
+1049
-771
File diff suppressed because it is too large
Load Diff
+1021
-743
File diff suppressed because it is too large
Load Diff
+1049
-771
File diff suppressed because it is too large
Load Diff
+1051
-771
File diff suppressed because it is too large
Load Diff
+1049
-771
File diff suppressed because it is too large
Load Diff
+1019
-743
File diff suppressed because it is too large
Load Diff
+1017
-743
File diff suppressed because it is too large
Load Diff
+1021
-743
File diff suppressed because it is too large
Load Diff
+1017
-743
File diff suppressed because it is too large
Load Diff
+7
-2
@@ -173,7 +173,9 @@ HEADERS += $$files(sources/*.h) \
|
||||
$$files(sources/qet_elementscaler/*.h) \
|
||||
$$files(sources/svg/*.h) \
|
||||
$$files(sources/import/edz/*.h) \
|
||||
$$files(sources/import/edz/lzma/*.h)
|
||||
$$files(sources/import/edz/lzma/*.h) \
|
||||
$$files(sources/logging/*.h) \
|
||||
$$files(sources/logging/ui/*.h)
|
||||
|
||||
SOURCES += $$files(sources/*.cpp) \
|
||||
$$files(sources/editor/*.cpp) \
|
||||
@@ -219,7 +221,10 @@ SOURCES += $$files(sources/*.cpp) \
|
||||
$$files(sources/qet_elementscaler/*.cpp) \
|
||||
$$files(sources/svg/*.cpp) \
|
||||
$$files(sources/import/edz/*.cpp) \
|
||||
$$files(sources/import/edz/lzma/*.c)
|
||||
$$files(sources/import/edz/lzma/*.c) \
|
||||
$$files(sources/logging/*.cpp) \
|
||||
$$files(sources/logging/ui/*.cpp)
|
||||
|
||||
|
||||
# Needed for use promote QTreeWidget in terminalstripeditor.ui
|
||||
INCLUDEPATH += sources/TerminalStrip/ui
|
||||
|
||||
@@ -22,7 +22,39 @@
|
||||
#include "../qeticons.h"
|
||||
#include "elementslocation.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
#include <QStyle>
|
||||
|
||||
namespace {
|
||||
/**
|
||||
@return the folder icon overlaid with a small warning badge in the
|
||||
bottom-right corner. Used for a directory whose qet_directory could
|
||||
not be read (@see FileElementCollectionItem::m_qet_directory_unreadable),
|
||||
so the problem is visible in the tree itself and not only on hover
|
||||
via the tooltip. Built once: same folder icon, same badge, every time.
|
||||
*/
|
||||
const QIcon &unreadableFolderIcon()
|
||||
{
|
||||
static const QIcon icon = []() {
|
||||
QPixmap pixmap = QET::Icons::Folder.pixmap(16, 16);
|
||||
const QPixmap badge = QApplication::style()
|
||||
->standardIcon(QStyle::SP_MessageBoxWarning)
|
||||
.pixmap(9, 9);
|
||||
|
||||
QPainter painter(&pixmap);
|
||||
painter.drawPixmap(pixmap.width() - badge.width(),
|
||||
pixmap.height() - badge.height(),
|
||||
badge);
|
||||
painter.end();
|
||||
|
||||
return QIcon(pixmap);
|
||||
}();
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief FileElementCollectionItem::FileElementCollectionItem
|
||||
@@ -136,6 +168,14 @@ QString FileElementCollectionItem::localName()
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fall back to the raw directory name (m_path) whenever the
|
||||
// translated name can't be obtained -- qet_directory missing,
|
||||
// unreadable (e.g. a Windows path-encoding issue with special
|
||||
// characters, see bugtracker #332), malformed, or present but
|
||||
// without a usable name entry -- rather than leaving the item
|
||||
// blank.
|
||||
QString display_name;
|
||||
bool readable = false;
|
||||
QString str(fileSystemPath() % "/qet_directory");
|
||||
pugi::xml_document docu;
|
||||
if (docu.load_file(str.toStdWString().c_str()))
|
||||
@@ -143,11 +183,26 @@ QString FileElementCollectionItem::localName()
|
||||
if (QString(docu.document_element().name())
|
||||
== "qet-directory")
|
||||
{
|
||||
readable = true;
|
||||
NamesList nl;
|
||||
nl.fromXml(docu.document_element());
|
||||
setText(nl.name());
|
||||
// Deliberately no fallback argument: a non-empty one
|
||||
// is returned *before* NamesList::name() reaches its
|
||||
// "first available translation" step, so passing
|
||||
// m_path here would replace a perfectly good name in
|
||||
// some other language with the raw directory name.
|
||||
// The fallback belongs after the chain, not inside it.
|
||||
display_name = nl.name();
|
||||
}
|
||||
}
|
||||
setText(display_name.isEmpty() ? m_path : display_name);
|
||||
|
||||
// Only a file-level failure counts: a readable qet-directory
|
||||
// with no entry for the current language is not an error,
|
||||
// NamesList::name() resolves that on its own. Recorded here
|
||||
// and reported by setUpData(), which sets the tooltip after
|
||||
// this runs.
|
||||
m_qet_directory_unreadable = !readable;
|
||||
}
|
||||
}
|
||||
else if (isElement()) {
|
||||
@@ -350,7 +405,21 @@ void FileElementCollectionItem::setUpData()
|
||||
}
|
||||
}
|
||||
|
||||
setToolTip(collectionPath());
|
||||
// Falling back to the raw directory name keeps the folder usable, but
|
||||
// on its own it hides the fact that a file is broken: the user sees a
|
||||
// plausible name and never learns there is anything to repair. Say so
|
||||
// above the collection path, which stays as the last line the way the
|
||||
// element tooltip above builds it.
|
||||
QStringList tip;
|
||||
if (isDir() && m_qet_directory_unreadable)
|
||||
{
|
||||
tip << QObject::tr("Le fichier « %1 » est absent ou illisible : "
|
||||
"le nom traduit de ce dossier n'a pas pu être lu, "
|
||||
"son nom de dossier est affiché à la place.")
|
||||
.arg(fileSystemPath() % "/qet_directory");
|
||||
}
|
||||
tip << collectionPath();
|
||||
setToolTip(tip.join(QLatin1Char('\n')));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -361,6 +430,19 @@ void FileElementCollectionItem::setUpData()
|
||||
*/
|
||||
void FileElementCollectionItem::setUpIcon()
|
||||
{
|
||||
// Must return unconditionally once an icon is set: setIcon() calls
|
||||
// setData(), which emits dataChanged() regardless of whether the new
|
||||
// icon differs from the old one (QIcon has no meaningful equality).
|
||||
// QTreeView responds to dataChanged() by recomputing the row's size
|
||||
// hint, which re-enters data() for this same index -- so without this
|
||||
// guard, any repeated setIcon() here recurses until the stack
|
||||
// overflows. Confirmed by crash report on PR #633.
|
||||
//
|
||||
// This item's m_qet_directory_unreadable is already final by the time
|
||||
// this can run at all: ElementsCollectionModel only attaches itself
|
||||
// to the tree view (making data() reachable) from loadingFinished(),
|
||||
// which fires after the QtConcurrent::map over every item -- this one
|
||||
// included -- has completed. So there is no race to work around here.
|
||||
if (!icon().isNull())
|
||||
return;
|
||||
|
||||
@@ -380,7 +462,8 @@ void FileElementCollectionItem::setUpIcon()
|
||||
else
|
||||
{
|
||||
if (isDir()) {
|
||||
setIcon(QET::Icons::Folder);
|
||||
setIcon(m_qet_directory_unreadable ? unreadableFolderIcon()
|
||||
: QET::Icons::Folder);
|
||||
} else {
|
||||
if (m_path.endsWith(".qetmak")) {
|
||||
setIcon(QIcon());
|
||||
|
||||
@@ -64,6 +64,11 @@ class FileElementCollectionItem : public ElementCollectionItem
|
||||
|
||||
private:
|
||||
QString m_path;
|
||||
/// True when this directory's qet_directory file is missing or
|
||||
/// unreadable, so setUpData() can say so in the tooltip. Recorded
|
||||
/// rather than acted on in localName(), because setUpData() resets
|
||||
/// the tooltip afterwards and would otherwise discard it.
|
||||
bool m_qet_directory_unreadable = false;
|
||||
};
|
||||
|
||||
#endif // FILEELEMENTCOLLECTIONITEM2_H
|
||||
|
||||
@@ -220,6 +220,24 @@ void NumerotationContext::replaceValue(int index, QString content) {
|
||||
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
|
||||
@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();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NumerotationContext::formatValue
|
||||
@param item : a context item as returned by itemAt()
|
||||
@return the part's value, zero-padded exactly as
|
||||
autonum::setSequentialToList() pads it when composing a real label: an
|
||||
explicit format mask wins, then "ten"/"hundred" parts get their
|
||||
implicit 2/3-digit width, "alpha" is used as-is, everything else is a
|
||||
plain number. Kept in step with that function by hand since the two
|
||||
cannot share code without exposing an assignvariables.cpp-local helper.
|
||||
*/
|
||||
QString NumerotationContext::formatValue(const QStringList &item)
|
||||
{
|
||||
const QString &type = item.at(0);
|
||||
const QString &value = item.at(1);
|
||||
if (type == QLatin1String("alpha"))
|
||||
return value;
|
||||
|
||||
const QString mask = formatOf(item);
|
||||
if (!mask.isEmpty())
|
||||
return QString("%1").arg(value.toInt(), mask.length(), 10, QChar('0'));
|
||||
if (type == QLatin1String("ten") || type == QLatin1String("tenfolio"))
|
||||
return QString("%1").arg(value.toInt(), 2, 10, QChar('0'));
|
||||
if (type == QLatin1String("hundred") || type == QLatin1String("hundredfolio"))
|
||||
return QString("%1").arg(value.toInt(), 3, 10, QChar('0'));
|
||||
return QString::number(value.toInt());
|
||||
}
|
||||
|
||||
@@ -54,6 +54,11 @@ class NumerotationContext
|
||||
QDomElement toXml(QDomDocument &, const QString&);
|
||||
void fromXml(QDomElement &);
|
||||
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:
|
||||
QStringList content_;
|
||||
|
||||
@@ -24,10 +24,13 @@
|
||||
#include "../../titleblockproperties.h"
|
||||
#include "../../ui/projectpropertiesdialog.h"
|
||||
#include "../numerotationcontext.h"
|
||||
#include "../numerotationcontextcommands.h"
|
||||
#include "ui_autonumberingdockwidget.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QLineEdit>
|
||||
#include <QSignalBlocker>
|
||||
#include <QSpinBox>
|
||||
|
||||
/**
|
||||
@brief AutoNumberingDockWidget::AutoNumberingDockWidget
|
||||
@@ -64,6 +67,29 @@ void AutoNumberingDockWidget::clear()
|
||||
ui->m_conductor_value_le->clear();
|
||||
ui->m_element_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()
|
||||
@@ -177,9 +203,9 @@ void AutoNumberingDockWidget::setContext()
|
||||
|
||||
//The combo boxes have just been repopulated, so the value fields next
|
||||
//to them are showing whatever the previous project left there.
|
||||
refreshValueField(ui->m_conductor_cb, ui->m_conductor_value_le, AutoNumCategory::Conductor);
|
||||
refreshValueField(ui->m_element_cb, ui->m_element_value_le, AutoNumCategory::Element);
|
||||
refreshValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
|
||||
refreshRow(AutoNumCategory::Conductor);
|
||||
refreshRow(AutoNumCategory::Element);
|
||||
refreshRow(AutoNumCategory::Folio);
|
||||
|
||||
this->setActive();
|
||||
}
|
||||
@@ -255,7 +281,7 @@ void AutoNumberingDockWidget::on_m_conductor_cb_activated(int)
|
||||
m_project->setCurrentConductorAutoNum(current_autonum);
|
||||
m_project_view->currentDiagram()->diagram()->setConductorsAutonumName(current_autonum);
|
||||
m_project_view->currentDiagram()->diagram()->loadCndFolioSeq();
|
||||
refreshValueField(ui->m_conductor_cb, ui->m_conductor_value_le, AutoNumCategory::Conductor);
|
||||
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_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()) {
|
||||
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()
|
||||
@@ -367,6 +394,21 @@ void AutoNumberingDockWidget::on_m_folio_value_le_editingFinished()
|
||||
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
|
||||
@return the numerotation context named by combo_box, for category
|
||||
@@ -433,17 +475,21 @@ int AutoNumberingDockWidget::counterIndex(const NumerotationContext &context)
|
||||
*/
|
||||
void AutoNumberingDockWidget::refreshValueFields()
|
||||
{
|
||||
//Leave alone a field the user is typing in: numbering an element
|
||||
//refreshes all three, and overwriting a half-typed value under the
|
||||
//cursor is worse than showing it a moment out of date. Only this
|
||||
//automatic path skips; an explicit refresh after a reset or an edit
|
||||
//still writes, so the field always ends up canonical.
|
||||
if (!ui->m_conductor_value_le->hasFocus())
|
||||
refreshValueField(ui->m_conductor_cb, ui->m_conductor_value_le, AutoNumCategory::Conductor);
|
||||
if (!ui->m_element_value_le->hasFocus())
|
||||
refreshValueField(ui->m_element_cb, ui->m_element_value_le, AutoNumCategory::Element);
|
||||
if (!ui->m_folio_value_le->hasFocus())
|
||||
refreshValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
|
||||
//Leave alone a row the user is typing in: numbering an element
|
||||
//refreshes all three rows, and overwriting a half-typed value or
|
||||
//increment under the cursor is worse than showing it a moment out
|
||||
//of date. Only this automatic path skips; an explicit refresh after
|
||||
//a reset or an edit still writes, so the row always ends up
|
||||
//canonical. The next-value preview has no such guard: it is
|
||||
//read-only, so there is nothing a refresh could clobber.
|
||||
for (AutoNumCategory category : {AutoNumCategory::Conductor,
|
||||
AutoNumCategory::Element,
|
||||
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();
|
||||
if (typed.isEmpty() || typed == context.itemAt(index).at(1))
|
||||
{
|
||||
refreshValueField(combo_box, line_edit, category);
|
||||
refreshRow(category);
|
||||
return;
|
||||
}
|
||||
|
||||
context.replaceValue(index, typed);
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
refreshRow(category);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
class QComboBox;
|
||||
class QLineEdit;
|
||||
class QSpinBox;
|
||||
|
||||
namespace Ui {
|
||||
class AutoNumberingDockWidget;
|
||||
@@ -66,10 +67,28 @@ class AutoNumberingDockWidget : public QDockWidget
|
||||
void on_m_element_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:
|
||||
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
|
||||
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 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;
|
||||
QETProject* m_project = nullptr;
|
||||
ProjectView* m_project_view = nullptr;
|
||||
|
||||
@@ -15,6 +15,36 @@
|
||||
</property>
|
||||
<widget class="QWidget" name="dockWidgetContents">
|
||||
<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">
|
||||
<widget class="QComboBox" name="m_element_cb"/>
|
||||
</item>
|
||||
@@ -61,6 +91,47 @@
|
||||
</property>
|
||||
</widget>
|
||||
</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">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
@@ -108,6 +179,47 @@
|
||||
</property>
|
||||
</widget>
|
||||
</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">
|
||||
<widget class="QComboBox" name="m_folio_cb"/>
|
||||
</item>
|
||||
@@ -144,6 +256,47 @@
|
||||
</property>
|
||||
</widget>
|
||||
</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">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "qet.h"
|
||||
#include "qetdiagrameditor.h"
|
||||
#include "ui/potentialselectordialog.h"
|
||||
#include "undocommand/setautonumcontextcommand.h"
|
||||
|
||||
/**
|
||||
@brief ConductorAutoNumerotation::ConductorAutoNumerotation
|
||||
@@ -156,7 +157,16 @@ void ConductorAutoNumerotation::newProperties(
|
||||
autonum::setSequential(formula, seq, context, diagram, autoNum_name);
|
||||
|
||||
NumerotationContextCommands ncc (context, diagram);
|
||||
diagram->project()->addConductorAutoNum(autoNum_name, ncc.next());
|
||||
NumerotationContext new_context = ncc.next();
|
||||
|
||||
QETProject *project = diagram->project();
|
||||
auto *undo = new SetAutoNumContextCommand(
|
||||
[project](const QString &k, const NumerotationContext &c) {project->addConductorAutoNum(k, c);},
|
||||
autoNum_name,
|
||||
context,
|
||||
new_context);
|
||||
undo->setText(QObject::tr("Numéroter automatiquement un conducteur", "undo caption"));
|
||||
diagram->undoStack().push(undo);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,7 +255,21 @@ void ConductorAutoNumerotation::numerateNewConductor()
|
||||
autoNum_name);
|
||||
|
||||
NumerotationContextCommands ncc (context, m_diagram);
|
||||
m_diagram->project()->addConductorAutoNum(autoNum_name, ncc.next());
|
||||
NumerotationContext new_context = ncc.next();
|
||||
|
||||
QETProject *project = m_diagram->project();
|
||||
auto setter = [project](const QString &k, const NumerotationContext &c) {project->addConductorAutoNum(k, c);};
|
||||
|
||||
if (m_parent_undo)
|
||||
{
|
||||
new SetAutoNumContextCommand(setter, autoNum_name, context, new_context, m_parent_undo);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto *undo = new SetAutoNumContextCommand(setter, autoNum_name, context, new_context);
|
||||
undo->setText(QObject::tr("Numéroter automatiquement un conducteur", "undo caption"));
|
||||
m_diagram->undoStack().push(undo);
|
||||
}
|
||||
}
|
||||
|
||||
applyText(autonum::AssignVariables::formulaToLabel(
|
||||
|
||||
+19
-1
@@ -67,6 +67,7 @@ Diagram::Diagram(QETProject *project) :
|
||||
m_project (project),
|
||||
use_border_ (true),
|
||||
draw_terminals_ (true),
|
||||
draw_terminal_names_ (true),
|
||||
draw_colored_conductors_ (true),
|
||||
m_event_interface (nullptr),
|
||||
m_freeze_new_elements (false),
|
||||
@@ -2319,6 +2320,7 @@ ExportProperties Diagram::applyProperties(
|
||||
old_properties.draw_border = border_and_titleblock.borderIsDisplayed();
|
||||
old_properties.draw_titleblock = border_and_titleblock.titleBlockIsDisplayed();
|
||||
old_properties.draw_terminals = drawTerminals();
|
||||
old_properties.draw_terminal_names = drawTerminalNames();
|
||||
old_properties.draw_colored_conductors = drawColoredConductors();
|
||||
old_properties.exported_area = useBorder() ? QET::BorderArea
|
||||
: QET::ElementsArea;
|
||||
@@ -2327,6 +2329,7 @@ ExportProperties Diagram::applyProperties(
|
||||
// applique les nouvelles options de rendu
|
||||
setUseBorder (new_properties.exported_area == QET::BorderArea);
|
||||
setDrawTerminals (new_properties.draw_terminals);
|
||||
setDrawTerminalNames (new_properties.draw_terminal_names);
|
||||
setDrawColoredConductors (new_properties.draw_colored_conductors);
|
||||
setDisplayGrid (new_properties.draw_grid);
|
||||
setDisplayGuides (new_properties.draw_guides);
|
||||
@@ -2397,9 +2400,24 @@ QPointF Diagram::snapToGrid(const QPointF &p)
|
||||
\~French true pour afficher les bornes, false sinon
|
||||
*/
|
||||
void Diagram::setDrawTerminals(bool dt) {
|
||||
draw_terminals_ = dt;
|
||||
foreach(QGraphicsItem *qgi, items()) {
|
||||
if (Terminal *t = qgraphicsitem_cast<Terminal *>(qgi)) {
|
||||
t -> setVisible(dt);
|
||||
t -> update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Diagram::setDrawTerminalNames
|
||||
Defines whether or not to display the terminal names/labels
|
||||
@param dt : true to display the terminal names, false otherwise
|
||||
*/
|
||||
void Diagram::setDrawTerminalNames(bool dt) {
|
||||
draw_terminal_names_ = dt;
|
||||
foreach(QGraphicsItem *qgi, items()) {
|
||||
if (Terminal *t = qgraphicsitem_cast<Terminal *>(qgi)) {
|
||||
t -> update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ class Diagram : public QGraphicsScene
|
||||
bool draw_guides_;
|
||||
QList<Diagram::Guide> m_guides_list;
|
||||
bool draw_terminals_;
|
||||
bool draw_terminal_names_;
|
||||
bool draw_colored_conductors_;
|
||||
|
||||
QString m_conductors_autonum_name;
|
||||
@@ -226,6 +227,8 @@ class Diagram : public QGraphicsScene
|
||||
|
||||
bool drawTerminals() const;
|
||||
void setDrawTerminals(bool);
|
||||
bool drawTerminalNames() const;
|
||||
void setDrawTerminalNames(bool);
|
||||
bool drawColoredConductors() const;
|
||||
void setDrawColoredConductors(bool);
|
||||
|
||||
@@ -426,6 +429,15 @@ inline bool Diagram::drawTerminals() const
|
||||
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
|
||||
@return true if conductors colors are rendered, false otherwise.
|
||||
|
||||
@@ -20,11 +20,44 @@
|
||||
#include "../conductorautonumerotation.h"
|
||||
#include "../diagram.h"
|
||||
#include "../undocommand/addgraphicsobjectcommand.h"
|
||||
#include "../undocommand/deleteqgraphicsitemcommand.h"
|
||||
#include "../factory/elementfactory.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../qetdiagrameditor.h"
|
||||
#include "../qetgraphicsitem/element.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
|
||||
@@ -255,9 +288,206 @@ void DiagramEventAddElement::addElement()
|
||||
//added to the scene so new conductor are created between terminal of the new element
|
||||
//and the opposite terminal of 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);
|
||||
new AddGraphicsObjectCommand(conductor, m_diagram, QPointF(), undo_object);
|
||||
@@ -271,7 +501,12 @@ void DiagramEventAddElement::addElement()
|
||||
}
|
||||
m_diagram->addItem(m_element);
|
||||
|
||||
//Autonum the new element before pushing undo_object, so the counter
|
||||
//change it triggers is part of the same undo macro as the element's
|
||||
//own placement (one Ctrl+Z reverts both, instead of silently
|
||||
//leaving the counter advanced).
|
||||
element->setUpFormula(true, undo_object);
|
||||
|
||||
m_diagram -> undoStack().push(undo_object);
|
||||
element->setUpFormula();
|
||||
element->freezeNewAddedElement();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "diagrameventaddshape.h"
|
||||
|
||||
#include "../diagram.h"
|
||||
#include "../lastusedstyle.h"
|
||||
#include "../undocommand/addgraphicsobjectcommand.h"
|
||||
|
||||
/**
|
||||
@@ -77,6 +78,14 @@ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
if (!m_shape_item)
|
||||
{
|
||||
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);
|
||||
event->setAccepted(true);
|
||||
return;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "partplctable.h"
|
||||
|
||||
#include "../../QPropertyUndoCommand/qpropertyundocommand.h"
|
||||
#include "../../qetapp.h"
|
||||
#include "../../QetGraphicsItemModeler/qetgraphicshandleritem.h"
|
||||
#include "../../QetGraphicsItemModeler/qetgraphicshandlerutility.h"
|
||||
#include "../../properties/elementdata.h"
|
||||
@@ -302,7 +303,7 @@ void PartPlcTable::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti
|
||||
|
||||
// Draw column headers
|
||||
QFont header_font = plc_data.headerFont.family().isEmpty()
|
||||
? painter->font() : plc_data.headerFont;
|
||||
? QETApp::diagramTextsFont() : plc_data.headerFont;
|
||||
header_font.setBold(true);
|
||||
painter->setFont(header_font);
|
||||
|
||||
@@ -318,7 +319,7 @@ void PartPlcTable::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti
|
||||
|
||||
// Draw IO rows
|
||||
QFont cell_font = plc_data.cellFont.family().isEmpty()
|
||||
? painter->font() : plc_data.cellFont;
|
||||
? QETApp::diagramTextsFont() : plc_data.cellFont;
|
||||
painter->setFont(cell_font);
|
||||
int start_idx = block_starts.at(block);
|
||||
int end_idx = (block + 1 < block_starts.size())
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
#include <QSignalBlocker>
|
||||
#include <QTableWidgetItem>
|
||||
#include <QHeaderView>
|
||||
#include <QScrollBar>
|
||||
#include <QWheelEvent>
|
||||
#include <QTableWidget>
|
||||
#include <QCheckBox>
|
||||
#include <QGroupBox>
|
||||
@@ -41,6 +43,8 @@
|
||||
#include <QFont>
|
||||
#include <QLineEdit>
|
||||
#include <QSplitter>
|
||||
#include <QShortcut>
|
||||
#include <QMenu>
|
||||
|
||||
/**
|
||||
@brief The EditorDelegate class
|
||||
@@ -666,10 +670,15 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
|
||||
plc_layout->addLayout(toolbar);
|
||||
|
||||
// Tables side by side: IO table (left) + Terminal table (right)
|
||||
auto *tables_splitter = new QSplitter(Qt::Horizontal, m_plc_gb);
|
||||
// Both share a single vertical scrollbar on the right
|
||||
auto *tables_container = new QWidget(m_plc_gb);
|
||||
auto *tables_layout = new QHBoxLayout(tables_container);
|
||||
tables_layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto *splitter = new QSplitter(Qt::Horizontal, tables_container);
|
||||
|
||||
// IO Table
|
||||
m_plc_table = new QTableWidget(tables_splitter);
|
||||
m_plc_table = new QTableWidget(splitter);
|
||||
m_plc_table->setColumnCount(5);
|
||||
m_plc_table->setHorizontalHeaderLabels({
|
||||
tr("Type"), tr("Adresse"), tr("Fonction"),
|
||||
@@ -686,10 +695,10 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
|
||||
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems);
|
||||
m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
m_plc_table->setMinimumHeight(200);
|
||||
tables_splitter->addWidget(m_plc_table);
|
||||
m_plc_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
|
||||
// Terminal table (per IO: Nb + T1-T4)
|
||||
m_plc_terminal_table = new QTableWidget(tables_splitter);
|
||||
m_plc_terminal_table = new QTableWidget(splitter);
|
||||
m_plc_terminal_table->setColumnCount(2);
|
||||
m_plc_terminal_table->setHorizontalHeaderLabels({
|
||||
tr("Nb."), tr("T1")
|
||||
@@ -700,13 +709,61 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
|
||||
m_plc_terminal_table->setSelectionBehavior(QAbstractItemView::SelectItems);
|
||||
m_plc_terminal_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
m_plc_terminal_table->setMinimumHeight(200);
|
||||
tables_splitter->addWidget(m_plc_terminal_table);
|
||||
m_plc_terminal_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
|
||||
tables_splitter->setStretchFactor(0, 3);
|
||||
tables_splitter->setStretchFactor(1, 1);
|
||||
tables_splitter->setSizes({500, 150});
|
||||
// Shared vertical scrollbar
|
||||
m_plc_shared_scrollbar = new QScrollBar(Qt::Vertical, tables_container);
|
||||
m_plc_shared_scrollbar->setMinimum(0);
|
||||
|
||||
plc_layout->addWidget(tables_splitter);
|
||||
splitter->addWidget(m_plc_table);
|
||||
splitter->addWidget(m_plc_terminal_table);
|
||||
splitter->setStretchFactor(0, 3);
|
||||
splitter->setStretchFactor(1, 1);
|
||||
|
||||
tables_layout->addWidget(splitter);
|
||||
tables_layout->addWidget(m_plc_shared_scrollbar);
|
||||
|
||||
// Bidirectional sync between tables and shared scrollbar
|
||||
// Use flag to prevent infinite loops
|
||||
connect(m_plc_shared_scrollbar, &QScrollBar::valueChanged,
|
||||
this, [this](int value) {
|
||||
if (m_plc_scroll_sync) return;
|
||||
m_plc_scroll_sync = true;
|
||||
m_plc_table->verticalScrollBar()->setValue(value);
|
||||
m_plc_terminal_table->verticalScrollBar()->setValue(value);
|
||||
m_plc_scroll_sync = false;
|
||||
});
|
||||
connect(m_plc_table->verticalScrollBar(), &QScrollBar::valueChanged,
|
||||
this, [this](int value) {
|
||||
if (m_plc_scroll_sync) return;
|
||||
m_plc_scroll_sync = true;
|
||||
m_plc_shared_scrollbar->setValue(value);
|
||||
m_plc_terminal_table->verticalScrollBar()->setValue(value);
|
||||
m_plc_scroll_sync = false;
|
||||
});
|
||||
connect(m_plc_terminal_table->verticalScrollBar(), &QScrollBar::valueChanged,
|
||||
this, [this](int value) {
|
||||
if (m_plc_scroll_sync) return;
|
||||
m_plc_scroll_sync = true;
|
||||
m_plc_shared_scrollbar->setValue(value);
|
||||
m_plc_table->verticalScrollBar()->setValue(value);
|
||||
m_plc_scroll_sync = false;
|
||||
});
|
||||
|
||||
// Update shared scrollbar range from both tables
|
||||
auto syncRange = [this]() {
|
||||
int max = qMax(m_plc_table->verticalScrollBar()->maximum(),
|
||||
m_plc_terminal_table->verticalScrollBar()->maximum());
|
||||
m_plc_shared_scrollbar->blockSignals(true);
|
||||
m_plc_shared_scrollbar->setMaximum(max);
|
||||
m_plc_shared_scrollbar->blockSignals(false);
|
||||
};
|
||||
connect(m_plc_table->verticalScrollBar(), &QScrollBar::rangeChanged,
|
||||
this, syncRange);
|
||||
connect(m_plc_terminal_table->verticalScrollBar(), &QScrollBar::rangeChanged,
|
||||
this, syncRange);
|
||||
|
||||
plc_layout->addWidget(tables_container);
|
||||
|
||||
// Font settings
|
||||
auto *font_layout = new QHBoxLayout();
|
||||
@@ -790,12 +847,25 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
|
||||
}
|
||||
plc_layout->addLayout(col_layout);
|
||||
|
||||
// Add to master group box
|
||||
ui->m_master_gb->layout()->addWidget(m_plc_gb);
|
||||
// Add to master group box - row 4, full width
|
||||
auto *gl = qobject_cast<QGridLayout*>(ui->m_master_gb->layout());
|
||||
gl->addWidget(m_plc_gb, 4, 0, 1, 2);
|
||||
|
||||
// Connect signals
|
||||
connect(add_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcAddRow);
|
||||
connect(remove_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcRemoveRow);
|
||||
|
||||
// Ctrl+V shortcut for paste
|
||||
auto *paste_shortcut = new QShortcut(QKeySequence::Paste, m_plc_table);
|
||||
connect(paste_shortcut, &QShortcut::activated, this, &ElementPropertiesEditorWidget::plcPasteFromClipboard);
|
||||
|
||||
// Context menu for the PLC table
|
||||
m_plc_table->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_plc_table, &QTableWidget::customContextMenuRequested, this, [this](const QPoint &pos) {
|
||||
QMenu menu;
|
||||
menu.addAction(tr("Coller depuis le presse-papiers"), this, &ElementPropertiesEditorWidget::plcPasteFromClipboard);
|
||||
menu.exec(m_plc_table->mapToGlobal(pos));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -877,12 +947,12 @@ void ElementPropertiesEditorWidget::populatePlcTable()
|
||||
m_plc_header_font = plc_data.headerFont;
|
||||
m_plc_cell_font = plc_data.cellFont;
|
||||
if (m_plc_header_font.family().isEmpty()) {
|
||||
m_plc_header_font = QFont(m_plc_table->font());
|
||||
m_plc_header_font = QETApp::diagramTextsFont();
|
||||
m_plc_header_font.setBold(true);
|
||||
m_plc_header_font.setPointSize(8);
|
||||
}
|
||||
if (m_plc_cell_font.family().isEmpty()) {
|
||||
m_plc_cell_font = QFont(m_plc_table->font());
|
||||
m_plc_cell_font = QETApp::diagramTextsFont();
|
||||
m_plc_cell_font.setPointSize(8);
|
||||
}
|
||||
m_plc_header_font_btn->setText(tr("Police des en-têtes: %1 %2pt")
|
||||
@@ -920,6 +990,21 @@ void ElementPropertiesEditorWidget::populatePlcTable()
|
||||
hdr->moveSection(hdr->visualIndex(logical), visual);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure scrollbars stay hidden and sync shared scrollbar range
|
||||
if (m_plc_table) {
|
||||
m_plc_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_plc_table->verticalScrollBar()->setVisible(false);
|
||||
}
|
||||
if (m_plc_terminal_table) {
|
||||
m_plc_terminal_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_plc_terminal_table->verticalScrollBar()->setVisible(false);
|
||||
}
|
||||
if (m_plc_shared_scrollbar && m_plc_table) {
|
||||
int max = qMax(m_plc_table->verticalScrollBar()->maximum(),
|
||||
m_plc_terminal_table->verticalScrollBar()->maximum());
|
||||
m_plc_shared_scrollbar->setMaximum(max);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1158,53 +1243,80 @@ void ElementPropertiesEditorWidget::plcPasteFromClipboard()
|
||||
return;
|
||||
|
||||
QStringList lines = clipboard_text.split('\n', Qt::SkipEmptyParts);
|
||||
if (lines.isEmpty())
|
||||
return;
|
||||
|
||||
int start_row = m_plc_table->rowCount();
|
||||
m_plc_table->setRowCount(start_row + lines.size());
|
||||
|
||||
for (int i = 0; i < lines.size(); ++i) {
|
||||
QStringList cells = lines.at(i).split('\t');
|
||||
int row = start_row + i;
|
||||
|
||||
// Type combo
|
||||
auto *type_cb = new QComboBox(m_plc_table);
|
||||
QStringList plc_types = ElementData::plcIOTypeList();
|
||||
for (int t = 0; t < plc_types.size(); ++t) {
|
||||
type_cb->addItem(plc_types.at(t), t);
|
||||
}
|
||||
|
||||
// Try to match type from clipboard
|
||||
if (!cells.isEmpty()) {
|
||||
QString type_str = cells.at(0).trimmed();
|
||||
int type_idx = -1;
|
||||
for (int t = 0; t < plc_types.size(); ++t) {
|
||||
if (plc_types.at(t).compare(type_str, Qt::CaseInsensitive) == 0) {
|
||||
type_idx = t;
|
||||
bool has_tabs = false;
|
||||
for (const QString &line : lines) {
|
||||
if (line.contains('\t')) {
|
||||
has_tabs = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (type_idx >= 0)
|
||||
type_cb->setCurrentIndex(type_idx);
|
||||
|
||||
if (!has_tabs) {
|
||||
// Vertical paste: values go down the same column
|
||||
int target_col = m_plc_table->currentColumn();
|
||||
if (target_col < 0) target_col = 0;
|
||||
int target_row = m_plc_table->currentRow();
|
||||
if (target_row < 0) target_row = 0;
|
||||
int max_rows = m_plc_table->rowCount();
|
||||
|
||||
for (int i = 0; i < lines.size(); ++i) {
|
||||
int row = target_row + i;
|
||||
if (row >= max_rows) break;
|
||||
plcSetCellFromValue(row, target_col, lines.at(i).trimmed());
|
||||
}
|
||||
m_plc_table->setCellWidget(row, 0, type_cb);
|
||||
} else {
|
||||
// Horizontal paste: each line is a separate IO row
|
||||
int target_row = m_plc_table->currentRow();
|
||||
if (target_row < 0) target_row = 0;
|
||||
int max_rows = m_plc_table->rowCount();
|
||||
|
||||
// Address
|
||||
m_plc_table->setItem(row, 1, new QTableWidgetItem(
|
||||
cells.size() > 1 ? cells.at(1).trimmed() : QString()));
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function text
|
||||
m_plc_table->setItem(row, 2, new QTableWidgetItem(
|
||||
cells.size() > 2 ? cells.at(2).trimmed() : QString()));
|
||||
/**
|
||||
* @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;
|
||||
|
||||
// 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);
|
||||
if (col == 0) {
|
||||
auto *type_cb = qobject_cast<QComboBox*>(m_plc_table->cellWidget(row, col));
|
||||
if (!type_cb)
|
||||
return;
|
||||
if (!val.isEmpty()) {
|
||||
QStringList plc_types = ElementData::plcIOTypeList();
|
||||
for (int t = 0; t < plc_types.size(); ++t) {
|
||||
if (plc_types.at(t).compare(val, Qt::CaseInsensitive) == 0) {
|
||||
type_cb->setCurrentIndex(t);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (col == 4) {
|
||||
// CrossRef - read-only
|
||||
auto *item = new QTableWidgetItem(val);
|
||||
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
|
||||
m_plc_table->setItem(row, col, item);
|
||||
}
|
||||
else {
|
||||
// Text columns: Address, Function, Comment
|
||||
m_plc_table->setItem(row, col, new QTableWidgetItem(val));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ class QCheckBox;
|
||||
class QGroupBox;
|
||||
class QPushButton;
|
||||
class QLineEdit;
|
||||
class QScrollBar;
|
||||
|
||||
namespace Ui {
|
||||
class ElementPropertiesEditorWidget;
|
||||
@@ -74,6 +75,7 @@ class ElementPropertiesEditorWidget : public QDialog
|
||||
void plcTerminalCountChanged(int row, int count);
|
||||
void plcSelectHeaderFont();
|
||||
void plcSelectCellFont();
|
||||
void plcSetCellFromValue(int row, int col, const QString &val);
|
||||
|
||||
//ATTRIBUTES
|
||||
private:
|
||||
@@ -95,6 +97,8 @@ class ElementPropertiesEditorWidget : public QDialog
|
||||
QList<QCheckBox *> m_plc_col_visibility_checkboxes;
|
||||
QList<QSpinBox *> m_plc_col_width_spinboxes;
|
||||
QList<QLineEdit *> m_plc_col_name_edits;
|
||||
QScrollBar *m_plc_shared_scrollbar = nullptr;
|
||||
bool m_plc_scroll_sync = false;
|
||||
};
|
||||
|
||||
#endif // ELEMENTPROPERTIESEDITORWIDGET_H
|
||||
|
||||
@@ -39,6 +39,19 @@
|
||||
<item>
|
||||
<widget class="QComboBox" name="m_base_type_cb"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
@@ -94,16 +107,33 @@
|
||||
<string>Élément maître</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="type_concret_layout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Type concret</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<item>
|
||||
<spacer name="type_concret_spacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="m_master_type_cb"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="max_slaves_checkbox">
|
||||
<property name="text">
|
||||
@@ -202,19 +232,6 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="Informations">
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "ElementsCollection/elementcollectionitem.h"
|
||||
#include "ElementsCollection/elementscollectionmodel.h"
|
||||
#include "ElementsCollection/elementstreeview.h"
|
||||
#include "qetapp.h"
|
||||
#include "qetmessagebox.h"
|
||||
#include "qfilenameedit.h"
|
||||
@@ -88,7 +89,7 @@ void ElementDialog::setUpWidget()
|
||||
layout->addWidget(new QLabel(label_));
|
||||
|
||||
|
||||
m_tree_view = new QTreeView(this);
|
||||
m_tree_view = new ElementsTreeView(this);
|
||||
|
||||
m_model = new ElementsCollectionModel(m_tree_view);
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ ElementsPanelWidget::ElementsPanelWidget(QWidget *parent) : QWidget(parent) {
|
||||
prj_edit_prop = new QAction(QET::Icons::DialogInformation, tr("Propriétés du projet"), this);
|
||||
prj_prop_diagram = new QAction(QET::Icons::DialogInformation, tr("Propriétés du folio"), this);
|
||||
prj_add_diagram = new QAction(QET::Icons::DiagramAdd, tr("Ajouter un folio"), this);
|
||||
prj_insert_diagram_above = new QAction(QET::Icons::DiagramAdd, tr("Insérer un folio au-dessus"), this);
|
||||
prj_insert_diagram_below = new QAction(QET::Icons::DiagramAdd, tr("Insérer un folio en dessous"), this);
|
||||
prj_duplicate_diagram = new QAction(QET::Icons::IC_CopyFile, tr("Copier et coller"), this);
|
||||
prj_del_diagram = new QAction(QET::Icons::DiagramDelete, tr("Supprimer ce folio"), this);
|
||||
prj_move_diagram_up = new QAction(QET::Icons::GoUp, tr("Remonter ce folio"), this);
|
||||
@@ -101,6 +103,8 @@ ElementsPanelWidget::ElementsPanelWidget(QWidget *parent) : QWidget(parent) {
|
||||
connect(prj_edit_prop, SIGNAL(triggered()), this, SLOT(editProjectProperties()));
|
||||
connect(prj_prop_diagram, SIGNAL(triggered()), this, SLOT(editDiagramProperties()));
|
||||
connect(prj_add_diagram, SIGNAL(triggered()), this, SLOT(newDiagram()));
|
||||
connect(prj_insert_diagram_above, SIGNAL(triggered()), this, SLOT(insertDiagramAbove()));
|
||||
connect(prj_insert_diagram_below, SIGNAL(triggered()), this, SLOT(insertDiagramBelow()));
|
||||
connect(prj_del_diagram, SIGNAL(triggered()), this, SLOT(deleteDiagram()));
|
||||
connect(prj_duplicate_diagram, SIGNAL(triggered()), this, SLOT(duplicateDiagram()));
|
||||
connect(prj_move_diagram_up, SIGNAL(triggered()), this, SLOT(moveDiagramUp()));
|
||||
@@ -245,6 +249,32 @@ void ElementsPanelWidget::newDiagram()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementsPanelWidget::insertDiagramAbove
|
||||
Emit requestForNewDiagramAt with the position of the currently
|
||||
selected diagram, inserting the new folio right before it.
|
||||
*/
|
||||
void ElementsPanelWidget::insertDiagramAbove()
|
||||
{
|
||||
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
|
||||
QETProject *project = selected_diagram->project();
|
||||
emit(requestForNewDiagramAt(project, project->folioIndex(selected_diagram)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementsPanelWidget::insertDiagramBelow
|
||||
Emit requestForNewDiagramAt with the position right after the
|
||||
currently selected diagram, inserting the new folio right after it.
|
||||
*/
|
||||
void ElementsPanelWidget::insertDiagramBelow()
|
||||
{
|
||||
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
|
||||
QETProject *project = selected_diagram->project();
|
||||
emit(requestForNewDiagramAt(project, project->folioIndex(selected_diagram) + 1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emet le signal requestForDiagramsDeletion avec les schemas selectionnes
|
||||
*/
|
||||
@@ -451,6 +481,8 @@ void ElementsPanelWidget::updateButtons()
|
||||
|
||||
prj_del_diagram -> setEnabled(is_writable);
|
||||
prj_duplicate_diagram -> setEnabled(is_writable);
|
||||
prj_insert_diagram_above -> setEnabled(is_writable);
|
||||
prj_insert_diagram_below -> setEnabled(is_writable);
|
||||
prj_move_diagram_up -> setEnabled(is_writable && min_position > 0);
|
||||
prj_move_diagram_down -> setEnabled(is_writable && max_position < project_diagrams_count - 1);
|
||||
prj_move_diagram_top -> setEnabled(is_writable && min_position > 0);
|
||||
@@ -504,6 +536,8 @@ void ElementsPanelWidget::handleContextMenu(const QPoint &pos) {
|
||||
break;
|
||||
case QET::Diagram:
|
||||
context_menu -> addAction(prj_prop_diagram);
|
||||
context_menu -> addAction(prj_insert_diagram_above);
|
||||
context_menu -> addAction(prj_insert_diagram_below);
|
||||
context_menu -> addAction(prj_del_diagram);
|
||||
context_menu -> addAction(prj_duplicate_diagram);
|
||||
context_menu -> addAction(prj_move_diagram_top);
|
||||
|
||||
@@ -46,6 +46,8 @@ class ElementsPanelWidget : public QWidget {
|
||||
*prj_edit_prop,
|
||||
*prj_prop_diagram,
|
||||
*prj_add_diagram,
|
||||
*prj_insert_diagram_above,
|
||||
*prj_insert_diagram_below,
|
||||
*prj_del_diagram,
|
||||
*prj_duplicate_diagram,
|
||||
*prj_move_diagram_up,
|
||||
@@ -66,6 +68,7 @@ class ElementsPanelWidget : public QWidget {
|
||||
signals:
|
||||
void requestForProject(QETProject *);
|
||||
void requestForNewDiagram(QETProject *);
|
||||
void requestForNewDiagramAt(QETProject *, int);
|
||||
void requestForProjectClosing(QETProject *);
|
||||
void requestForProjectPropertiesEdition(QETProject *);
|
||||
void requestForDiagramPropertiesEdition(Diagram *);
|
||||
@@ -88,6 +91,8 @@ class ElementsPanelWidget : public QWidget {
|
||||
void editProjectProperties();
|
||||
void editDiagramProperties();
|
||||
void newDiagram();
|
||||
void insertDiagramAbove();
|
||||
void insertDiagramBelow();
|
||||
void deleteDiagram();
|
||||
void duplicateDiagram();
|
||||
void moveDiagramUp();
|
||||
|
||||
@@ -34,6 +34,7 @@ ExportProperties::ExportProperties() :
|
||||
draw_border(true),
|
||||
draw_titleblock(true),
|
||||
draw_terminals(false),
|
||||
draw_terminal_names(false),
|
||||
draw_bg_transparent(false),
|
||||
draw_colored_conductors(true),
|
||||
exported_area(QET::BorderArea)
|
||||
@@ -70,6 +71,8 @@ void ExportProperties::toSettings(QSettings &settings,
|
||||
draw_titleblock);
|
||||
settings.setValue(prefix % "drawterminals",
|
||||
draw_terminals);
|
||||
settings.setValue(prefix % "drawterminalnames",
|
||||
draw_terminal_names);
|
||||
settings.setValue(prefix % "drawbgtransparent",
|
||||
draw_bg_transparent);
|
||||
settings.setValue(prefix % "drawcoloredconductors",
|
||||
@@ -105,6 +108,8 @@ void ExportProperties::fromSettings(QSettings &settings,
|
||||
true ).toBool();
|
||||
draw_terminals = settings.value(prefix % "drawterminals",
|
||||
false).toBool();
|
||||
draw_terminal_names = settings.value(prefix % "drawterminalnames",
|
||||
false).toBool();
|
||||
draw_bg_transparent = settings.value(prefix % "drawbgtransparent",
|
||||
false).toBool();
|
||||
draw_colored_conductors = settings.value(
|
||||
|
||||
@@ -47,6 +47,7 @@ class ExportProperties {
|
||||
bool draw_border; ///< Whether to render the border (along with rows/columns headers)
|
||||
bool draw_titleblock; ///< Whether to render the title block
|
||||
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_colored_conductors; ///< Whether to render conductors colors
|
||||
QET::DiagramArea exported_area; ///< Area of diagrams to be rendered
|
||||
|
||||
@@ -63,6 +63,7 @@ ExportProperties ExportPropertiesWidget::exportProperties() const
|
||||
export_properties.draw_border = draw_border -> isChecked();
|
||||
export_properties.draw_titleblock = draw_titleblock -> 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_colored_conductors = draw_colored_conductors -> isChecked();
|
||||
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_titleblock -> setChecked(export_properties.draw_titleblock);
|
||||
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_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);
|
||||
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
|
||||
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
|
||||
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);
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ class ExportPropertiesWidget : public QWidget {
|
||||
QCheckBox *draw_border;
|
||||
QCheckBox *draw_titleblock;
|
||||
QCheckBox *draw_terminals;
|
||||
QCheckBox *draw_terminal_names;
|
||||
QCheckBox *draw_bg_transparent;
|
||||
QCheckBox *draw_colored_conductors;
|
||||
QRadioButton *export_border;
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "lastusedstyle.h"
|
||||
|
||||
QPen LastUsedStyle::m_shape_pen;
|
||||
bool LastUsedStyle::m_has_shape_pen = false;
|
||||
QBrush LastUsedStyle::m_shape_brush;
|
||||
bool LastUsedStyle::m_has_shape_brush = false;
|
||||
QFont LastUsedStyle::m_text_font;
|
||||
bool LastUsedStyle::m_has_text_font = false;
|
||||
|
||||
/**
|
||||
@return true if a shape pen was set this session
|
||||
*/
|
||||
bool LastUsedStyle::hasShapePen()
|
||||
{
|
||||
return m_has_shape_pen;
|
||||
}
|
||||
|
||||
/**
|
||||
@return the last pen applied to a shape this session
|
||||
*/
|
||||
QPen LastUsedStyle::shapePen()
|
||||
{
|
||||
return m_shape_pen;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LastUsedStyle::setShapePen
|
||||
Record @a pen as the last-used shape pen for this session
|
||||
@param pen
|
||||
*/
|
||||
void LastUsedStyle::setShapePen(const QPen &pen)
|
||||
{
|
||||
m_shape_pen = pen;
|
||||
m_has_shape_pen = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@return true if a shape brush was set this session
|
||||
*/
|
||||
bool LastUsedStyle::hasShapeBrush()
|
||||
{
|
||||
return m_has_shape_brush;
|
||||
}
|
||||
|
||||
/**
|
||||
@return the last brush applied to a shape this session
|
||||
*/
|
||||
QBrush LastUsedStyle::shapeBrush()
|
||||
{
|
||||
return m_shape_brush;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LastUsedStyle::setShapeBrush
|
||||
Record @a brush as the last-used shape brush for this session
|
||||
@param brush
|
||||
*/
|
||||
void LastUsedStyle::setShapeBrush(const QBrush &brush)
|
||||
{
|
||||
m_shape_brush = brush;
|
||||
m_has_shape_brush = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@return true if a text font was set this session
|
||||
*/
|
||||
bool LastUsedStyle::hasTextFont()
|
||||
{
|
||||
return m_has_text_font;
|
||||
}
|
||||
|
||||
/**
|
||||
@return the last font applied to a free text item this session
|
||||
*/
|
||||
QFont LastUsedStyle::textFont()
|
||||
{
|
||||
return m_text_font;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LastUsedStyle::setTextFont
|
||||
Record @a font as the last-used free text font for this session
|
||||
@param font
|
||||
*/
|
||||
void LastUsedStyle::setTextFont(const QFont &font)
|
||||
{
|
||||
m_text_font = font;
|
||||
m_has_text_font = true;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef LAST_USED_STYLE_H
|
||||
#define LAST_USED_STYLE_H
|
||||
|
||||
#include <QBrush>
|
||||
#include <QFont>
|
||||
#include <QPen>
|
||||
|
||||
/**
|
||||
@brief The LastUsedStyle class
|
||||
Session-scoped "last used" style for new shapes and free text created
|
||||
on the diagram canvas: whatever pen/brush/font was last applied through
|
||||
the properties editors becomes the starting point for the next new
|
||||
item of that type, the way most drawing tools behave.
|
||||
|
||||
Deliberately in-memory only, not QSettings-backed: this is a live
|
||||
"what did I just use" value for the current editing session, not an
|
||||
app-wide default (that's already covered by the Preferences dialog's
|
||||
font setting, read as the fallback when nothing has been set yet).
|
||||
*/
|
||||
class LastUsedStyle
|
||||
{
|
||||
public:
|
||||
static bool hasShapePen();
|
||||
static QPen shapePen();
|
||||
static void setShapePen(const QPen &pen);
|
||||
|
||||
static bool hasShapeBrush();
|
||||
static QBrush shapeBrush();
|
||||
static void setShapeBrush(const QBrush &brush);
|
||||
|
||||
static bool hasTextFont();
|
||||
static QFont textFont();
|
||||
static void setTextFont(const QFont &font);
|
||||
|
||||
private:
|
||||
LastUsedStyle() = delete;
|
||||
|
||||
static QPen m_shape_pen;
|
||||
static bool m_has_shape_pen;
|
||||
static QBrush m_shape_brush;
|
||||
static bool m_has_shape_brush;
|
||||
static QFont m_text_font;
|
||||
static bool m_has_text_font;
|
||||
};
|
||||
|
||||
#endif // LAST_USED_STYLE_H
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "crashhandler.h"
|
||||
|
||||
#include "logring.h"
|
||||
#include "../qetversion.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QSysInfo>
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include <fcntl.h>
|
||||
#include <io.h>
|
||||
#include <share.h>
|
||||
#include <sys/stat.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
// Everything the handler touches is preallocated here and filled in by
|
||||
// install() (normal context, runs once at startup) -- nothing under the
|
||||
// actual signal/exception path may allocate or touch QString/Qt.
|
||||
const LogRing *g_ring = nullptr;
|
||||
char g_dump_path[1024] = {};
|
||||
char g_header[1024] = {};
|
||||
int g_header_len = 0;
|
||||
|
||||
// Guards against two threads crashing at once, or the handler itself
|
||||
// faulting while dumping: only the first crash writes a dump. See
|
||||
// crashhandler.h invariant 4.
|
||||
std::atomic<bool> g_already_dumped{false};
|
||||
|
||||
#ifndef Q_OS_WIN
|
||||
|
||||
// A stack-overflow SIGSEGV leaves no usable stack for a handler to run
|
||||
// on at all, hence the alternate signal stack (invariant: sized well
|
||||
// above any known SIGSTKSZ so this doesn't depend on
|
||||
// sysconf(_SC_SIGSTKSZ), which some libc versions require at runtime
|
||||
// rather than offering as a compile-time constant).
|
||||
char g_altstack[65536];
|
||||
|
||||
const int kHandledSignals[] = {SIGSEGV, SIGABRT, SIGBUS, SIGFPE, SIGILL};
|
||||
|
||||
void restoreDefaultAndReraise(int sig)
|
||||
{
|
||||
struct sigaction sa {};
|
||||
sa.sa_handler = SIG_DFL;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sa.sa_flags = 0;
|
||||
sigaction(sig, &sa, nullptr);
|
||||
raise(sig);
|
||||
}
|
||||
|
||||
void signalHandler(int sig)
|
||||
{
|
||||
if (g_already_dumped.exchange(true, std::memory_order_acq_rel)) {
|
||||
// Not the first crash (concurrent fault on another thread, or
|
||||
// this handler faulting while dumping): skip straight to
|
||||
// restore-and-re-raise rather than risk a second, interleaved
|
||||
// write to the same file.
|
||||
restoreDefaultAndReraise(sig);
|
||||
return;
|
||||
}
|
||||
|
||||
// open/write/close are all on the POSIX async-signal-safe function
|
||||
// list; nothing else is called here.
|
||||
const int fd = ::open(g_dump_path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
if (fd >= 0) {
|
||||
if (g_header_len > 0) {
|
||||
::write(fd, g_header, static_cast<size_t>(g_header_len));
|
||||
}
|
||||
if (g_ring) {
|
||||
g_ring->dumpToFd(fd);
|
||||
}
|
||||
::close(fd);
|
||||
}
|
||||
|
||||
restoreDefaultAndReraise(sig);
|
||||
}
|
||||
|
||||
#else // Q_OS_WIN
|
||||
|
||||
LONG WINAPI windowsExceptionFilter(EXCEPTION_POINTERS *)
|
||||
{
|
||||
bool expected = false;
|
||||
if (!g_already_dumped.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
int fd = -1;
|
||||
errno_t err = _sopen_s(&fd, g_dump_path,
|
||||
_O_WRONLY | _O_CREAT | _O_TRUNC | _O_BINARY,
|
||||
_SH_DENYWR, _S_IREAD | _S_IWRITE);
|
||||
if (err == 0 && fd >= 0) {
|
||||
if (g_header_len > 0) {
|
||||
_write(fd, g_header, g_header_len);
|
||||
}
|
||||
if (g_ring) {
|
||||
g_ring->dumpToFd(fd);
|
||||
}
|
||||
_close(fd);
|
||||
}
|
||||
|
||||
// Do not suppress Windows Error Reporting / an attached debugger --
|
||||
// same invariant as re-raising on POSIX (see crashhandler.h,
|
||||
// invariant 3).
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
void CrashHandler::install(const LogRing *ring, const QString &dump_path)
|
||||
{
|
||||
g_ring = ring;
|
||||
|
||||
const QByteArray path_utf8 = dump_path.toUtf8();
|
||||
std::strncpy(g_dump_path, path_utf8.constData(), sizeof(g_dump_path) - 1);
|
||||
|
||||
const QByteArray header = QByteArray("QET crash dump\n")
|
||||
+ "Version: " + QetVersion::displayedVersion().toUtf8() + "\n"
|
||||
+ "Git: " GIT_COMMIT_SHA "\n"
|
||||
+ "OS: " + QSysInfo::prettyProductName().toUtf8() + " (" + QSysInfo::currentCpuArchitecture().toUtf8() + ")\n"
|
||||
+ "Qt: " QT_VERSION_STR "\n"
|
||||
+ "---\n";
|
||||
g_header_len = qMin<int>(header.size(), static_cast<int>(sizeof(g_header)) - 1);
|
||||
std::memcpy(g_header, header.constData(), static_cast<size_t>(g_header_len));
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
SetUnhandledExceptionFilter(windowsExceptionFilter);
|
||||
#else
|
||||
stack_t ss;
|
||||
ss.ss_sp = g_altstack;
|
||||
ss.ss_size = sizeof(g_altstack);
|
||||
ss.ss_flags = 0;
|
||||
sigaltstack(&ss, nullptr);
|
||||
|
||||
struct sigaction sa {};
|
||||
sa.sa_handler = signalHandler;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sa.sa_flags = SA_ONSTACK;
|
||||
|
||||
for (int sig : kHandledSignals) {
|
||||
sigaction(sig, &sa, nullptr);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CRASHHANDLER_H
|
||||
#define CRASHHANDLER_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
class LogRing;
|
||||
|
||||
/**
|
||||
@brief The CrashHandler class
|
||||
Discussion #644, step 4: on a fatal crash, flush the in-memory
|
||||
LogRing to a fixed file before the process dies, so the last N log
|
||||
lines leading up to the crash survive it -- today they only exist in
|
||||
memory and are lost with the process.
|
||||
|
||||
This is the highest-risk piece of the whole logging rework (the
|
||||
discussion's own words: "lands last, behind its own switch"), so its
|
||||
invariants are worth restating plainly:
|
||||
|
||||
1. The handler must never block. It takes no locks -- LogRing itself
|
||||
is lock-free for exactly this reason (see logring.h). A handler
|
||||
that can hang is worse than no handler: it turns a clean crash
|
||||
(which at least produces a core dump) into a hung process that has
|
||||
to be force-killed, producing neither a core dump nor a ring dump.
|
||||
2. The handler must never allocate. Under heap corruption -- a
|
||||
plausible *cause* of the very crash being handled -- malloc may
|
||||
itself deadlock or fault. Every buffer this code touches at crash
|
||||
time (the dump path, the header, the ring's own storage) is
|
||||
preallocated by install(), which runs once at startup in normal
|
||||
(non-signal) context.
|
||||
3. The handler must not swallow the crash. After writing the dump it
|
||||
restores the default disposition for the signal and re-raises, so
|
||||
the OS still produces a core dump (POSIX) / Windows Error
|
||||
Reporting still sees the exception. A handler that "fixed" the
|
||||
crash by not re-raising would destroy the post-mortem evidence a
|
||||
core dump provides.
|
||||
4. Only the *first* crash writes a dump. An atomic test-and-set
|
||||
guards against two threads faulting simultaneously (or the handler
|
||||
itself faulting while dumping) producing an interleaved or
|
||||
truncated file; every crash after the first goes straight to
|
||||
restore-and-re-raise.
|
||||
|
||||
Tested in this environment: POSIX/Linux only (sigaction, sigaltstack,
|
||||
SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL). The Windows path
|
||||
(SetUnhandledExceptionFilter) and macOS-specific behaviour (signal
|
||||
handling itself is POSIX and shares the Linux code path, but sandbox
|
||||
profiles can affect where the dump file may be written) are
|
||||
implemented per the discussion's guidance but could not be exercised
|
||||
here -- there is no Windows or macOS build available in this sandbox.
|
||||
Please sanity-check both before relying on them in the field.
|
||||
*/
|
||||
class CrashHandler
|
||||
{
|
||||
public:
|
||||
/// Installs the crash handler. Must be called from normal
|
||||
/// (non-signal) startup code, after the LogRing it will dump
|
||||
/// exists, and only once. `ring` must outlive the process (in
|
||||
/// practice: the LogRing owned by QetLogger's function-local
|
||||
/// static instance, which is never destroyed before exit).
|
||||
/// `dump_path` is resolved and copied into a fixed-size internal
|
||||
/// buffer here; nothing under the actual signal/exception path
|
||||
/// touches QString.
|
||||
static void install(const LogRing *ring, const QString &dump_path);
|
||||
|
||||
private:
|
||||
CrashHandler() = delete;
|
||||
};
|
||||
|
||||
#endif // CRASHHANDLER_H
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "eventloopwatchdog.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QProcessEnvironment>
|
||||
|
||||
EventLoopWatchdog::EventLoopWatchdog(QObject *parent) :
|
||||
QObject(parent)
|
||||
{
|
||||
m_disabled = QProcessEnvironment::systemEnvironment()
|
||||
.value(QStringLiteral("QET_WATCHDOG_DISABLE")) == QStringLiteral("1");
|
||||
|
||||
// Precise, not the default Coarse: Coarse explicitly trades timing
|
||||
// accuracy for power/scheduling efficiency (platform-dependent, but
|
||||
// commonly +/- a double-digit percentage), which would show up as
|
||||
// noise indistinguishable from a real stall in exactly the
|
||||
// measurement this class exists to make trustworthy.
|
||||
m_timer.setTimerType(Qt::PreciseTimer);
|
||||
connect(&m_timer, &QTimer::timeout, this, &EventLoopWatchdog::tick);
|
||||
}
|
||||
|
||||
void EventLoopWatchdog::start()
|
||||
{
|
||||
if (m_disabled)
|
||||
return;
|
||||
|
||||
m_elapsed.start();
|
||||
m_timer.start(kTickIntervalMs);
|
||||
}
|
||||
|
||||
void EventLoopWatchdog::tick()
|
||||
{
|
||||
// restart() returns the elapsed time and resets the clock in one
|
||||
// call, so this tick's own cost is never counted against the next.
|
||||
const qint64 actual_ms = m_elapsed.restart();
|
||||
|
||||
if (actual_ms > kStallThresholdMs) {
|
||||
qWarning() << "EventLoopWatchdog: main thread stalled for"
|
||||
<< actual_ms << "ms (expected a tick every"
|
||||
<< kTickIntervalMs << "ms)";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef EVENTLOOPWATCHDOG_H
|
||||
#define EVENTLOOPWATCHDOG_H
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
|
||||
/**
|
||||
@brief The EventLoopWatchdog class
|
||||
Detects when the main (GUI) thread's event loop goes unresponsive --
|
||||
QetLogger (discussion #644) can only see what an explicit qDebug()/
|
||||
qInfo()/qWarning() call already decided to report, and most of a
|
||||
session (painting, dragging, a slow synchronous operation) produces
|
||||
no log output at all, so a silent multi-second gap in the log is
|
||||
indistinguishable from the user simply not doing anything.
|
||||
|
||||
This closes that gap the direct way: a repeating QTimer::PreciseTimer
|
||||
ticks on a short, fixed interval; each tick measures the *actual*
|
||||
wall-clock time elapsed since the previous one via QElapsedTimer
|
||||
(monotonic -- unaffected by system clock/NTP adjustments, unlike
|
||||
QDateTime). Qt does not queue up missed fires for a normal repeating
|
||||
timer, so if the event loop is blocked for 600ms, the timer fires
|
||||
once as soon as the loop frees up, with ~600ms measured since the
|
||||
last tick -- that gap *is* the stall, measured at its source rather
|
||||
than inferred from log silence.
|
||||
|
||||
Only fires a qWarning() (and so only touches the log at all) when a
|
||||
tick is late by more than kStallThresholdMs, to stay within the
|
||||
spirit of QetLogger's bounded-log design (see its class comment) --
|
||||
a healthy session should produce zero output from this class. This
|
||||
tells you *that* a stall happened and *how long* it was, not what
|
||||
caused it; pair a reported timestamp with `docker exec`+gdb the way
|
||||
the CLI hang (PR #661) was diagnosed to go from "it lagged" to a
|
||||
root cause.
|
||||
|
||||
Escape hatch: if QET_WATCHDOG_DISABLE=1 is set in the environment at
|
||||
construction time, start() does nothing.
|
||||
*/
|
||||
class EventLoopWatchdog : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/// How often the watchdog checks in. Small enough to bound the
|
||||
/// measurement's own granularity, large enough that the tick
|
||||
/// itself is negligible overhead on the event loop it's watching.
|
||||
static constexpr int kTickIntervalMs = 50;
|
||||
|
||||
/// A tick arriving later than this many ms after the previous one
|
||||
/// is logged as a stall. Comfortably above kTickIntervalMs so
|
||||
/// ordinary OS scheduling noise doesn't produce a warning on every
|
||||
/// tick, and in the range a user would actually notice as lag.
|
||||
static constexpr int kStallThresholdMs = 200;
|
||||
|
||||
explicit EventLoopWatchdog(QObject *parent = nullptr);
|
||||
|
||||
/// Starts ticking. Must be called from the main thread, after the
|
||||
/// event loop it watches is about to run (i.e. immediately before
|
||||
/// QApplication::exec()) -- constructing this class earlier is
|
||||
/// harmless, but start() before there is an event loop to tick
|
||||
/// against would just measure the time until app.exec() is
|
||||
/// reached. No-op if QET_WATCHDOG_DISABLE=1 was set at
|
||||
/// construction time.
|
||||
void start();
|
||||
|
||||
private slots:
|
||||
void tick();
|
||||
|
||||
private:
|
||||
QTimer m_timer;
|
||||
QElapsedTimer m_elapsed;
|
||||
bool m_disabled = false;
|
||||
};
|
||||
|
||||
#endif // EVENTLOOPWATCHDOG_H
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "logring.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include <io.h>
|
||||
#else
|
||||
#include <cerrno>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
static_assert(std::atomic<int>::is_always_lock_free,
|
||||
"LogRing::Entry::length must be a lock-free atomic<int> -- "
|
||||
"dumpToFd() reads it from a signal handler and must never block.");
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
@brief writeAllSignalSafe
|
||||
Loops until length bytes have been written to fd or an unrecoverable
|
||||
error occurs. write(2) may write fewer bytes than requested and may
|
||||
return EINTR -- both are *more* likely from inside a signal handler
|
||||
than in normal code, so a single write() call is not enough here.
|
||||
Async-signal-safe: only calls write(2)/errno, nothing else.
|
||||
*/
|
||||
void writeAllSignalSafe(int fd, const char *data, int length) noexcept
|
||||
{
|
||||
int remaining = length;
|
||||
const char *p = data;
|
||||
|
||||
while (remaining > 0) {
|
||||
#ifdef Q_OS_WIN
|
||||
const int n = _write(fd, p, static_cast<unsigned int>(remaining));
|
||||
if (n <= 0) {
|
||||
return;
|
||||
}
|
||||
#else
|
||||
const ssize_t n = ::write(fd, p, static_cast<size_t>(remaining));
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return; // unrecoverable -- give up silently, never block/throw
|
||||
}
|
||||
if (n == 0) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
p += n;
|
||||
remaining -= static_cast<int>(n);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LogRing::LogRing() :
|
||||
// The sized constructor value-initialises each Entry in place; unlike
|
||||
// resize(), it doesn't require Entry to be move/copy-constructible,
|
||||
// which std::atomic<int> deliberately never is. The only allocation
|
||||
// this class ever does.
|
||||
m_entries(kCapacityEntries)
|
||||
{
|
||||
}
|
||||
|
||||
void LogRing::append(const QByteArray &line) noexcept
|
||||
{
|
||||
static const char kMarker[] = "...[ring-truncated]\n";
|
||||
const int marker_len = static_cast<int>(sizeof(kMarker)) - 1;
|
||||
|
||||
const quint64 idx = m_write_cursor.fetch_add(1, std::memory_order_relaxed);
|
||||
Entry &slot = m_entries[static_cast<size_t>(idx % static_cast<quint64>(kCapacityEntries))];
|
||||
|
||||
// Zero the length first so a concurrent reader landing on this exact
|
||||
// slot mid-copy sees "not ready" rather than the previous lap's
|
||||
// (now-being-overwritten) content at a stale length.
|
||||
slot.length.store(0, std::memory_order_relaxed);
|
||||
|
||||
int len;
|
||||
if (line.size() < kEntryBytes) {
|
||||
std::memcpy(slot.data, line.constData(), static_cast<size_t>(line.size()));
|
||||
len = line.size();
|
||||
} else {
|
||||
const int keep = kEntryBytes - marker_len;
|
||||
std::memcpy(slot.data, line.constData(), static_cast<size_t>(keep));
|
||||
std::memcpy(slot.data + keep, kMarker, static_cast<size_t>(marker_len));
|
||||
len = kEntryBytes;
|
||||
}
|
||||
|
||||
slot.length.store(len, std::memory_order_release);
|
||||
}
|
||||
|
||||
QVector<QByteArray> LogRing::snapshot() const
|
||||
{
|
||||
const quint64 cursor = m_write_cursor.load(std::memory_order_acquire);
|
||||
const quint64 cap = static_cast<quint64>(kCapacityEntries);
|
||||
const quint64 count = (cursor < cap) ? cursor : cap;
|
||||
const quint64 start = (cursor < cap) ? 0 : (cursor - cap);
|
||||
|
||||
QVector<QByteArray> result;
|
||||
result.reserve(static_cast<int>(count));
|
||||
|
||||
for (quint64 i = 0; i < count; ++i) {
|
||||
const Entry &slot = m_entries[static_cast<size_t>((start + i) % cap)];
|
||||
const int len = slot.length.load(std::memory_order_acquire);
|
||||
if (len > 0) {
|
||||
result.append(QByteArray(slot.data, len));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void LogRing::dumpToFd(int fd) const noexcept
|
||||
{
|
||||
const quint64 cursor = m_write_cursor.load(std::memory_order_acquire);
|
||||
const quint64 cap = static_cast<quint64>(kCapacityEntries);
|
||||
const quint64 count = (cursor < cap) ? cursor : cap;
|
||||
const quint64 start = (cursor < cap) ? 0 : (cursor - cap);
|
||||
|
||||
for (quint64 i = 0; i < count; ++i) {
|
||||
const Entry &slot = m_entries[static_cast<size_t>((start + i) % cap)];
|
||||
const int len = slot.length.load(std::memory_order_acquire);
|
||||
if (len > 0) {
|
||||
writeAllSignalSafe(fd, slot.data, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LogRing::clear()
|
||||
{
|
||||
m_write_cursor.store(0, std::memory_order_relaxed);
|
||||
for (auto &entry : m_entries) {
|
||||
entry.length.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef LOGRING_H
|
||||
#define LOGRING_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QVector>
|
||||
#include <atomic>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
@brief The LogRing class
|
||||
Fixed-capacity, always-on in-memory ring of the most recent log
|
||||
lines, preallocated once at construction -- append() never
|
||||
allocates.
|
||||
|
||||
Lock-free by construction, not just "thread-safe": step 4 (see
|
||||
crashhandler.h) reads this ring from inside a POSIX signal handler,
|
||||
where taking any lock is unsafe -- if the crashing thread happens to
|
||||
be the one that already holds it (or any other thread does and never
|
||||
gets scheduled again), the handler hangs forever, and you lose both
|
||||
the ring dump *and* the core dump. So there is no mutex here at all:
|
||||
append() claims a slot with a single atomic fetch-add, and
|
||||
dumpToFd()/snapshot() read the preallocated entries directly.
|
||||
|
||||
Accepted tradeoff: if dumpToFd() runs while another thread is
|
||||
mid-append into the exact slot being read (only possible in the
|
||||
crash-handler case, and only for at most one slot), that one entry
|
||||
may be read torn -- part old content, part new. Every other entry is
|
||||
unaffected. This is deliberate: the alternative (a seqlock or similar
|
||||
to detect and retry torn reads) adds real complexity for a window
|
||||
that, per discussion #644, is not worth trading "the handler must
|
||||
never block" against.
|
||||
*/
|
||||
class LogRing
|
||||
{
|
||||
public:
|
||||
static constexpr int kCapacityEntries = 4096;
|
||||
static constexpr int kEntryBytes = 512; // 4096 * 512 = 2 MiB total
|
||||
|
||||
LogRing();
|
||||
|
||||
/// Append one already-formatted, already-truncated log line.
|
||||
/// Bytes beyond kEntryBytes are dropped with a truncation marker.
|
||||
/// Never allocates, never blocks. Safe to call from any normal
|
||||
/// (non-signal) thread concurrently.
|
||||
void append(const QByteArray &line) noexcept;
|
||||
|
||||
/// Snapshot of the entries currently held, oldest first. Normal
|
||||
/// (non-signal) context only.
|
||||
QVector<QByteArray> snapshot() const;
|
||||
|
||||
/// Async-signal-safe: writes every entry currently held to fd via
|
||||
/// write(2) only -- no allocation, no Qt, no locks. May write a
|
||||
/// torn entry under the rare race described above; never blocks.
|
||||
void dumpToFd(int fd) const noexcept;
|
||||
|
||||
void clear();
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
char data[kEntryBytes];
|
||||
std::atomic<int> length{0}; // 0 = not yet written this lap
|
||||
};
|
||||
|
||||
std::vector<Entry> m_entries; // preallocated once, capacity fixed
|
||||
std::atomic<quint64> m_write_cursor{0}; // monotonically increasing
|
||||
};
|
||||
|
||||
#endif // LOGRING_H
|
||||
@@ -0,0 +1,431 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "qetlogger.h"
|
||||
|
||||
#include "crashhandler.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../qetversion.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QSysInfo>
|
||||
#include <cstdio>
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
@brief legacyStderrOutput
|
||||
The QET_LOG_DISABLE=1 escape hatch. Deliberately independent of
|
||||
every other function in this file -- including sanitize()/
|
||||
formatLine(), which are exactly the new code a problem might be in
|
||||
-- so this path stays usable even if the rest of the rework
|
||||
misbehaves. No ring, no file, no rotation, no mutex.
|
||||
*/
|
||||
void legacyStderrOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
|
||||
{
|
||||
const QByteArray local_msg = msg.toLocal8Bit();
|
||||
const char *file = context.file ? context.file : "";
|
||||
const char *function = context.function ? context.function : "";
|
||||
|
||||
const char *level = "Unknown";
|
||||
switch (type) {
|
||||
case QtDebugMsg: level = "Debug"; break;
|
||||
case QtInfoMsg: level = "Info"; break;
|
||||
case QtWarningMsg: level = "Warning"; break;
|
||||
case QtCriticalMsg: level = "Critical"; break;
|
||||
case QtFatalMsg: level = "Fatal"; break;
|
||||
}
|
||||
|
||||
fprintf(stderr, "%s: %s (%s:%u, %s)\n",
|
||||
level, local_msg.constData(), file, context.line, function);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ReentrancyGuard
|
||||
Sets the referenced flag on construction, clears it on destruction
|
||||
(including via early return / exception unwinding). Used as the
|
||||
per-thread guard against the logger recursing into itself.
|
||||
*/
|
||||
struct ReentrancyGuard
|
||||
{
|
||||
bool &flag;
|
||||
explicit ReentrancyGuard(bool &f) : flag(f) {flag = true;}
|
||||
~ReentrancyGuard() {flag = false;}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
@brief QetLogger::instance
|
||||
Function-local static: guaranteed constructed exactly once, in a
|
||||
thread-safe way, on first use -- but the *meaningful* initialisation
|
||||
(log path resolution, opening the file) happens in init(), called
|
||||
explicitly from main() at a defined point, not implicitly on
|
||||
whichever thread happens to log first.
|
||||
*/
|
||||
QetLogger &QetLogger::instance()
|
||||
{
|
||||
static QetLogger logger;
|
||||
return logger;
|
||||
}
|
||||
|
||||
void QetLogger::init()
|
||||
{
|
||||
m_disabled = (qgetenv("QET_LOG_DISABLE") == "1");
|
||||
if (m_disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_log_dir = QETApp::dataDir();
|
||||
m_base_name = QDate::currentDate().toString(QStringLiteral("yyyyMMdd"));
|
||||
|
||||
QMutexLocker locker(&m_file_mutex);
|
||||
m_file_output_ok = ensureFileOpenLocked();
|
||||
}
|
||||
|
||||
void QetLogger::installCrashHandler()
|
||||
{
|
||||
if (m_disabled) {
|
||||
return;
|
||||
}
|
||||
CrashHandler::install(&m_ring, crashDumpPath());
|
||||
}
|
||||
|
||||
QString QetLogger::crashDumpPath() const
|
||||
{
|
||||
return m_log_dir % QStringLiteral("/crash_dump.log");
|
||||
}
|
||||
|
||||
QString QetLogger::currentLogFilePath() const
|
||||
{
|
||||
return m_log_dir % QStringLiteral("/") % m_base_name % QStringLiteral(".log");
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetLogger::ensureFileOpenLocked
|
||||
Caller must hold m_file_mutex. Opens the current session's log file
|
||||
if not already open. Refuses to follow a pre-existing symlink at
|
||||
that path, and creates the file owner-read/write only.
|
||||
*/
|
||||
bool QetLogger::ensureFileOpenLocked()
|
||||
{
|
||||
if (m_file.isOpen()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QDir().mkpath(m_log_dir);
|
||||
|
||||
const QString path = currentLogFilePath();
|
||||
|
||||
const QFileInfo info(path);
|
||||
if (info.exists() && info.isSymLink()) {
|
||||
// Filesystem hardening: refuse a pre-planted symlink rather than
|
||||
// silently appending to whatever it points at.
|
||||
return false;
|
||||
}
|
||||
|
||||
m_file.setFileName(path);
|
||||
if (!m_file.open(QIODevice::WriteOnly | QIODevice::Append)) {
|
||||
return false;
|
||||
}
|
||||
m_file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
|
||||
m_bytes_written_current_file = m_file.size();
|
||||
return true;
|
||||
}
|
||||
|
||||
QString QetLogger::rotatedPath(int index) const
|
||||
{
|
||||
return m_log_dir % QStringLiteral("/") % m_base_name % QStringLiteral(".") % QString::number(index) % QStringLiteral(".log");
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetLogger::rotateLocked
|
||||
Caller must hold m_file_mutex. Shifts .3.log -> .4.log (dropping the
|
||||
previous .4.log), .2.log -> .3.log, .1.log -> .2.log, .log -> .1.log,
|
||||
then opens a fresh, empty current file.
|
||||
*/
|
||||
void QetLogger::rotateLocked()
|
||||
{
|
||||
m_file.close();
|
||||
|
||||
const QString base_path = currentLogFilePath();
|
||||
|
||||
for (int i = kRotationKeep; i >= 1; --i) {
|
||||
const QString from = (i == 1) ? base_path : rotatedPath(i - 1);
|
||||
const QString to = rotatedPath(i);
|
||||
|
||||
if (QFile::exists(to)) {
|
||||
QFile::remove(to);
|
||||
}
|
||||
if (QFile::exists(from)) {
|
||||
QFile::rename(from, to);
|
||||
}
|
||||
}
|
||||
|
||||
m_bytes_written_current_file = 0;
|
||||
m_file_output_ok = ensureFileOpenLocked();
|
||||
}
|
||||
|
||||
void QetLogger::writeToFile(const QByteArray &line, QtMsgType type)
|
||||
{
|
||||
QMutexLocker locker(&m_file_mutex);
|
||||
|
||||
if (!m_file_output_ok) {
|
||||
// Write-failure policy: once file output has failed, stop
|
||||
// attempting it rather than spin-retrying every message. The
|
||||
// ring keeps running regardless.
|
||||
return;
|
||||
}
|
||||
|
||||
const qint64 written = m_file.write(line);
|
||||
if (written != line.size()) {
|
||||
m_file_output_ok = false;
|
||||
m_file.close();
|
||||
return;
|
||||
}
|
||||
m_bytes_written_current_file += written;
|
||||
|
||||
if (type >= QtWarningMsg) {
|
||||
m_file.flush();
|
||||
}
|
||||
|
||||
if (m_bytes_written_current_file >= kMaxFileBytes) {
|
||||
rotateLocked();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetLogger::sanitize
|
||||
Escapes newlines, carriage returns and other control characters.
|
||||
Much of what QET logs is externally controlled (file paths, element
|
||||
names, font strings read out of a .qet file); left unescaped, a
|
||||
crafted string containing '\n' can forge additional log lines.
|
||||
Operates on already-UTF-8-encoded bytes: this is safe because UTF-8
|
||||
continuation bytes are always >= 0x80, so any byte < 0x20 found here
|
||||
is a genuine ASCII control character, never part of a multi-byte
|
||||
sequence.
|
||||
*/
|
||||
QByteArray QetLogger::sanitize(const QByteArray &input)
|
||||
{
|
||||
QByteArray out;
|
||||
out.reserve(input.size());
|
||||
|
||||
for (unsigned char c : input) {
|
||||
if (c == '\n') {
|
||||
out += "\\n";
|
||||
} else if (c == '\r') {
|
||||
out += "\\r";
|
||||
} else if (c == '\t') {
|
||||
out += static_cast<char>(c);
|
||||
} else if (c < 0x20 || c == 0x7F) {
|
||||
out += "\\x";
|
||||
out += QByteArray::number(c, 16).rightJustified(2, '0');
|
||||
} else {
|
||||
out += static_cast<char>(c);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetLogger::truncateMessage
|
||||
Caps a single message at max_bytes, appending a marker stating how
|
||||
many bytes were dropped, so one pathological caller (e.g. dumping an
|
||||
entire XML document to qDebug()) can't consume an unbounded amount
|
||||
of the ring's or file's byte budget.
|
||||
*/
|
||||
QByteArray QetLogger::truncateMessage(const QByteArray &input, int max_bytes)
|
||||
{
|
||||
if (input.size() <= max_bytes) {
|
||||
return input;
|
||||
}
|
||||
|
||||
const int dropped = input.size() - max_bytes;
|
||||
QByteArray out = input.left(max_bytes);
|
||||
out += " ...[truncated ";
|
||||
out += QByteArray::number(dropped);
|
||||
out += " bytes]";
|
||||
return out;
|
||||
}
|
||||
|
||||
QByteArray QetLogger::formatLine(QtMsgType type, const QMessageLogContext &context, const QByteArray &sanitized_msg)
|
||||
{
|
||||
// Includes the date (not just the time) so that a session crossing
|
||||
// midnight -- now kept in a single file -- doesn't read as ambiguous.
|
||||
const QByteArray timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss.zzz")).toUtf8();
|
||||
|
||||
const char *level = "Unknown";
|
||||
switch (type) {
|
||||
case QtDebugMsg: level = "Debug"; break;
|
||||
case QtInfoMsg: level = "Info"; break;
|
||||
case QtWarningMsg: level = "Warning"; break;
|
||||
case QtCriticalMsg: level = "Critical"; break;
|
||||
case QtFatalMsg: level = "Fatal"; break;
|
||||
}
|
||||
|
||||
const char *file = context.file ? context.file : "";
|
||||
const char *function = context.function ? context.function : "";
|
||||
|
||||
QByteArray line = timestamp;
|
||||
line += ' ';
|
||||
line += level;
|
||||
line += ": ";
|
||||
line += sanitized_msg;
|
||||
|
||||
if (type == QtInfoMsg) {
|
||||
line += " \n";
|
||||
} else {
|
||||
line += " (";
|
||||
line += file;
|
||||
line += ":";
|
||||
line += QByteArray::number(context.line ? context.line : 0);
|
||||
line += ", ";
|
||||
line += function;
|
||||
line += ")\n";
|
||||
}
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
void QetLogger::handleMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg)
|
||||
{
|
||||
if (m_disabled) {
|
||||
legacyStderrOutput(type, context, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
static thread_local bool in_handler = false;
|
||||
if (in_handler) {
|
||||
// The logger itself triggered a message (e.g. from inside a Qt
|
||||
// call it made) -- drop it rather than recurse.
|
||||
return;
|
||||
}
|
||||
ReentrancyGuard guard(in_handler);
|
||||
|
||||
const QByteArray sanitized = truncateMessage(sanitize(msg.toUtf8()), kMaxMessageBytes);
|
||||
const QByteArray line = formatLine(type, context, sanitized);
|
||||
|
||||
fwrite(line.constData(), 1, static_cast<size_t>(line.size()), stderr);
|
||||
|
||||
m_ring.append(line);
|
||||
writeToFile(line, type);
|
||||
}
|
||||
|
||||
void QetLogger::pruneOldLogFiles(int days)
|
||||
{
|
||||
if (m_disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QDate today = QDate::currentDate();
|
||||
const QStringList filters = {
|
||||
QStringLiteral("????????.log"), // base files, e.g. 20260803.log
|
||||
QStringLiteral("????????.?.log"), // rotated files, e.g. 20260803.1.log
|
||||
};
|
||||
|
||||
const QDir dir(m_log_dir);
|
||||
const auto entries = dir.entryInfoList(filters, QDir::Files);
|
||||
for (const QFileInfo &file_info : entries) {
|
||||
if (!file_info.isFile()) {
|
||||
continue;
|
||||
}
|
||||
// lastModified(), not lastRead(): reading the log (opening it to
|
||||
// attach to a bug report, a backup job, an indexer) must not
|
||||
// reset the retention clock and keep it alive indefinitely.
|
||||
if (file_info.lastModified().date().daysTo(today) > days) {
|
||||
QFile::remove(file_info.absoluteFilePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Step 5: getting the data back out ----------------------------------
|
||||
|
||||
bool QetLogger::hasPendingCrashDump() const
|
||||
{
|
||||
if (m_disabled) {
|
||||
return false;
|
||||
}
|
||||
const QFileInfo info(crashDumpPath());
|
||||
return info.exists() && info.isFile() && info.size() > 0;
|
||||
}
|
||||
|
||||
QByteArray QetLogger::pendingCrashDumpContents() const
|
||||
{
|
||||
QFile file(crashDumpPath());
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
return QByteArray();
|
||||
}
|
||||
return redact(file.readAll());
|
||||
}
|
||||
|
||||
void QetLogger::clearPendingCrashDump()
|
||||
{
|
||||
QFile::remove(crashDumpPath());
|
||||
}
|
||||
|
||||
QByteArray QetLogger::buildDiagnosticsReport() const
|
||||
{
|
||||
QByteArray header;
|
||||
header += "QElectroTech diagnostics report\n";
|
||||
header += "Generated: " % QDateTime::currentDateTime().toString(Qt::ISODate) % "\n";
|
||||
header += "Version: " % QetVersion::displayedVersion() % "\n";
|
||||
header += "Git: " GIT_COMMIT_SHA "\n";
|
||||
header += "OS: " % QSysInfo::prettyProductName() % " (" % QSysInfo::currentCpuArchitecture() % ")\n";
|
||||
header += "Qt: " QT_VERSION_STR "\n";
|
||||
header += "---\n";
|
||||
|
||||
QByteArray body;
|
||||
QFile file(currentLogFilePath());
|
||||
if (file.open(QIODevice::ReadOnly)) {
|
||||
body = file.readAll();
|
||||
} else {
|
||||
// Fall back to the in-memory ring if the file itself can't be
|
||||
// read (e.g. file output already failed this session).
|
||||
for (const QByteArray &line : m_ring.snapshot()) {
|
||||
body += line;
|
||||
}
|
||||
}
|
||||
|
||||
return redact(header + body);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetLogger::redact
|
||||
Replaces the user's home directory with "~" wherever it appears.
|
||||
Applied before a crash dump or a diagnostics report is ever shown to
|
||||
the user: both are destined to be attached to a public bug tracker,
|
||||
and an absolute path under the home directory leaks the account name
|
||||
(discussion #644's privacy section: "/home/laurent/... leaks a
|
||||
username"). This is the one redaction implemented here; the
|
||||
discussion's fancier "optionally redact project filenames too" is
|
||||
not attempted -- reliably telling a project path apart from
|
||||
arbitrary log text is a much fuzzier problem than a literal prefix
|
||||
match against a known directory.
|
||||
*/
|
||||
QByteArray QetLogger::redact(const QByteArray &input)
|
||||
{
|
||||
const QByteArray home = QDir::homePath().toUtf8();
|
||||
if (home.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
QByteArray out = input;
|
||||
out.replace(home, QByteArrayLiteral("~"));
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef QETLOGGER_H
|
||||
#define QETLOGGER_H
|
||||
|
||||
#include "logring.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QMutex>
|
||||
#include <QString>
|
||||
#include <QtGlobal>
|
||||
|
||||
/**
|
||||
@brief The QetLogger class
|
||||
Rework of QET's diagnostic logging (discussion #644, steps 1-3):
|
||||
|
||||
- Step 1: one file handle held open for the session under a mutex
|
||||
instead of opening/closing per message; the log path (including
|
||||
the date-stamped filename) is resolved exactly once, at init(),
|
||||
instead of being recomputed on every message -- a session that
|
||||
crosses midnight now stays in one file; retention now uses
|
||||
lastModified() instead of lastRead(); stderr and file output both
|
||||
use UTF-8 explicitly (previously stderr used the local 8-bit
|
||||
codec and the file's encoding silently differed between Qt5 and
|
||||
Qt6).
|
||||
- Step 2: the previously-unbounded daily file is now size-capped
|
||||
and rotated (kMaxFileBytes per file, kRotationKeep old files kept
|
||||
beyond the current one); each message is truncated to
|
||||
kMaxMessageBytes and control characters are escaped before being
|
||||
written, so one pathological caller can't blow the size budget or
|
||||
forge log lines; the log file is refused if it already exists as
|
||||
a symlink and is created owner-read/write only.
|
||||
- Step 3: every formatted line is also appended to an in-memory
|
||||
LogRing (see logring.h) -- always on, fixed capacity, allocation-
|
||||
free on the hot path.
|
||||
- Step 4: installCrashHandler() wires the ring up to CrashHandler
|
||||
(see crashhandler.h), so a SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL (or,
|
||||
on Windows, an unhandled structured exception) flushes the ring to
|
||||
a fixed crash-dump file before the process dies.
|
||||
- Step 5: hasPendingCrashDump()/pendingCrashDumpContents()/
|
||||
clearPendingCrashDump() let startup code (see QETApp::checkBackupFiles())
|
||||
notice and offer an unretrieved crash dump from the *previous* run;
|
||||
buildDiagnosticsReport() is the equivalent for a manual "save a
|
||||
report right now" action on the *current*, still-running session.
|
||||
Both go through redact() before ever reaching the user, since both
|
||||
are destined for a public bug tracker.
|
||||
|
||||
Deliberately NOT included: log categories, a full session header
|
||||
beyond what the crash dump/report already carry, repeat collapsing,
|
||||
rate limiting. Those are listed in discussion #644 under "best
|
||||
practices worth building in", not part of the numbered steps.
|
||||
|
||||
Escape hatch: if QET_LOG_DISABLE=1 is set in the environment at
|
||||
init() time, this class does nothing beyond a minimal, independent
|
||||
stderr passthrough -- no ring, no file, no rotation -- so a problem
|
||||
in this rework can be worked around without a rebuild.
|
||||
*/
|
||||
class QetLogger
|
||||
{
|
||||
public:
|
||||
static constexpr qint64 kMaxFileBytes = 2 * 1024 * 1024; // 2 MiB per file
|
||||
static constexpr int kRotationKeep = 4; // .1.log .. .4.log
|
||||
static constexpr int kMaxMessageBytes = 4096; // per-message truncation
|
||||
|
||||
static QetLogger &instance();
|
||||
|
||||
/// Must be called exactly once, from main(), before
|
||||
/// qInstallMessageHandler(). Resolves the log directory and the
|
||||
/// session's log filename, and opens the file.
|
||||
void init();
|
||||
|
||||
/// Step 4: installs the crash handler (see crashhandler.h). Must
|
||||
/// be called after init() (the ring and the dump path must exist
|
||||
/// first) and, like init(), only once.
|
||||
void installCrashHandler();
|
||||
|
||||
/// The function installed via qInstallMessageHandler() forwards here.
|
||||
void handleMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg);
|
||||
|
||||
/// Replaces the old delete_old_log_files(): same call shape, fixed
|
||||
/// to use lastModified() (not lastRead()) and to also match rotated
|
||||
/// file names.
|
||||
void pruneOldLogFiles(int days);
|
||||
|
||||
/// Snapshot of the in-memory ring, oldest first.
|
||||
QVector<QByteArray> ringSnapshot() const {return m_ring.snapshot();}
|
||||
|
||||
// --- Step 5: getting the data back out -------------------------
|
||||
|
||||
/// True if a previous run's crash handler left an unretrieved
|
||||
/// dump behind.
|
||||
bool hasPendingCrashDump() const;
|
||||
|
||||
/// Raw contents of the pending crash dump, or an empty array if
|
||||
/// there isn't one. Does not delete it -- call
|
||||
/// clearPendingCrashDump() once it has been offered to the user.
|
||||
QByteArray pendingCrashDumpContents() const;
|
||||
|
||||
/// Deletes the pending crash dump file. Call after the user has
|
||||
/// been offered it (whether they chose to save it or not) so it
|
||||
/// is never offered a second time.
|
||||
void clearPendingCrashDump();
|
||||
|
||||
/// Builds a redacted diagnostics bundle from the *current* session
|
||||
/// (header + this session's log file so far) for the manual
|
||||
/// "Save report" action -- as opposed to pendingCrashDumpContents(),
|
||||
/// which is about a *previous*, already-terminated session.
|
||||
QByteArray buildDiagnosticsReport() const;
|
||||
|
||||
/// Replaces occurrences of the user's home directory with "~".
|
||||
/// Applied to both the crash dump and buildDiagnosticsReport()
|
||||
/// before they are ever shown to the user, since both are
|
||||
/// destined for a public bug tracker.
|
||||
static QByteArray redact(const QByteArray &input);
|
||||
|
||||
private:
|
||||
QetLogger() = default;
|
||||
QetLogger(const QetLogger &) = delete;
|
||||
|
||||
bool ensureFileOpenLocked();
|
||||
void rotateLocked();
|
||||
void writeToFile(const QByteArray &line, QtMsgType type);
|
||||
QString rotatedPath(int index) const;
|
||||
QString crashDumpPath() const;
|
||||
QString currentLogFilePath() const;
|
||||
|
||||
static QByteArray sanitize(const QByteArray &input);
|
||||
static QByteArray truncateMessage(const QByteArray &input, int max_bytes);
|
||||
static QByteArray formatLine(QtMsgType type, const QMessageLogContext &context, const QByteArray &sanitized_msg);
|
||||
|
||||
bool m_disabled = false;
|
||||
|
||||
QString m_log_dir;
|
||||
QString m_base_name; // e.g. "20260803", resolved once in init()
|
||||
|
||||
QMutex m_file_mutex;
|
||||
QFile m_file;
|
||||
qint64 m_bytes_written_current_file = 0;
|
||||
bool m_file_output_ok = false;
|
||||
|
||||
LogRing m_ring;
|
||||
};
|
||||
|
||||
#endif // QETLOGGER_H
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "diagnosticsreportdialog.h"
|
||||
|
||||
#include "../../qetmessagebox.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFile>
|
||||
#include <QFileDialog>
|
||||
#include <QFontDatabase>
|
||||
#include <QLabel>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
DiagnosticsReportDialog::DiagnosticsReportDialog(
|
||||
const QString &title,
|
||||
const QString &intro,
|
||||
const QByteArray &content,
|
||||
QWidget *parent) :
|
||||
QDialog(parent)
|
||||
{
|
||||
setWindowTitle(title);
|
||||
resize(700, 500);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
|
||||
auto *intro_label = new QLabel(intro, this);
|
||||
intro_label->setWordWrap(true);
|
||||
layout->addWidget(intro_label);
|
||||
|
||||
auto *preview = new QPlainTextEdit(this);
|
||||
preview->setReadOnly(true);
|
||||
preview->setLineWrapMode(QPlainTextEdit::NoWrap);
|
||||
preview->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
|
||||
preview->setPlainText(QString::fromUtf8(content));
|
||||
layout->addWidget(preview);
|
||||
|
||||
auto *buttons = new QDialogButtonBox(this);
|
||||
QPushButton *save_button = buttons->addButton(tr("Enregistrer..."), QDialogButtonBox::ActionRole);
|
||||
buttons->addButton(QDialogButtonBox::Close);
|
||||
connect(save_button, &QPushButton::clicked, this, &DiagnosticsReportDialog::saveToFile);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(buttons->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &QDialog::accept);
|
||||
layout->addWidget(buttons);
|
||||
|
||||
// Stash the content for saveToFile(); the preview widget already
|
||||
// holds a QString copy but we save the original UTF-8 bytes to avoid
|
||||
// any round-trip surprises.
|
||||
setProperty("qet_report_content", content);
|
||||
}
|
||||
|
||||
void DiagnosticsReportDialog::saveToFile()
|
||||
{
|
||||
const QString path = QFileDialog::getSaveFileName(
|
||||
this,
|
||||
tr("Enregistrer le rapport de diagnostic"),
|
||||
QStringLiteral("qet-diagnostic-report.txt"),
|
||||
tr("Fichiers texte (*.txt);;Tous les fichiers (*)"));
|
||||
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
QET::QetMessageBox::critical(
|
||||
this,
|
||||
tr("Erreur"),
|
||||
tr("Impossible d'écrire dans le fichier « %1 ».").arg(path));
|
||||
return;
|
||||
}
|
||||
|
||||
file.write(property("qet_report_content").toByteArray());
|
||||
file.close();
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef DIAGNOSTICSREPORTDIALOG_H
|
||||
#define DIAGNOSTICSREPORTDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
/**
|
||||
@brief The DiagnosticsReportDialog class
|
||||
Discussion #644, step 5: "Show what's in it before saving -- the user
|
||||
is about to attach this to a public tracker." Used for both the
|
||||
after-a-crash offer (QETApp::checkBackupFiles()) and the manual
|
||||
"Help > Diagnostics > Save report" action -- the only difference
|
||||
between the two is the intro text and where the content comes from
|
||||
(QetLogger::pendingCrashDumpContents() vs. buildDiagnosticsReport()).
|
||||
|
||||
The content passed in is expected to already be redacted
|
||||
(QetLogger::redact()) -- this dialog just displays and optionally
|
||||
saves whatever it's given.
|
||||
*/
|
||||
class DiagnosticsReportDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DiagnosticsReportDialog(
|
||||
const QString &title,
|
||||
const QString &intro,
|
||||
const QByteArray &content,
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void saveToFile();
|
||||
};
|
||||
|
||||
#endif // DIAGNOSTICSREPORTDIALOG_H
|
||||
+29
-123
@@ -16,6 +16,8 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "cli_export.h"
|
||||
#include "logging/eventloopwatchdog.h"
|
||||
#include "logging/qetlogger.h"
|
||||
#include "machine_info.h"
|
||||
#include "qet.h"
|
||||
#include "qetapp.h"
|
||||
@@ -62,131 +64,16 @@ class EarlyFileOpenCatcher : public QObject
|
||||
#endif
|
||||
|
||||
/**
|
||||
@brief myMessageOutput
|
||||
for debugging
|
||||
@param type : the messages that can be sent to a message handler
|
||||
@param context : were? wat?
|
||||
@param msg : Message
|
||||
@brief qetLogMessageHandler
|
||||
Installed via qInstallMessageHandler(); forwards to QetLogger, which
|
||||
holds all the actual formatting/ring/rotation state. See
|
||||
logging/qetlogger.h for the rationale (discussion #644).
|
||||
*/
|
||||
void myMessageOutput(QtMsgType type,
|
||||
void qetLogMessageHandler(QtMsgType type,
|
||||
const QMessageLogContext &context,
|
||||
const QString &msg)
|
||||
{
|
||||
|
||||
QString txt=QTime::currentTime().toString("hh:mm:ss.zzz");
|
||||
QByteArray dbs =txt.toLocal8Bit();
|
||||
QByteArray localMsg = msg.toLocal8Bit();
|
||||
const char *file = context.file ? context.file : "";
|
||||
const char *function = context.function ? context.function : "";
|
||||
|
||||
switch (type) {
|
||||
case QtDebugMsg:
|
||||
fprintf(stderr,
|
||||
"%s Debug: %s (%s:%u, %s)\n",
|
||||
dbs.constData(),
|
||||
localMsg.constData(),
|
||||
file,
|
||||
context.line,
|
||||
function);
|
||||
txt+=" Debug: ";
|
||||
break;
|
||||
case QtInfoMsg:
|
||||
fprintf(stderr,
|
||||
"%s Info: %s \n",
|
||||
dbs.constData(),
|
||||
localMsg.constData());
|
||||
txt+=" Info: ";
|
||||
break;
|
||||
case QtWarningMsg:
|
||||
fprintf(stderr,
|
||||
"%s Warning: %s (%s:%u, %s)\n",
|
||||
dbs.constData(),
|
||||
localMsg.constData(),
|
||||
file, context.line,
|
||||
function);
|
||||
txt+=" Warning: ";
|
||||
break;
|
||||
case QtCriticalMsg:
|
||||
fprintf(stderr,
|
||||
"%s Critical: %s (%s:%u, %s)\n",
|
||||
dbs.constData(),
|
||||
localMsg.constData(),
|
||||
file,
|
||||
context.line,
|
||||
function);
|
||||
txt+=" Critical: ";
|
||||
break;
|
||||
case QtFatalMsg:
|
||||
fprintf(stderr,
|
||||
"%s Fatal: %s (%s:%u, %s)\n",
|
||||
dbs.constData(),
|
||||
localMsg.constData(),
|
||||
file,
|
||||
context.line,
|
||||
function);
|
||||
txt+=" Fatal: ";
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr,
|
||||
"%s Unknown: %s (%s:%u, %s)\n",
|
||||
dbs.constData(),
|
||||
localMsg.constData(),
|
||||
file,
|
||||
context.line,
|
||||
function);
|
||||
txt+=" Unknown: ";
|
||||
}
|
||||
txt+= msg;
|
||||
if(type==QtInfoMsg){
|
||||
txt+=" \n";
|
||||
} else {
|
||||
txt+= " (";
|
||||
txt+= context.file ? context.file : "";
|
||||
txt+= ":";
|
||||
txt+=QString::number(context.line ? context.line :0);
|
||||
txt+= ", ";
|
||||
txt+= context.function ? context.function : "";
|
||||
txt+=")\n";
|
||||
}
|
||||
QFile outFile(QETApp::dataDir()
|
||||
+"/"
|
||||
+QDate::currentDate().toString("yyyyMMdd")
|
||||
+".log");
|
||||
if(outFile.open(QIODevice::WriteOnly | QIODevice::Append))
|
||||
{
|
||||
QTextStream ts(&outFile);
|
||||
ts << txt;
|
||||
}
|
||||
outFile.close();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief delete_old_log_files
|
||||
delete old log files
|
||||
@param days : max days old
|
||||
*/
|
||||
void delete_old_log_files(int days)
|
||||
{
|
||||
const QDate today = QDate::currentDate();
|
||||
const QString path = QETApp::dataDir() % "/";
|
||||
|
||||
QString filter("%1%1%1%1%1%1%1%1.log"); // pattern
|
||||
filter = filter.arg("[0123456789]"); // valid characters
|
||||
|
||||
Q_FOREACH (auto fileInfo,
|
||||
QDir(path).entryInfoList(
|
||||
QStringList(filter),
|
||||
QDir::Files))
|
||||
{
|
||||
if (fileInfo.lastRead().date().daysTo(today) > days)
|
||||
{
|
||||
QString filepath = fileInfo.absoluteFilePath();
|
||||
QDir deletefile;
|
||||
deletefile.setPath(filepath);
|
||||
deletefile.remove(filepath);
|
||||
qDebug() << "File " % filepath % " is deleted!";
|
||||
}
|
||||
}
|
||||
QetLogger::instance().handleMessage(type, context, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,13 +140,25 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the logger's state (log directory, session filename, open
|
||||
// file handle) explicitly here, immediately before installing the
|
||||
// handler -- not implicitly on whichever thread happens to log
|
||||
// first. See QetLogger::init().
|
||||
//
|
||||
// Install the log-file message handler BEFORE the application starts:
|
||||
// QETApp's constructor does the whole startup (collections, editor,
|
||||
// opening the projects given on the command line), so installing the
|
||||
// handler afterwards - as was done in the startup worker below - meant
|
||||
// exactly the interesting lines (collection and project load timers)
|
||||
// went to stderr, which is invisible in a Windows GUI session.
|
||||
qInstallMessageHandler(myMessageOutput);
|
||||
QetLogger::instance().init();
|
||||
qInstallMessageHandler(qetLogMessageHandler);
|
||||
// Step 4 (discussion #644): flush the ring to a crash-dump file if
|
||||
// the process dies from here on. Installed right after the ring
|
||||
// exists (init() just constructed it) and as early as reasonably
|
||||
// possible, so it also covers whatever runs between here and
|
||||
// QETApp's own construction below.
|
||||
QetLogger::instance().installCrashHandler();
|
||||
|
||||
SingleApplication app(argc, argv, true);
|
||||
#ifdef Q_OS_MACOS
|
||||
@@ -308,9 +207,16 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
|
||||
{
|
||||
qInfo("Start-up");
|
||||
// delete old log files of max 7 days old.
|
||||
delete_old_log_files(7);
|
||||
QetLogger::instance().pruneOldLogFiles(7);
|
||||
MachineInfo::instance()->send_info_to_debug();
|
||||
});
|
||||
|
||||
// Constructed here rather than earlier: start() measures ticks against
|
||||
// the event loop app.exec() is about to run, so there is no point
|
||||
// (and no accurate baseline) before this line.
|
||||
EventLoopWatchdog watchdog;
|
||||
watchdog.start();
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "ElementsCollection/elementcollectionitem.h"
|
||||
#include "ElementsCollection/elementscollectionmodel.h"
|
||||
#include "ElementsCollection/elementstreeview.h"
|
||||
#include "NameList/ui/namelistwidget.h"
|
||||
#include "editor/ui/qetelementeditor.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"));
|
||||
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->hideElement();
|
||||
|
||||
@@ -161,6 +161,7 @@ ProjectPrintWindow::ProjectPrintWindow(QETProject *project, QPrinter *printer, Q
|
||||
ui->m_draw_border_cb->setChecked(exp.draw_border);
|
||||
ui->m_draw_titleblock_cb->setChecked(exp.draw_titleblock);
|
||||
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_date_cb->blockSignals(true);
|
||||
@@ -523,6 +524,7 @@ ExportProperties ProjectPrintWindow::exportProperties() const
|
||||
exp.draw_border = ui->m_draw_border_cb->isChecked();
|
||||
exp.draw_titleblock = ui->m_draw_titleblock_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_grid = 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_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_names_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()
|
||||
{
|
||||
|
||||
@@ -55,6 +55,7 @@ class ProjectPrintWindow : public QMainWindow
|
||||
void on_m_draw_titleblock_cb_clicked();
|
||||
void on_m_keep_conductor_color_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_use_full_page_cb_clicked();
|
||||
void on_m_zoom_out_action_triggered();
|
||||
|
||||
@@ -183,6 +183,16 @@
|
||||
</property>
|
||||
</widget>
|
||||
</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>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -842,7 +842,7 @@ void ProjectView::initWidgets()
|
||||
QHBoxLayout *TopRightCorner_Layout = new QHBoxLayout();
|
||||
TopRightCorner_Layout->setContentsMargins(0,0,0,0);
|
||||
// some place left to the 'next_right_view_button' button
|
||||
TopRightCorner_Layout->insertSpacing(1,10);
|
||||
TopRightCorner_Layout->addSpacing(10);
|
||||
|
||||
QHBoxLayout *TopLeftCorner_Layout = new QHBoxLayout();
|
||||
TopLeftCorner_Layout->setContentsMargins(0,0,0,0);
|
||||
|
||||
@@ -267,6 +267,211 @@ QDomElement ElementData::kindInfoToXml(QDomDocument &document)
|
||||
return returned_elmt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ElementData::plcMasterDataToXml
|
||||
* Serialize PLC master data to XML (used by Element::toXml for diagram instances)
|
||||
*/
|
||||
QDomElement ElementData::plcMasterDataToXml(QDomDocument &document) const
|
||||
{
|
||||
auto xml_plc = document.createElement(QStringLiteral("plcMasterData"));
|
||||
xml_plc.setAttribute(QStringLiteral("rowHeight"),
|
||||
QString::number(m_plc_master_data.rowHeight, 'f', 2));
|
||||
|
||||
// Save break positions
|
||||
{
|
||||
auto xml_breaks = document.createElement(QStringLiteral("breakPositions"));
|
||||
for (int bp : m_plc_master_data.breakPositions) {
|
||||
auto xml_bp = document.createElement(QStringLiteral("break"));
|
||||
xml_bp.appendChild(document.createTextNode(QString::number(bp)));
|
||||
xml_breaks.appendChild(xml_bp);
|
||||
}
|
||||
xml_plc.appendChild(xml_breaks);
|
||||
}
|
||||
|
||||
// Save column widths
|
||||
auto xml_col_widths = document.createElement(QStringLiteral("columnWidths"));
|
||||
for (auto it = m_plc_master_data.colWidths.constBegin();
|
||||
it != m_plc_master_data.colWidths.constEnd(); ++it) {
|
||||
auto xml_col = document.createElement(QStringLiteral("column"));
|
||||
xml_col.setAttribute(QStringLiteral("index"), it.key());
|
||||
xml_col.setAttribute(QStringLiteral("width"), QString::number(it.value(), 'f', 2));
|
||||
xml_col_widths.appendChild(xml_col);
|
||||
}
|
||||
xml_plc.appendChild(xml_col_widths);
|
||||
|
||||
// Save column visibility
|
||||
auto xml_col_vis = document.createElement(QStringLiteral("columnVisibility"));
|
||||
for (auto it = m_plc_master_data.colVisible.constBegin();
|
||||
it != m_plc_master_data.colVisible.constEnd(); ++it) {
|
||||
auto xml_col = document.createElement(QStringLiteral("column"));
|
||||
xml_col.setAttribute(QStringLiteral("index"), it.key());
|
||||
xml_col.setAttribute(QStringLiteral("visible"), it.value() ? "true" : "false");
|
||||
xml_col_vis.appendChild(xml_col);
|
||||
}
|
||||
xml_plc.appendChild(xml_col_vis);
|
||||
|
||||
// Save fonts
|
||||
if (!m_plc_master_data.headerFont.family().isEmpty()) {
|
||||
auto xml_hfont = document.createElement(QStringLiteral("headerFont"));
|
||||
xml_hfont.setAttribute(QStringLiteral("family"), m_plc_master_data.headerFont.family());
|
||||
xml_hfont.setAttribute(QStringLiteral("size"), m_plc_master_data.headerFont.pointSize());
|
||||
xml_hfont.setAttribute(QStringLiteral("bold"), m_plc_master_data.headerFont.bold() ? "true" : "false");
|
||||
xml_plc.appendChild(xml_hfont);
|
||||
}
|
||||
if (!m_plc_master_data.cellFont.family().isEmpty()) {
|
||||
auto xml_cfont = document.createElement(QStringLiteral("cellFont"));
|
||||
xml_cfont.setAttribute(QStringLiteral("family"), m_plc_master_data.cellFont.family());
|
||||
xml_cfont.setAttribute(QStringLiteral("size"), m_plc_master_data.cellFont.pointSize());
|
||||
xml_cfont.setAttribute(QStringLiteral("bold"), m_plc_master_data.cellFont.bold() ? "true" : "false");
|
||||
xml_plc.appendChild(xml_cfont);
|
||||
}
|
||||
|
||||
// Save custom column names
|
||||
if (!m_plc_master_data.columnNames.isEmpty()) {
|
||||
auto xml_names = document.createElement(QStringLiteral("columnNames"));
|
||||
for (int i = 0; i < m_plc_master_data.columnNames.size(); ++i) {
|
||||
auto xml_name = document.createElement(QStringLiteral("column"));
|
||||
xml_name.setAttribute(QStringLiteral("index"), i);
|
||||
xml_name.appendChild(document.createTextNode(m_plc_master_data.columnNames.at(i)));
|
||||
xml_names.appendChild(xml_name);
|
||||
}
|
||||
xml_plc.appendChild(xml_names);
|
||||
}
|
||||
|
||||
// Save column order
|
||||
if (!m_plc_master_data.columnOrder.isEmpty()) {
|
||||
auto xml_order = document.createElement(QStringLiteral("columnOrder"));
|
||||
QString order_str;
|
||||
for (int i = 0; i < m_plc_master_data.columnOrder.size(); ++i) {
|
||||
if (i > 0) order_str += QStringLiteral(",");
|
||||
order_str += QString::number(m_plc_master_data.columnOrder.at(i));
|
||||
}
|
||||
xml_order.appendChild(document.createTextNode(order_str));
|
||||
xml_plc.appendChild(xml_order);
|
||||
}
|
||||
|
||||
// Save showHeaders
|
||||
{
|
||||
auto xml_sh = document.createElement(QStringLiteral("showHeaders"));
|
||||
xml_sh.appendChild(document.createTextNode(
|
||||
m_plc_master_data.showHeaders ? QStringLiteral("1") : QStringLiteral("0")));
|
||||
xml_plc.appendChild(xml_sh);
|
||||
}
|
||||
|
||||
// Save IO entries
|
||||
auto xml_ios = document.createElement(QStringLiteral("plcIOs"));
|
||||
for (const auto &io : m_plc_master_data.ios) {
|
||||
auto xml_io = document.createElement(QStringLiteral("plcIO"));
|
||||
xml_io.setAttribute(QStringLiteral("type"), plcIOTypeToString(io.type));
|
||||
xml_io.setAttribute(QStringLiteral("address"), io.address);
|
||||
xml_io.setAttribute(QStringLiteral("functionText"), io.functionText);
|
||||
xml_io.setAttribute(QStringLiteral("comment"), io.comment);
|
||||
xml_io.setAttribute(QStringLiteral("crossRef"), io.crossRef);
|
||||
xml_io.setAttribute(QStringLiteral("terminalCount"), io.terminalCount);
|
||||
for (const auto &t : io.terminals) {
|
||||
auto xml_term = document.createElement(QStringLiteral("terminal"));
|
||||
xml_term.appendChild(document.createTextNode(t));
|
||||
xml_io.appendChild(xml_term);
|
||||
}
|
||||
xml_ios.appendChild(xml_io);
|
||||
}
|
||||
xml_plc.appendChild(xml_ios);
|
||||
|
||||
return xml_plc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ElementData::plcMasterDataFromXml
|
||||
* Deserialize PLC master data from XML
|
||||
*/
|
||||
void ElementData::plcMasterDataFromXml(const QDomElement &xml_plc)
|
||||
{
|
||||
if (xml_plc.isNull())
|
||||
return;
|
||||
|
||||
// Reset PLC data before loading to avoid appending to existing data
|
||||
m_plc_master_data = PlcMasterData();
|
||||
|
||||
m_plc_master_data.rowHeight = xml_plc.attribute(
|
||||
QStringLiteral("rowHeight"), QStringLiteral("8.0")).toDouble();
|
||||
|
||||
// Load break positions
|
||||
auto xml_breaks = xml_plc.firstChildElement(QStringLiteral("breakPositions"));
|
||||
for (const auto &xml_bp : QETXML::findInDomElement(xml_breaks, QStringLiteral("break"))) {
|
||||
m_plc_master_data.breakPositions.append(xml_bp.text().toInt());
|
||||
}
|
||||
|
||||
// Load column widths
|
||||
auto xml_col_widths = xml_plc.firstChildElement(QStringLiteral("columnWidths"));
|
||||
for (const auto &xml_col : QETXML::findInDomElement(xml_col_widths, QStringLiteral("column"))) {
|
||||
int idx = xml_col.attribute(QStringLiteral("index")).toInt();
|
||||
qreal w = xml_col.attribute(QStringLiteral("width")).toDouble();
|
||||
m_plc_master_data.colWidths.insert(idx, w);
|
||||
}
|
||||
|
||||
// Load column visibility
|
||||
auto xml_col_vis = xml_plc.firstChildElement(QStringLiteral("columnVisibility"));
|
||||
for (const auto &xml_col : QETXML::findInDomElement(xml_col_vis, QStringLiteral("column"))) {
|
||||
int idx = xml_col.attribute(QStringLiteral("index")).toInt();
|
||||
bool vis = xml_col.attribute(QStringLiteral("visible")) == QLatin1String("true");
|
||||
m_plc_master_data.colVisible.insert(idx, vis);
|
||||
}
|
||||
|
||||
// Load fonts
|
||||
auto xml_hfont = xml_plc.firstChildElement(QStringLiteral("headerFont"));
|
||||
if (!xml_hfont.isNull()) {
|
||||
m_plc_master_data.headerFont.setFamily(xml_hfont.attribute(QStringLiteral("family")));
|
||||
m_plc_master_data.headerFont.setPointSize(xml_hfont.attribute(QStringLiteral("size")).toInt());
|
||||
m_plc_master_data.headerFont.setBold(xml_hfont.attribute(QStringLiteral("bold")) == QLatin1String("true"));
|
||||
}
|
||||
auto xml_cfont = xml_plc.firstChildElement(QStringLiteral("cellFont"));
|
||||
if (!xml_cfont.isNull()) {
|
||||
m_plc_master_data.cellFont.setFamily(xml_cfont.attribute(QStringLiteral("family")));
|
||||
m_plc_master_data.cellFont.setPointSize(xml_cfont.attribute(QStringLiteral("size")).toInt());
|
||||
m_plc_master_data.cellFont.setBold(xml_cfont.attribute(QStringLiteral("bold")) == QLatin1String("true"));
|
||||
}
|
||||
|
||||
// Load custom column names
|
||||
auto xml_names = xml_plc.firstChildElement(QStringLiteral("columnNames"));
|
||||
for (const auto &xml_col : QETXML::findInDomElement(xml_names, QStringLiteral("column"))) {
|
||||
int idx = xml_col.attribute(QStringLiteral("index")).toInt();
|
||||
while (m_plc_master_data.columnNames.size() <= idx)
|
||||
m_plc_master_data.columnNames.append(QString());
|
||||
m_plc_master_data.columnNames.replace(idx, xml_col.text());
|
||||
}
|
||||
|
||||
// Load column order
|
||||
auto xml_order = xml_plc.firstChildElement(QStringLiteral("columnOrder"));
|
||||
if (!xml_order.isNull()) {
|
||||
QStringList order_str_list = xml_order.text().split(',');
|
||||
for (const auto &s : order_str_list) {
|
||||
m_plc_master_data.columnOrder.append(s.trimmed().toInt());
|
||||
}
|
||||
}
|
||||
|
||||
// Load showHeaders
|
||||
auto xml_sh = xml_plc.firstChildElement(QStringLiteral("showHeaders"));
|
||||
if (!xml_sh.isNull()) {
|
||||
m_plc_master_data.showHeaders = xml_sh.text() == QLatin1String("1");
|
||||
}
|
||||
|
||||
// Load IO entries
|
||||
auto xml_ios = xml_plc.firstChildElement(QStringLiteral("plcIOs"));
|
||||
for (const auto &xml_io : QETXML::findInDomElement(xml_ios, QStringLiteral("plcIO"))) {
|
||||
PlcIO io;
|
||||
io.type = plcIOTypeFromString(xml_io.attribute(QStringLiteral("type")));
|
||||
io.address = xml_io.attribute(QStringLiteral("address"));
|
||||
io.functionText = xml_io.attribute(QStringLiteral("functionText"));
|
||||
io.comment = xml_io.attribute(QStringLiteral("comment"));
|
||||
io.crossRef = xml_io.attribute(QStringLiteral("crossRef"));
|
||||
io.terminalCount = xml_io.attribute(QStringLiteral("terminalCount")).toInt();
|
||||
for (const auto &xml_term : QETXML::findInDomElement(xml_io, QStringLiteral("terminal"))) {
|
||||
io.terminals.append(xml_term.text());
|
||||
}
|
||||
m_plc_master_data.ios.append(io);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ElementData::setTerminalType
|
||||
* Override the terminal type by \p t_type
|
||||
|
||||
@@ -130,6 +130,11 @@ class ElementData : public PropertiesInterface
|
||||
QList<int> columnOrder; ///< Column display order (logical indices)
|
||||
bool showHeaders = true; ///< Show column headers on sheet
|
||||
|
||||
PlcMasterData() {
|
||||
headerFont.setFamily(QString());
|
||||
cellFont.setFamily(QString());
|
||||
}
|
||||
|
||||
bool operator==(const PlcMasterData &other) const {
|
||||
return ios == other.ios
|
||||
&& breakPositions == other.breakPositions
|
||||
@@ -201,6 +206,8 @@ class ElementData : public PropertiesInterface
|
||||
QDomElement toXml(QDomDocument &xml_element) const override;
|
||||
bool fromXml(const QDomElement &xml_element) override;
|
||||
QDomElement kindInfoToXml(QDomDocument &document);
|
||||
QDomElement plcMasterDataToXml(QDomDocument &document) const;
|
||||
void plcMasterDataFromXml(const QDomElement &xml_plc);
|
||||
|
||||
void setTerminalType(ElementData::TerminalType t_type);
|
||||
ElementData::TerminalType terminalType() const;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
#include "terminaldata.h"
|
||||
|
||||
#include "../qetapp.h"
|
||||
#include "../utils/qetutils.h"
|
||||
|
||||
#include <QGraphicsObject>
|
||||
@@ -38,6 +39,7 @@ TerminalData::TerminalData(QGraphicsObject *parent):
|
||||
|
||||
void TerminalData::init()
|
||||
{
|
||||
m_label_font = QETApp::diagramTextsFont();
|
||||
}
|
||||
|
||||
TerminalData::~TerminalData()
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
#include "machine_info.h"
|
||||
#include "TerminalStrip/ui/terminalstripeditorwindow.h"
|
||||
#include "qetversion.h"
|
||||
#include "logging/qetlogger.h"
|
||||
#include "logging/ui/diagnosticsreportdialog.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
@@ -2577,6 +2579,10 @@ void QETApp::checkBackupFiles()
|
||||
}
|
||||
|
||||
if (stale_files.isEmpty()) {
|
||||
// Only offer an unretrieved crash dump when there's no project
|
||||
// to recover this run -- discussion #644 step 5 is explicit
|
||||
// that the two prompts must never both show at once.
|
||||
checkCrashDump();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
Updates the booleans concerning the state of the windows
|
||||
|
||||
@@ -271,6 +271,7 @@ class QETApp : public QObject
|
||||
void openTitleBlockTemplateFiles(const QStringList &);
|
||||
void configureQET();
|
||||
void aboutQET();
|
||||
void showDiagnosticsReport();
|
||||
void receiveMessage(int instanceId, QByteArray message);
|
||||
|
||||
private:
|
||||
@@ -287,6 +288,7 @@ class QETApp : public QObject
|
||||
void initSystemTray();
|
||||
void buildSystemTrayMenu();
|
||||
void checkBackupFiles();
|
||||
void checkCrashDump();
|
||||
void fetchWindowStats(
|
||||
const QList<QETDiagramEditor *> &,
|
||||
const QList<QETElementEditor *> &,
|
||||
|
||||
@@ -183,6 +183,7 @@ void QETDiagramEditor::setUpElementsPanel()
|
||||
connect(pa, &ElementsPanelWidget::requestForProjectClosing, this, qOverload<QETProject*>(&QETDiagramEditor::closeProject));
|
||||
connect(pa, SIGNAL(requestForProjectPropertiesEdition (QETProject *)), this, SLOT(editProjectProperties(QETProject *)));
|
||||
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, &ElementsPanelWidget::requestForDiagramsDeletion, this, &QETDiagramEditor::removeDiagrams);
|
||||
connect(pa, &ElementsPanelWidget::requestForDiagramMoveUp, this, &QETDiagramEditor::moveDiagramUp);
|
||||
@@ -367,6 +368,21 @@ void QETDiagramEditor::setUpActions()
|
||||
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
|
||||
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"));
|
||||
@@ -806,6 +822,7 @@ void QETDiagramEditor::setUpToolBar()
|
||||
diagram_tool_bar -> addAction (m_edit_diagram_properties);
|
||||
diagram_tool_bar -> addAction (m_conductor_reset);
|
||||
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->setObjectName("adding");
|
||||
@@ -878,6 +895,8 @@ void QETDiagramEditor::setUpMenu()
|
||||
|
||||
// menu Projet
|
||||
menu_project -> addAction(m_project_edit_properties);
|
||||
menu_project -> addAction(m_auto_conductor);
|
||||
menu_project -> addSeparator();
|
||||
menu_project -> addAction(m_project_add_diagram);
|
||||
menu_project -> addAction(m_remove_diagram_from_project);
|
||||
menu_project -> addAction(m_clean_project);
|
||||
@@ -913,6 +932,7 @@ void QETDiagramEditor::setUpMenu()
|
||||
menu_affichage -> addAction(m_mode_visualise);
|
||||
menu_affichage -> addSeparator();
|
||||
menu_affichage -> addAction(m_draw_grid);
|
||||
menu_affichage -> addAction(m_draw_guides);
|
||||
menu_affichage -> addAction(m_grey_background);
|
||||
menu_affichage -> addSeparator();
|
||||
menu_affichage -> addActions(m_zoom_actions_group.actions());
|
||||
@@ -1280,6 +1300,12 @@ bool QETDiagramEditor::addProject(QETProject *project, bool update_panel)
|
||||
|
||||
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);
|
||||
|
||||
// met a jour le panel d'elements
|
||||
@@ -1889,9 +1915,14 @@ void QETDiagramEditor::slot_updateModeActions()
|
||||
{
|
||||
m_auto_conductor -> setEnabled (true);
|
||||
m_auto_conductor -> setChecked (pv -> project() -> autoConductor());
|
||||
m_auto_break_conductor -> setEnabled (true);
|
||||
m_auto_break_conductor -> setChecked (pv -> project() -> autoBreakConductor());
|
||||
}
|
||||
else
|
||||
{
|
||||
m_auto_conductor -> setDisabled(true);
|
||||
m_auto_break_conductor -> setDisabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2267,6 +2298,25 @@ void QETDiagramEditor::addDiagramToProject(QETProject *project)
|
||||
project_view->project()->addNewDiagram();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETDiagramEditor::addDiagramToProjectAt
|
||||
Add a diagram to project, inserted at a specific position.
|
||||
@param project
|
||||
@param pos
|
||||
*/
|
||||
void QETDiagramEditor::addDiagramToProjectAt(QETProject *project, int pos)
|
||||
{
|
||||
if (!project) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ProjectView *project_view = findProject(project))
|
||||
{
|
||||
activateProject(project);
|
||||
project_view->project()->addNewDiagram(pos);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @brief QETDiagramEditor::removeDiagram
|
||||
* Wrapper für einzelne Diagramme, um Abwärtskompatibilität zu erhalten.
|
||||
@@ -2543,6 +2593,7 @@ void QETDiagramEditor::subWindowActivated(QMdiSubWindow *subWindows)
|
||||
slot_updateWindowsMenu();
|
||||
emit syncElementsPanel();
|
||||
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
|
||||
This slot is called when a diagram selection was changed.
|
||||
|
||||
@@ -98,6 +98,7 @@ class QETDiagramEditor : public QETMainWindow
|
||||
ProjectView *findProject(const QString &) const;
|
||||
QMdiSubWindow *subWindowForWidget(QWidget *) const;
|
||||
void updateUsageTrackersActiveState();
|
||||
void updateWindowModifiedState();
|
||||
|
||||
signals:
|
||||
void syncElementsPanel();
|
||||
@@ -137,6 +138,7 @@ class QETDiagramEditor : public QETMainWindow
|
||||
void editDiagramProperties(DiagramView *);
|
||||
void editDiagramProperties(Diagram *);
|
||||
void addDiagramToProject(QETProject *);
|
||||
void addDiagramToProjectAt(QETProject *, int);
|
||||
void removeDiagram(Diagram *);
|
||||
void removeDiagrams(const QList<Diagram *> &diagrams);
|
||||
void removeDiagramFromProject();
|
||||
@@ -191,7 +193,7 @@ class QETDiagramEditor : public QETMainWindow
|
||||
*redo, ///< Redo the latest cancelled operation
|
||||
*m_paste, ///< Paste clipboard content on the current diagram
|
||||
*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_draw_grid, ///< Switch the background grid 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_remove_diagram_from_project, ///< Delete a diagram from the current project
|
||||
*m_clean_project, ///< Clean the content of the current project by removing useless items
|
||||
*m_project_folio_list, ///< Sommaire des schemas
|
||||
*m_csv_export, ///< generate nomenclature
|
||||
*m_add_nomenclature, ///< Add nomenclature graphics item;
|
||||
*m_add_summary, ///<Add summary graphics item
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "element.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../PropertiesEditor/propertieseditordialog.h"
|
||||
#include "../autoNum/assignvariables.h"
|
||||
@@ -33,6 +34,7 @@
|
||||
#include "../qetgraphicsitem/terminal.h"
|
||||
#include "../ui/elementpropertieswidget.h"
|
||||
#include "../undocommand/changeelementinformationcommand.h"
|
||||
#include "../undocommand/setautonumcontextcommand.h"
|
||||
#include "dynamicelementtextitem.h"
|
||||
#include "elementtextitemgroup.h"
|
||||
#include "iostream"
|
||||
@@ -860,6 +862,15 @@ bool Element::fromXml(QDomElement &e,
|
||||
}
|
||||
}
|
||||
|
||||
//Load PLC master data override from diagram XML
|
||||
if (m_data.m_type == ElementData::Master &&
|
||||
m_data.m_master_type == ElementData::PLC)
|
||||
{
|
||||
auto xml_plc = e.firstChildElement(QStringLiteral("plcMasterData"));
|
||||
if (!xml_plc.isNull())
|
||||
m_data.plcMasterDataFromXml(xml_plc);
|
||||
}
|
||||
|
||||
//We must block the update of the alignment when loading the information
|
||||
//otherwise the pos of the text will not be the same as it was at save time.
|
||||
for(DynamicElementTextItem *deti : m_dynamic_text_list)
|
||||
@@ -993,6 +1004,15 @@ QDomElement Element::toXml(
|
||||
element.appendChild(properties);
|
||||
}
|
||||
|
||||
//Save PLC master data override for elements on diagram
|
||||
if (m_data.m_type == ElementData::Master &&
|
||||
m_data.m_master_type == ElementData::PLC)
|
||||
{
|
||||
auto xml_plc = m_data.plcMasterDataToXml(document);
|
||||
if (!xml_plc.isNull())
|
||||
element.appendChild(xml_plc);
|
||||
}
|
||||
|
||||
//Dynamic texts
|
||||
QDomElement dyn_text = document.createElement(QStringLiteral("dynamic_texts"));
|
||||
for (DynamicElementTextItem *deti : m_dynamic_text_list)
|
||||
@@ -1626,7 +1646,7 @@ void Element::hoverLeaveEvent(QGraphicsSceneHoverEvent *e)
|
||||
(ex K for coil) with condition :
|
||||
formula is empty, text tagged "label" is emptty or "_";
|
||||
*/
|
||||
void Element::setUpFormula(bool code_letter)
|
||||
void Element::setUpFormula(bool code_letter, QUndoCommand *parent_undo)
|
||||
{
|
||||
Q_UNUSED(code_letter)
|
||||
|
||||
@@ -1655,8 +1675,21 @@ void Element::setUpFormula(bool code_letter)
|
||||
nc,
|
||||
diagram(),
|
||||
element_currentAutoNum);
|
||||
diagram()->project()->addElementAutoNum(element_currentAutoNum,
|
||||
ncc.next());
|
||||
|
||||
NumerotationContext new_context = ncc.next();
|
||||
QETProject *project = diagram()->project();
|
||||
auto setter = [project](const QString &k, const NumerotationContext &c) {project->addElementAutoNum(k, c);};
|
||||
|
||||
if (parent_undo)
|
||||
{
|
||||
new SetAutoNumContextCommand(setter, element_currentAutoNum, nc, new_context, parent_undo);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto *undo = new SetAutoNumContextCommand(setter, element_currentAutoNum, nc, new_context);
|
||||
undo->setText(tr("Numéroter automatiquement un élément", "undo caption"));
|
||||
diagram()->undoStack().push(undo);
|
||||
}
|
||||
|
||||
if(!m_freeze_label && !formula.isEmpty())
|
||||
{
|
||||
@@ -1876,12 +1909,12 @@ void Element::drawPlcTable(QPainter *painter)
|
||||
// Fonts
|
||||
QFont header_font = plc_data.headerFont;
|
||||
if (header_font.family().isEmpty()) {
|
||||
header_font = painter->font();
|
||||
header_font = QETApp::diagramTextsFont();
|
||||
header_font.setBold(true);
|
||||
}
|
||||
QFont cell_font = plc_data.cellFont;
|
||||
if (cell_font.family().isEmpty()) {
|
||||
cell_font = painter->font();
|
||||
cell_font = QETApp::diagramTextsFont();
|
||||
}
|
||||
|
||||
for (const QPointF &pos : positions) {
|
||||
|
||||
@@ -35,6 +35,7 @@ class Terminal;
|
||||
class Conductor;
|
||||
class DynamicElementTextItem;
|
||||
class ElementTextItemGroup;
|
||||
class QUndoCommand;
|
||||
|
||||
/**
|
||||
This is the base class for electrical elements.
|
||||
@@ -142,7 +143,7 @@ class Element : public QetGraphicsItem
|
||||
{return m_autoNum_seq;}
|
||||
autonum::sequentialNumbers& rSequenceStruct()
|
||||
{return m_autoNum_seq;}
|
||||
void setUpFormula(bool code_letter = true);
|
||||
void setUpFormula(bool code_letter = true, QUndoCommand *parent_undo = nullptr);
|
||||
void setPrefix(QString);
|
||||
QString getPrefix() const;
|
||||
void freezeLabel(bool freeze);
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "../diagram.h"
|
||||
#include "../diagramcommands.h"
|
||||
#include "../lastusedstyle.h"
|
||||
#include "../qet.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../utils/qetutils.h"
|
||||
@@ -33,7 +34,10 @@
|
||||
IndependentTextItem::IndependentTextItem() :
|
||||
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;
|
||||
setRotation(settings.value("diagrameditor/independent_text_rotation", 0).toInt());
|
||||
}
|
||||
|
||||
@@ -199,6 +199,7 @@ void Terminal::paint(
|
||||
|
||||
// dessin de la borne en rouge
|
||||
// draw the terminal in red
|
||||
if (!diagram() || diagram()->drawTerminals()) {
|
||||
t.setColor(Qt::red);
|
||||
painter -> setPen(t);
|
||||
painter -> drawLine(c, e);
|
||||
@@ -212,6 +213,7 @@ void Terminal::paint(
|
||||
painter -> setRenderHint(QPainter::Antialiasing, true);
|
||||
painter -> drawEllipse(QRectF(c.x() - 2.5, c.y() - 2.5, 5.0, 5.0));
|
||||
} else painter -> drawPoint(c);
|
||||
}
|
||||
|
||||
//Draw help line if needed,
|
||||
if (diagram() && m_draw_help_line)
|
||||
@@ -273,9 +275,10 @@ void Terminal::paint(
|
||||
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();
|
||||
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::TextAntialiasing, true);
|
||||
painter->setFont(d->m_label_font);
|
||||
|
||||
@@ -136,6 +136,12 @@ void QETMainWindow::initCommonActions()
|
||||
about_qt_ = new QAction(QET::Icons::QtLogo, tr("À propos de &Qt"), this);
|
||||
about_qt_ -> setStatusTip(tr("Affiche des informations sur la bibliothèque Qt", "status bar tip"));
|
||||
connect(about_qt_, SIGNAL(triggered()), qApp, SLOT(aboutQt()));
|
||||
|
||||
diagnostics_action_ = new QAction(QET::Icons::DialogInformation, tr("Enregistrer un rapport de diagnostic..."), this);
|
||||
diagnostics_action_ -> setStatusTip(tr("Génère un rapport avec les derniers messages de journalisation, pour l'inclure dans un rapport de bug", "status bar tip"));
|
||||
connect(diagnostics_action_, &QAction::triggered, this, []() {
|
||||
QETApp::instance()->showDiagnosticsReport();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,6 +164,8 @@ void QETMainWindow::initCommonMenus()
|
||||
help_menu_ -> addAction(donate_);
|
||||
help_menu_ -> addAction(about_qt_);
|
||||
help_menu_ -> addAction(about_qet_);
|
||||
help_menu_ -> addSeparator();
|
||||
help_menu_ -> addAction(diagnostics_action_);
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
upgrade_ -> setVisible(true);
|
||||
|
||||
@@ -62,6 +62,7 @@ class QETMainWindow : public QMainWindow {
|
||||
QAction *upgrade_M; ///< Launch browser on QElectroTech MAC_OS_X builds
|
||||
QAction *donate_; ///< Launch browser to donate link
|
||||
QAction *about_qt_; ///< launch the "About Qt" dialog
|
||||
QAction *diagnostics_action_; ///< Open the diagnostics report dialog (discussion #644, step 5)
|
||||
QMenu *settings_menu_; ///< Settings menu
|
||||
QMenu *help_menu_; ///< Help menu
|
||||
QMenu *display_toolbars_; ///< Show/hide toolbars/docks
|
||||
|
||||
@@ -69,6 +69,10 @@ m_project_properties_handler{this}
|
||||
init();
|
||||
|
||||
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"));
|
||||
for (int i = 0; i < size; ++i) {
|
||||
settings.setArrayIndex(i);
|
||||
@@ -927,6 +931,28 @@ void QETProject::setAutoConductor(bool 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
|
||||
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_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")))
|
||||
{
|
||||
NumerotationContext nc;
|
||||
@@ -1861,6 +1888,7 @@ void QETProject::writeDefaultPropertiesXml(QDomElement &xml_element)
|
||||
QDomElement conductor_autonums = xml_document.createElement("conductors_autonums");
|
||||
conductor_autonums.setAttribute("current_autonum", m_current_conductor_autonum);
|
||||
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()) {
|
||||
QDomElement conductor_autonum = conductorAutoNum(key).toXml(xml_document, "conductor_autonum");
|
||||
if (key != "" && conductorAutoNumFormula(key) != "") {
|
||||
|
||||
@@ -90,6 +90,7 @@ class QETProject : public QObject
|
||||
};
|
||||
|
||||
Q_PROPERTY(bool autoConductor READ autoConductor WRITE setAutoConductor)
|
||||
Q_PROPERTY(bool autoBreakConductor READ autoBreakConductor WRITE setAutoBreakConductor)
|
||||
|
||||
// constructors, destructor
|
||||
public:
|
||||
@@ -181,9 +182,11 @@ class QETProject : public QObject
|
||||
void setFreezeNewConductors(bool);
|
||||
|
||||
bool autoConductor () const;
|
||||
bool autoBreakConductor () const;
|
||||
bool autoElement () const;
|
||||
bool autoFolio () const;
|
||||
void setAutoConductor (bool ac);
|
||||
void setAutoBreakConductor (bool abc);
|
||||
void setAutoElement (bool ae);
|
||||
void autoFolioNumberingNewFolios ();
|
||||
void autoFolioNumberingSelectedFolios(int, int, const QString&);
|
||||
@@ -318,6 +321,7 @@ class QETProject : public QObject
|
||||
QHash <QString, NumerotationContext> m_element_autonum; //Title and NumContext hash
|
||||
QString m_current_element_autonum;
|
||||
bool m_auto_conductor = true;
|
||||
bool m_auto_break_conductor = false;
|
||||
XmlElementCollection *m_elements_collection = nullptr;
|
||||
bool m_freeze_new_elements = false;
|
||||
bool m_freeze_new_conductors = false;
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
/********************************************************************************
|
||||
** Form generated from reading UI file 'addlinkdialog.ui'
|
||||
**
|
||||
** Created: Thu 4. Apr 17:13:59 2013
|
||||
** by: Qt User Interface Compiler version 4.8.4
|
||||
**
|
||||
** WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef UI_ADDLINKDIALOG_H
|
||||
#define UI_ADDLINKDIALOG_H
|
||||
|
||||
#include <QtCore/QVariant>
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QButtonGroup>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QFrame>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QSpacerItem>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
class Ui_AddLinkDialog
|
||||
{
|
||||
public:
|
||||
QVBoxLayout *verticalLayout;
|
||||
QFormLayout *formLayout;
|
||||
QLabel *label;
|
||||
QLineEdit *titleInput;
|
||||
QLabel *label_2;
|
||||
QLineEdit *urlInput;
|
||||
QSpacerItem *verticalSpacer;
|
||||
QFrame *line;
|
||||
QDialogButtonBox *buttonBox;
|
||||
|
||||
void setupUi(QDialog *AddLinkDialog)
|
||||
{
|
||||
if (AddLinkDialog->objectName().isEmpty())
|
||||
AddLinkDialog->setObjectName(QString::fromUtf8("AddLinkDialog"));
|
||||
AddLinkDialog->setSizeGripEnabled(false);
|
||||
AddLinkDialog->setModal(true);
|
||||
verticalLayout = new QVBoxLayout(AddLinkDialog);
|
||||
verticalLayout->setObjectName(QString::fromUtf8("verticalLayout"));
|
||||
formLayout = new QFormLayout();
|
||||
formLayout->setObjectName(QString::fromUtf8("formLayout"));
|
||||
label = new QLabel(AddLinkDialog);
|
||||
label->setObjectName(QString::fromUtf8("label"));
|
||||
|
||||
formLayout->setWidget(0, QFormLayout::LabelRole, label);
|
||||
|
||||
titleInput = new QLineEdit(AddLinkDialog);
|
||||
titleInput->setObjectName(QString::fromUtf8("titleInput"));
|
||||
titleInput->setMinimumSize(QSize(337, 0));
|
||||
|
||||
formLayout->setWidget(0, QFormLayout::FieldRole, titleInput);
|
||||
|
||||
label_2 = new QLabel(AddLinkDialog);
|
||||
label_2->setObjectName(QString::fromUtf8("label_2"));
|
||||
|
||||
formLayout->setWidget(1, QFormLayout::LabelRole, label_2);
|
||||
|
||||
urlInput = new QLineEdit(AddLinkDialog);
|
||||
urlInput->setObjectName(QString::fromUtf8("urlInput"));
|
||||
|
||||
formLayout->setWidget(1, QFormLayout::FieldRole, urlInput);
|
||||
|
||||
|
||||
verticalLayout->addLayout(formLayout);
|
||||
|
||||
verticalSpacer = new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding);
|
||||
|
||||
verticalLayout->addItem(verticalSpacer);
|
||||
|
||||
line = new QFrame(AddLinkDialog);
|
||||
line->setObjectName(QString::fromUtf8("line"));
|
||||
line->setFrameShape(QFrame::HLine);
|
||||
line->setFrameShadow(QFrame::Sunken);
|
||||
|
||||
verticalLayout->addWidget(line);
|
||||
|
||||
buttonBox = new QDialogButtonBox(AddLinkDialog);
|
||||
buttonBox->setObjectName(QString::fromUtf8("buttonBox"));
|
||||
buttonBox->setOrientation(Qt::Horizontal);
|
||||
buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok);
|
||||
|
||||
verticalLayout->addWidget(buttonBox);
|
||||
|
||||
|
||||
retranslateUi(AddLinkDialog);
|
||||
QObject::connect(buttonBox, SIGNAL(accepted()), AddLinkDialog, SLOT(accept()));
|
||||
QObject::connect(buttonBox, SIGNAL(rejected()), AddLinkDialog, SLOT(reject()));
|
||||
|
||||
QMetaObject::connectSlotsByName(AddLinkDialog);
|
||||
} // setupUi
|
||||
|
||||
void retranslateUi(QDialog *AddLinkDialog)
|
||||
{
|
||||
AddLinkDialog->setWindowTitle(QApplication::translate("AddLinkDialog", "Insert Link", nullptr));
|
||||
label->setText(QApplication::translate("AddLinkDialog", "Title:", nullptr));
|
||||
label_2->setText(QApplication::translate("AddLinkDialog", "URL:", nullptr));
|
||||
} // retranslateUi
|
||||
|
||||
};
|
||||
|
||||
namespace Ui {
|
||||
class AddLinkDialog: public Ui_AddLinkDialog {};
|
||||
} // namespace Ui
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#endif // UI_ADDLINKDIALOG_H
|
||||
@@ -214,7 +214,11 @@ void NewDiagramPage::applyConf()
|
||||
rpw->toSettings(settings, "diagrameditor/defaultreport");
|
||||
|
||||
// default xref properties
|
||||
QHash <QString, XRefProperties> hash_xrp = xrefpw -> properties();
|
||||
const QHash<QString, XRefProperties> hash_xrp = xrefpw->properties();
|
||||
for (auto it = hash_xrp.constBegin() ; it != hash_xrp.constEnd() ; ++it) {
|
||||
it.value().toSettings(settings,
|
||||
QStringLiteral("diagrameditor/defaultxref") % it.key());
|
||||
}
|
||||
|
||||
// Global in QSettings speichern
|
||||
QList<Diagram::Guide> current_guides = m_gpw->guides();
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "customelementinfopartwidget.h"
|
||||
|
||||
#include "../diagramcontext.h"
|
||||
#include "../qeticons.h"
|
||||
|
||||
#include <QGridLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QToolButton>
|
||||
|
||||
/**
|
||||
@brief CustomElementInfoPartWidget::CustomElementInfoPartWidget
|
||||
Constructor
|
||||
@param key initial key name (empty for a freshly added row)
|
||||
@param value initial value
|
||||
@param parent parent widget
|
||||
*/
|
||||
CustomElementInfoPartWidget::CustomElementInfoPartWidget(
|
||||
const QString &key,
|
||||
const QString &value,
|
||||
QWidget *parent) :
|
||||
QWidget(parent),
|
||||
m_key_edit(new QLineEdit(key, this)),
|
||||
m_value_edit(new QLineEdit(value, this)),
|
||||
m_remove_button(new QToolButton(this))
|
||||
{
|
||||
m_key_edit->setPlaceholderText(tr("nom_de_la_propriete"));
|
||||
m_key_edit->setToolTip(tr("Lettres minuscules, chiffres, tiret et underscore uniquement"));
|
||||
m_value_edit->setClearButtonEnabled(true);
|
||||
|
||||
m_remove_button->setIcon(QET::Icons::Remove);
|
||||
m_remove_button->setToolTip(tr("Supprimer cette propriété"));
|
||||
m_remove_button->setAutoRaise(true);
|
||||
|
||||
auto *layout = new QGridLayout(this);
|
||||
layout->setContentsMargins(0, 2, 0, 2);
|
||||
layout->setVerticalSpacing(2);
|
||||
layout->setHorizontalSpacing(0);
|
||||
layout->addWidget(m_key_edit, 0, 0);
|
||||
layout->addWidget(m_value_edit, 1, 0);
|
||||
layout->addWidget(m_remove_button, 0, 1, 2, 1);
|
||||
|
||||
connect(m_key_edit, &QLineEdit::textChanged, this, &CustomElementInfoPartWidget::validateKey);
|
||||
connect(m_key_edit, &QLineEdit::textChanged, this, &CustomElementInfoPartWidget::changed);
|
||||
connect(m_value_edit, &QLineEdit::textChanged, this, &CustomElementInfoPartWidget::changed);
|
||||
connect(m_remove_button, &QToolButton::clicked, this, [this]() {
|
||||
emit removeRequested(this);
|
||||
});
|
||||
|
||||
setFocusProxy(m_key_edit);
|
||||
validateKey();
|
||||
}
|
||||
|
||||
CustomElementInfoPartWidget::~CustomElementInfoPartWidget()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
@return the key name currently typed in this row
|
||||
*/
|
||||
QString CustomElementInfoPartWidget::key() const
|
||||
{
|
||||
return m_key_edit->text().trimmed();
|
||||
}
|
||||
|
||||
/**
|
||||
@return the value currently typed in this row
|
||||
*/
|
||||
QString CustomElementInfoPartWidget::value() const
|
||||
{
|
||||
return m_value_edit->text();
|
||||
}
|
||||
|
||||
/**
|
||||
@return true if the typed key is non-empty and matches
|
||||
DiagramContext::isKeyAcceptable()
|
||||
*/
|
||||
bool CustomElementInfoPartWidget::hasValidKey() const
|
||||
{
|
||||
const QString k = key();
|
||||
return !k.isEmpty() && DiagramContext::isKeyAcceptable(k);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief CustomElementInfoPartWidget::validateKey
|
||||
Flag the key field when it doesn't match the accepted format,
|
||||
instead of silently dropping it later.
|
||||
*/
|
||||
void CustomElementInfoPartWidget::validateKey()
|
||||
{
|
||||
const QString k = key();
|
||||
if (k.isEmpty() || DiagramContext::isKeyAcceptable(k)) {
|
||||
m_key_edit->setStyleSheet(QString());
|
||||
} else {
|
||||
m_key_edit->setStyleSheet(QStringLiteral("border: 1px solid red;"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CUSTOMELEMENTINFOPARTWIDGET_H
|
||||
#define CUSTOMELEMENTINFOPARTWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class QLineEdit;
|
||||
class QToolButton;
|
||||
|
||||
/**
|
||||
@brief The CustomElementInfoPartWidget class
|
||||
A single row letting the user define their own element information
|
||||
key/value pair, unlike ElementInfoPartWidget which is bound to one
|
||||
predefined key. The key is validated against
|
||||
DiagramContext::isKeyAcceptable() as the user types.
|
||||
*/
|
||||
class CustomElementInfoPartWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CustomElementInfoPartWidget(
|
||||
const QString &key = QString(),
|
||||
const QString &value = QString(),
|
||||
QWidget *parent = nullptr);
|
||||
~CustomElementInfoPartWidget() override;
|
||||
|
||||
QString key() const;
|
||||
QString value() const;
|
||||
bool hasValidKey() const;
|
||||
|
||||
signals:
|
||||
void changed();
|
||||
void removeRequested(CustomElementInfoPartWidget *self);
|
||||
|
||||
private slots:
|
||||
void validateKey();
|
||||
|
||||
private:
|
||||
QLineEdit *m_key_edit;
|
||||
QLineEdit *m_value_edit;
|
||||
QToolButton *m_remove_button;
|
||||
};
|
||||
|
||||
#endif // CUSTOMELEMENTINFOPARTWIDGET_H
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user