mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-17 19:34:13 +02:00
Merge remote-tracking branch 'upstream/master' into pr721-merge
# Conflicts: # sources/editor/ui/elementpropertieseditorwidget.cpp
This commit is contained in:
@@ -38,6 +38,20 @@ ElementsCollectionModel::ElementsCollectionModel(QObject *parent) :
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementsCollectionModel::~ElementsCollectionModel
|
||||
Destructor. loadCollections() may still have background threads
|
||||
(via QtConcurrent::map()) running setUpData() on this model's items
|
||||
when the model is destroyed (e.g. the user cancels the dialog before
|
||||
loading finishes). Wait for them here so QStandardItemModel's
|
||||
destructor doesn't free items out from under them, which used to
|
||||
crash the whole application (bugtracker #291).
|
||||
*/
|
||||
ElementsCollectionModel::~ElementsCollectionModel()
|
||||
{
|
||||
m_future.waitForFinished();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementsCollectionModel::data
|
||||
Reimplemented from QStandardItemModel
|
||||
@@ -49,6 +63,7 @@ QVariant ElementsCollectionModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (role == Qt::DecorationRole) {
|
||||
QStandardItem *item = itemFromIndex(index);
|
||||
if (!item) return QStandardItemModel::data(index, role);
|
||||
|
||||
if (item->type() == FileElementCollectionItem::Type)
|
||||
static_cast<FileElementCollectionItem*>(item)->setUpIcon();
|
||||
@@ -316,7 +331,6 @@ void ElementsCollectionModel::loadMacrosCollection()
|
||||
void ElementsCollectionModel::addMacrosCollection(bool set_data)
|
||||
{
|
||||
QString macrosPath = QETApp::userMacrosDir();
|
||||
qDebug() << "=== MAKRO PFAD CHECK ===" << macrosPath;
|
||||
if (macrosPath.endsWith("/")) {
|
||||
macrosPath.remove(macrosPath.length() - 1, 1);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ class ElementsCollectionModel : public QStandardItemModel
|
||||
|
||||
public:
|
||||
ElementsCollectionModel(QObject *parent = Q_NULLPTR);
|
||||
~ElementsCollectionModel() override;
|
||||
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
QMimeData *mimeData(const QModelIndexList &indexes) const override;
|
||||
|
||||
@@ -949,14 +949,7 @@ void ElementsCollectionWidget::search()
|
||||
}
|
||||
|
||||
hideCollection(true);
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
|
||||
const QStringList text_list = text.split("+", QString::SkipEmptyParts);
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 5.14 or later")
|
||||
#endif
|
||||
const QStringList text_list = text.split("+", Qt::SkipEmptyParts);
|
||||
#endif
|
||||
QModelIndexList match_index;
|
||||
for (QString txt : text_list) {
|
||||
match_index << m_model->match(m_showed_index.isValid()
|
||||
|
||||
@@ -143,11 +143,7 @@ void ElementsTreeView::startElementDrag(const ElementsLocation &location)
|
||||
QString file_name = (last_slash != -1) ? path.mid(last_slash + 1) : path;
|
||||
|
||||
if (!dir_path.isEmpty()) {
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
|
||||
QStringList parts = dir_path.split('/', QString::SkipEmptyParts);
|
||||
#else
|
||||
QStringList parts = dir_path.split('/', Qt::SkipEmptyParts);
|
||||
#endif
|
||||
QString current_path = "";
|
||||
for (const QString &part : parts) {
|
||||
QString parent_path = current_path;
|
||||
|
||||
@@ -430,7 +430,7 @@ void FileElementCollectionItem::setUpData()
|
||||
*/
|
||||
void FileElementCollectionItem::setUpIcon()
|
||||
{
|
||||
// Must return unconditionally once an icon is set: setIcon() calls
|
||||
// Must return unconditionally once setUpIcon has run: 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
|
||||
@@ -438,13 +438,18 @@ void FileElementCollectionItem::setUpIcon()
|
||||
// guard, any repeated setIcon() here recurses until the stack
|
||||
// overflows. Confirmed by crash report on PR #633.
|
||||
//
|
||||
// We use a dedicated bool instead of icon().isNull() because
|
||||
// some items intentionally keep a null icon (e.g. .qetmak),
|
||||
// which would bypass a null-based guard and still recurse.
|
||||
//
|
||||
// This item's m_qet_directory_unreadable is already final by the time
|
||||
// this can run at all: ElementsCollectionModel only attaches itself
|
||||
// to the tree view (making data() reachable) from loadingFinished(),
|
||||
// which fires after the QtConcurrent::map over every item -- this one
|
||||
// included -- has completed. So there is no race to work around here.
|
||||
if (!icon().isNull())
|
||||
if (m_icon_initialized)
|
||||
return;
|
||||
m_icon_initialized = true;
|
||||
|
||||
if (isCollectionRoot()) {
|
||||
QString macrosPath = QETApp::userMacrosDir();
|
||||
|
||||
@@ -69,6 +69,7 @@ class FileElementCollectionItem : public ElementCollectionItem
|
||||
/// rather than acted on in localName(), because setUpData() resets
|
||||
/// the tooltip afterwards and would otherwise discard it.
|
||||
bool m_qet_directory_unreadable = false;
|
||||
bool m_icon_initialized = false;
|
||||
};
|
||||
|
||||
#endif // FILEELEMENTCOLLECTIONITEM2_H
|
||||
|
||||
@@ -46,6 +46,18 @@ class PropertiesEditorDialog : public QDialog
|
||||
PropertiesEditorDialog(T editor, QWidget *parent = nullptr) :
|
||||
QDialog (parent)
|
||||
{
|
||||
// Without this, a QWidget-modal QDialog with a parent falls
|
||||
// back to macOS's automatic sheet presentation, which some
|
||||
// Qt/Cocoa versions render stuck centered on screen and
|
||||
// non-draggable rather than as a proper attached sheet
|
||||
// (bugtracker #275). Other dialogs in this codebase already
|
||||
// opt in explicitly (see ElementDialog, DiagramPropertiesDialog)
|
||||
// -- this one was missing it.
|
||||
setWindowModality(Qt::WindowModal);
|
||||
#ifdef Q_OS_MACOS
|
||||
setWindowFlags(Qt::Sheet);
|
||||
#endif
|
||||
|
||||
//Set dialog title
|
||||
setWindowTitle(editor->title());
|
||||
// Reparent the editor,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#define TERMINALSTRIPDRAWER_H
|
||||
|
||||
#include <QPointer>
|
||||
#include <QUuid>
|
||||
|
||||
#include "properties/terminalstriplayoutpattern.h"
|
||||
|
||||
|
||||
@@ -130,12 +130,7 @@ bool PhysicalTerminal::setLevelOf(const QSharedPointer<RealTerminal> &terminal,
|
||||
const int i = m_real_terminal.indexOf(terminal);
|
||||
if (i >= 0)
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5,14,0)
|
||||
m_real_terminal.swapItemsAt(i, std::min<int>(level, static_cast<int>(m_real_terminal.size())-1));
|
||||
#else
|
||||
auto j = std::min(level, m_real_terminal.size()-1);
|
||||
std::swap(m_real_terminal.begin()[i], m_real_terminal.begin()[j]);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -64,11 +64,8 @@ bool TerminalStripData::fromXml(const QDomElement &xml_element)
|
||||
"due to wrong tag name. Expected " << this->xmlTagName() << " used " << xml_element.tagName();
|
||||
return false;
|
||||
}
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
||||
|
||||
m_uuid = QUuid::fromString(xml_element.attribute(QStringLiteral("uuid")));
|
||||
#else
|
||||
m_uuid = QUuid(xml_element.attribute(QStringLiteral("uuid")));
|
||||
#endif
|
||||
|
||||
for (auto &xml_info :
|
||||
QETXML::findInDomElement(xml_element.firstChildElement(QStringLiteral("informations")),
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
#include "../terminalstrip.h"
|
||||
#include "../../qetinformation.h"
|
||||
|
||||
#include <QUuid>
|
||||
|
||||
TerminalStripTreeDockWidget::TerminalStripTreeDockWidget(QETProject *project, QWidget *parent) :
|
||||
QDockWidget(parent),
|
||||
ui(new Ui::TerminalStripTreeDockWidget)
|
||||
@@ -35,11 +37,7 @@ TerminalStripTreeDockWidget::TerminalStripTreeDockWidget(QETProject *project, QW
|
||||
ui->setupUi(this);
|
||||
setProject(project);
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
|
||||
ui->m_tree_view->expandRecursively(ui->m_tree_view->rootIndex());
|
||||
#else
|
||||
ui->m_tree_view->expandAll();
|
||||
#endif
|
||||
}
|
||||
|
||||
TerminalStripTreeDockWidget::~TerminalStripTreeDockWidget()
|
||||
@@ -93,11 +91,7 @@ void TerminalStripTreeDockWidget::reload()
|
||||
|
||||
buildTree();
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
|
||||
ui->m_tree_view->expandRecursively(ui->m_tree_view->rootIndex());
|
||||
#else
|
||||
ui->m_tree_view->expandAll();
|
||||
#endif
|
||||
|
||||
//Reselect the tree widget item of the current edited strip
|
||||
auto item = m_item_strip_H.key(current_);
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#include <QDockWidget>
|
||||
#include <QPointer>
|
||||
#include <QHash>
|
||||
|
||||
class QETProject;
|
||||
class QTreeWidgetItem;
|
||||
|
||||
@@ -141,7 +141,6 @@
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
@@ -163,7 +162,6 @@
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
@@ -238,7 +236,6 @@
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
@@ -328,7 +325,6 @@
|
||||
<widget class="QWidget" name="folioWidget" native="true">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
<kerning>true</kerning>
|
||||
</font>
|
||||
@@ -338,7 +334,6 @@
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
|
||||
@@ -73,6 +73,8 @@ class BorderTitleBlock : public QObject
|
||||
return(rows_count_ * rows_height_); }
|
||||
/// @return la rows header width, in pixels
|
||||
qreal rowsHeaderWidth() const { return(rows_header_width_); }
|
||||
/// @return the edge where title block is docked
|
||||
Qt::Edge titleBlockEdge() const { return(m_edge); }
|
||||
|
||||
// border - title block = diagram
|
||||
/**
|
||||
|
||||
@@ -72,14 +72,8 @@ bool ConductorNumExport::toCsv()
|
||||
if (file.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
{
|
||||
QTextStream stream(&file);
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // ### Qt 6: remove
|
||||
stream << wiresNum() << endl;
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 5.15 or later")
|
||||
#endif
|
||||
|
||||
stream << wiresNum() << &Qt::endl(stream);
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
|
||||
@@ -811,14 +811,7 @@ void ConductorProperties::readStyle(const QString &style_string) {
|
||||
if (style_string.isEmpty()) return;
|
||||
|
||||
// recupere la liste des couples style / valeur
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
|
||||
QStringList styles = style_string.split(";", QString::SkipEmptyParts);
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code QString::SkipEmptyParts for QT 5.14 or later")
|
||||
#endif
|
||||
QStringList styles = style_string.split(";", Qt::SkipEmptyParts);
|
||||
#endif
|
||||
|
||||
QRegularExpression Rx("^(?<name>[a-z-]+): (?<value>[a-z-]+)$");
|
||||
if (!Rx.isValid())
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
#include "../diagramposition.h"
|
||||
#include "../elementprovider.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../qetgraphicsitem/conductor.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "../qetgraphicsitem/terminal.h"
|
||||
#include "../qetinformation.h"
|
||||
#include "../qetproject.h"
|
||||
|
||||
@@ -87,6 +89,7 @@ void projectDataBase::updateDB()
|
||||
populateDiagramInfoTable();
|
||||
populateElementTable();
|
||||
populateElementInfoTable();
|
||||
populateConductorTable();
|
||||
emit dataBaseUpdated();
|
||||
}
|
||||
|
||||
@@ -245,6 +248,132 @@ void projectDataBase::diagramOrderChanged()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::addConductor
|
||||
@param conductor
|
||||
*/
|
||||
void projectDataBase::addConductor(Conductor *conductor)
|
||||
{
|
||||
if (!conductor || !conductor->diagram()) {
|
||||
qDebug() << "projectDataBase::addConductor: null conductor or diagram";
|
||||
return;
|
||||
}
|
||||
|
||||
//Both endpoints must belong to an element: the terminal table is keyed
|
||||
//on (terminal, element) and a terminal with no parent has no identity
|
||||
//to key on. Terminals whose *definition* predates terminal uuids are
|
||||
//fine -- Terminal::stableUuid() derives one from the terminal's local
|
||||
//position, which is what the project format itself matches on.
|
||||
if (!conductor->terminal1->parentElement()
|
||||
|| !conductor->terminal2->parentElement()) {
|
||||
return;
|
||||
}
|
||||
|
||||
insertTerminal(conductor->terminal1);
|
||||
insertTerminal(conductor->terminal2);
|
||||
|
||||
watchConductor(conductor);
|
||||
bindConductorValues(m_insert_conductor_query, conductor, conductor->diagram());
|
||||
if (!m_insert_conductor_query.exec()) {
|
||||
qDebug() << "projectDataBase::addConductor insert error : " << m_insert_conductor_query.lastError();
|
||||
} else {
|
||||
emit dataBaseUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::removeConductor
|
||||
@param conductor
|
||||
*/
|
||||
void projectDataBase::removeConductor(Conductor *conductor)
|
||||
{
|
||||
m_remove_conductor_query.bindValue(":uuid", conductor->uuid().toString());
|
||||
if (!m_remove_conductor_query.exec()) {
|
||||
qDebug() << "projectDataBase::removeConductor delete error : " << m_remove_conductor_query.lastError();
|
||||
} else {
|
||||
emit dataBaseUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::updateConductor
|
||||
Refresh the mutable columns of an already-inserted conductor.
|
||||
|
||||
Only the text (the wire number) can change without the conductor being
|
||||
removed and re-added: its endpoints are fixed for its lifetime. Without
|
||||
this, renaming a wire left the database holding the old number and the
|
||||
wiring list showed a stale value until the next full repopulate.
|
||||
@param conductor
|
||||
*/
|
||||
void projectDataBase::updateConductor(Conductor *conductor)
|
||||
{
|
||||
if (!conductor) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_update_conductor_query.bindValue(QStringLiteral(":uuid"), conductor->uuid().toString());
|
||||
m_update_conductor_query.bindValue(QStringLiteral(":text"), conductor->properties().text);
|
||||
if (!m_update_conductor_query.exec()) {
|
||||
qDebug() << "projectDataBase::updateConductor update error : " << m_update_conductor_query.lastError();
|
||||
}
|
||||
|
||||
//Deliberately no dataBaseUpdated() here, unlike add/remove. The only
|
||||
//column this touches is the wire text, which no view watched by
|
||||
//ProjectDBModel displays -- the nomenclature shows elements, and its
|
||||
//wire_count changes when a conductor appears or disappears, not when
|
||||
//it is renamed. Emitting would make every ProjectDBModel re-run its
|
||||
//query, and auto-numbering renames every conductor in the project in
|
||||
//one pass.
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::watchConductor
|
||||
Keep this conductor's row in step with its properties.
|
||||
|
||||
Conductor::setProperties() has a dozen call sites (auto-numbering, the
|
||||
properties dialog, element moves, deletion re-links...), so listening to
|
||||
the signal it already emits is the only way to catch them all -- and the
|
||||
only way to catch the ones added later. Qt::UniqueConnection makes a
|
||||
repeated insert or a full repopulate harmless.
|
||||
@param conductor
|
||||
*/
|
||||
void projectDataBase::watchConductor(Conductor *conductor)
|
||||
{
|
||||
connect(conductor, &Conductor::propertiesChange,
|
||||
this, &projectDataBase::conductorPropertiesChanged,
|
||||
Qt::UniqueConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::conductorPropertiesChanged
|
||||
*/
|
||||
void projectDataBase::conductorPropertiesChanged()
|
||||
{
|
||||
if (auto *conductor = qobject_cast<Conductor *>(sender())) {
|
||||
updateConductor(conductor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::bindConductorValues
|
||||
One binder for both insert paths, so a conductor added to a live diagram
|
||||
and one read from a file can never drift apart -- the same reason
|
||||
bindElementValues() exists for elements.
|
||||
@param query
|
||||
@param conductor
|
||||
@param diagram : the diagram the conductor belongs to
|
||||
*/
|
||||
void projectDataBase::bindConductorValues(QSqlQuery &query, Conductor *conductor, Diagram *diagram)
|
||||
{
|
||||
query.bindValue(QStringLiteral(":uuid"), conductor->uuid().toString());
|
||||
query.bindValue(QStringLiteral(":diagram_uuid"), diagram->uuid().toString());
|
||||
query.bindValue(QStringLiteral(":terminal1_uuid"), conductor->terminal1->stableUuid().toString());
|
||||
query.bindValue(QStringLiteral(":terminal1_element_uuid"), conductor->terminal1->parentElement()->uuid().toString());
|
||||
query.bindValue(QStringLiteral(":terminal2_uuid"), conductor->terminal2->stableUuid().toString());
|
||||
query.bindValue(QStringLiteral(":terminal2_element_uuid"), conductor->terminal2->parentElement()->uuid().toString());
|
||||
query.bindValue(QStringLiteral(":text"), conductor->properties().text);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::createDataBase
|
||||
Create the data base
|
||||
@@ -323,6 +452,57 @@ bool projectDataBase::createDataBase()
|
||||
qDebug() << " element_info_table query : " << query_.lastError();
|
||||
}
|
||||
|
||||
//Create the terminal table.
|
||||
//Terminal::uuid() is the terminal-position id baked into the catalog
|
||||
//.elmt definition (e.g. "the top terminal") -- identical across every
|
||||
//placed instance of that catalog element, not a per-instance id. A
|
||||
//terminal instance is only uniquely identified by (uuid, element_uuid)
|
||||
//together, so that pair is the primary key here, not uuid alone.
|
||||
QString terminal_table("CREATE TABLE terminal"
|
||||
"( "
|
||||
"uuid VARCHAR(50) NOT NULL, "
|
||||
"element_uuid VARCHAR(50) NOT NULL,"
|
||||
"name VARCHAR(50),"
|
||||
"PRIMARY KEY (uuid, element_uuid),"
|
||||
"FOREIGN KEY (element_uuid) REFERENCES element (uuid)"
|
||||
")");
|
||||
if (!query_.exec(terminal_table)) {
|
||||
qDebug() << "terminal_table query : "<< query_.lastError();
|
||||
}
|
||||
|
||||
//Create the conductor table
|
||||
QString conductor_table("CREATE TABLE conductor"
|
||||
"( "
|
||||
"uuid VARCHAR(50) PRIMARY KEY NOT NULL, "
|
||||
"diagram_uuid VARCHAR(50) NOT NULL,"
|
||||
"terminal1_uuid VARCHAR(50) NOT NULL,"
|
||||
"terminal1_element_uuid VARCHAR(50) NOT NULL,"
|
||||
"terminal2_uuid VARCHAR(50) NOT NULL,"
|
||||
"terminal2_element_uuid VARCHAR(50) NOT NULL,"
|
||||
"text VARCHAR(100),"
|
||||
"FOREIGN KEY (diagram_uuid) REFERENCES diagram (uuid),"
|
||||
"FOREIGN KEY (terminal1_uuid, terminal1_element_uuid) REFERENCES terminal (uuid, element_uuid),"
|
||||
"FOREIGN KEY (terminal2_uuid, terminal2_element_uuid) REFERENCES terminal (uuid, element_uuid)"
|
||||
")");
|
||||
if (!query_.exec(conductor_table)) {
|
||||
qDebug() << "conductor_table query : "<< query_.lastError();
|
||||
}
|
||||
|
||||
//The element-facing columns are looked up per element row, not per
|
||||
//conductor row: element_nomenclature_view carries a correlated
|
||||
//subquery counting the wires touching each element. Without these
|
||||
//indexes each element row full-scans the conductor table, which grows
|
||||
//as elements x conductors.
|
||||
for (const QString &index_ : {
|
||||
QStringLiteral("CREATE INDEX idx_conductor_terminal1_element ON conductor (terminal1_element_uuid)"),
|
||||
QStringLiteral("CREATE INDEX idx_conductor_terminal2_element ON conductor (terminal2_element_uuid)"),
|
||||
QStringLiteral("CREATE INDEX idx_conductor_diagram ON conductor (diagram_uuid)") })
|
||||
{
|
||||
if (!query_.exec(index_)) {
|
||||
qDebug() << "conductor index query : " << query_.lastError();
|
||||
}
|
||||
}
|
||||
|
||||
createElementNomenclatureView();
|
||||
createSummaryView();
|
||||
prepareQuery();
|
||||
@@ -534,6 +714,58 @@ void projectDataBase::populateDiagramInfoTable()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::populateConductorTable
|
||||
Populate the terminal and conductor tables. Terminals only matter here
|
||||
in the context of a conductor referencing them, so their population is
|
||||
folded into this method rather than tracked independently.
|
||||
*/
|
||||
void projectDataBase::populateConductorTable()
|
||||
{
|
||||
QSqlQuery query(m_data_base);
|
||||
query.exec(QStringLiteral("DELETE FROM conductor"));
|
||||
query.exec(QStringLiteral("DELETE FROM terminal"));
|
||||
|
||||
for (auto *diagram : m_project->diagrams())
|
||||
{
|
||||
const auto conductor_list = diagram->conductors();
|
||||
for (auto *conductor : conductor_list)
|
||||
{
|
||||
//See addConductor(): only a terminal with no parent element is
|
||||
//skipped. A missing terminal uuid is handled by stableUuid().
|
||||
if (!conductor->terminal1->parentElement()
|
||||
|| !conductor->terminal2->parentElement()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
insertTerminal(conductor->terminal1);
|
||||
insertTerminal(conductor->terminal2);
|
||||
|
||||
watchConductor(conductor);
|
||||
bindConductorValues(m_insert_conductor_query, conductor, diagram);
|
||||
if (!m_insert_conductor_query.exec()) {
|
||||
qDebug() << "projectDataBase::populateConductorTable insert error : " << m_insert_conductor_query.lastError();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief projectDataBase::insertTerminal
|
||||
Insert (or, if already present -- e.g. a junction shared by several
|
||||
conductors -- silently keep) @terminal in the terminal table.
|
||||
@param terminal
|
||||
*/
|
||||
void projectDataBase::insertTerminal(Terminal *terminal)
|
||||
{
|
||||
m_insert_terminal_query.bindValue(":uuid", terminal->stableUuid().toString());
|
||||
m_insert_terminal_query.bindValue(":element_uuid", terminal->parentElement()->uuid().toString());
|
||||
m_insert_terminal_query.bindValue(":name", terminal->name());
|
||||
if (!m_insert_terminal_query.exec()) {
|
||||
qDebug() << "projectDataBase::insertTerminal insert error : " << m_insert_terminal_query.lastError();
|
||||
}
|
||||
}
|
||||
|
||||
void projectDataBase::prepareQuery()
|
||||
{
|
||||
//INSERT DIAGRAM
|
||||
@@ -606,6 +838,23 @@ void projectDataBase::prepareQuery()
|
||||
update_str.append(" WHERE element_uuid = :uuid");
|
||||
m_update_element_query = QSqlQuery(m_data_base);
|
||||
m_update_element_query.prepare(update_str);
|
||||
|
||||
//INSERT TERMINAL
|
||||
m_insert_terminal_query = QSqlQuery(m_data_base);
|
||||
m_insert_terminal_query.prepare("INSERT OR IGNORE INTO terminal (uuid, element_uuid, name) VALUES (:uuid, :element_uuid, :name)");
|
||||
|
||||
//INSERT CONDUCTOR
|
||||
m_insert_conductor_query = QSqlQuery(m_data_base);
|
||||
m_insert_conductor_query.prepare("INSERT INTO conductor (uuid, diagram_uuid, terminal1_uuid, terminal1_element_uuid, terminal2_uuid, terminal2_element_uuid, text) "
|
||||
"VALUES (:uuid, :diagram_uuid, :terminal1_uuid, :terminal1_element_uuid, :terminal2_uuid, :terminal2_element_uuid, :text)");
|
||||
|
||||
//UPDATE CONDUCTOR
|
||||
m_update_conductor_query = QSqlQuery(m_data_base);
|
||||
m_update_conductor_query.prepare(QStringLiteral("UPDATE conductor SET text = :text WHERE uuid = :uuid"));
|
||||
|
||||
//REMOVE CONDUCTOR
|
||||
m_remove_conductor_query = QSqlQuery(m_data_base);
|
||||
m_remove_conductor_query.prepare("DELETE FROM conductor WHERE uuid=:uuid");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
class Element;
|
||||
class QETProject;
|
||||
class Diagram;
|
||||
class Conductor;
|
||||
class Terminal;
|
||||
class sqlite3;
|
||||
|
||||
/**
|
||||
@@ -58,6 +60,16 @@ class projectDataBase : public QObject
|
||||
void diagramInfoChanged (Diagram *diagram);
|
||||
void diagramOrderChanged();
|
||||
|
||||
void addConductor (Conductor *conductor);
|
||||
void removeConductor (Conductor *conductor);
|
||||
void updateConductor (Conductor *conductor);
|
||||
|
||||
private slots:
|
||||
//Refresh the sender()'s row after Conductor::setProperties().
|
||||
void conductorPropertiesChanged();
|
||||
|
||||
public:
|
||||
|
||||
signals:
|
||||
void dataBaseUpdated();
|
||||
|
||||
@@ -69,6 +81,10 @@ class projectDataBase : public QObject
|
||||
void populateElementTable();
|
||||
void populateElementInfoTable();
|
||||
void populateDiagramInfoTable();
|
||||
void populateConductorTable();
|
||||
void bindConductorValues(QSqlQuery &query, Conductor *conductor, Diagram *diagram);
|
||||
void watchConductor(Conductor *conductor);
|
||||
void insertTerminal(Terminal *terminal);
|
||||
void prepareQuery();
|
||||
static QHash<QString, QString> elementInfoToString(
|
||||
Element *elmt);
|
||||
@@ -86,7 +102,11 @@ class projectDataBase : public QObject
|
||||
m_insert_diagram_info_query,
|
||||
m_update_diagram_info_query,
|
||||
m_diagram_order_changed,
|
||||
m_diagram_info_order_changed;
|
||||
m_diagram_info_order_changed,
|
||||
m_insert_terminal_query,
|
||||
m_insert_conductor_query,
|
||||
m_update_conductor_query,
|
||||
m_remove_conductor_query;
|
||||
|
||||
#ifdef QET_EXPORT_PROJECT_DB
|
||||
public:
|
||||
|
||||
@@ -48,14 +48,8 @@ ElementQueryWidget::ElementQueryWidget(QWidget *parent) :
|
||||
m_button_group.addButton(ui->m_protection_cb, 5);
|
||||
m_button_group.addButton(ui->m_thumbnail_cb, 6);
|
||||
m_button_group.addButton(ui->m_plc_cb, 7);
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // ### Qt 6: remove
|
||||
connect(&m_button_group, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), [this](int id)
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 5.15 or later")
|
||||
#endif
|
||||
connect(&m_button_group, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::idClicked), [this](int id)
|
||||
#endif
|
||||
|
||||
{
|
||||
auto check_box = static_cast<QCheckBox *>(m_button_group.button(0));
|
||||
if (id == 0)
|
||||
@@ -384,10 +378,10 @@ QString ElementQueryWidget::queryStr() const
|
||||
where.clear();
|
||||
}
|
||||
|
||||
QString exclude_condition = "(exclude_from_bom IS NULL OR exclude_from_bom != '1')";
|
||||
|
||||
filter_ += " AND " + exclude_condition;
|
||||
// -------------------------------------------------------------
|
||||
// exclude_from_bom is already filtered by element_nomenclature_view
|
||||
// (see createElementNomenclatureView() in projectdatabase.cpp); this
|
||||
// widget's query reads FROM that view, so a flagged element never
|
||||
// reaches this point in the first place.
|
||||
|
||||
if (where.isEmpty() && !filter_.isEmpty()) {
|
||||
filter_.remove(0, 4); //Remove the first " AND" of filter.
|
||||
|
||||
+2
-9
@@ -1556,14 +1556,6 @@ bool Diagram::fromXml(QDomElement &document,
|
||||
if (content_ptr) {
|
||||
content_ptr -> m_elements = added_elements;
|
||||
content_ptr -> m_conductors_to_move = added_conductors;
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
|
||||
content_ptr -> m_text_fields = added_texts.toSet();
|
||||
content_ptr -> m_images = added_images.toSet();
|
||||
content_ptr -> m_shapes = added_shapes.toSet();
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 5.14 or later")
|
||||
#endif
|
||||
content_ptr -> m_text_fields = QSet<IndependentTextItem *>(
|
||||
added_texts.begin(),
|
||||
added_texts.end());
|
||||
@@ -1574,7 +1566,6 @@ bool Diagram::fromXml(QDomElement &document,
|
||||
added_shapes.begin(),
|
||||
added_shapes.end());
|
||||
content_ptr->m_terminal_strip.swap(added_strips);
|
||||
#endif
|
||||
content_ptr->m_tables.swap(added_tables);
|
||||
}
|
||||
|
||||
@@ -1675,6 +1666,7 @@ void Diagram::addItem(QGraphicsItem *item)
|
||||
conductor->terminal1->addConductor(conductor);
|
||||
conductor->terminal2->addConductor(conductor);
|
||||
conductor->calculateTextItemPosition();
|
||||
m_project->dataBase()->addConductor(conductor);
|
||||
break;
|
||||
}
|
||||
default: {break;}
|
||||
@@ -1705,6 +1697,7 @@ void Diagram::removeItem(QGraphicsItem *item)
|
||||
Conductor *conductor = static_cast<Conductor *>(item);
|
||||
conductor->terminal1->removeConductor(conductor);
|
||||
conductor->terminal2->removeConductor(conductor);
|
||||
m_project->dataBase()->removeConductor(conductor);
|
||||
break;
|
||||
}
|
||||
default: {break;}
|
||||
|
||||
+15
-34
@@ -17,9 +17,7 @@
|
||||
*/
|
||||
#include "diagramcommands.h"
|
||||
|
||||
#include "autobreakconductor.h"
|
||||
#include "diagram.h"
|
||||
#include "qetproject.h"
|
||||
#include "qetgraphicsitem/conductortextitem.h"
|
||||
#include "qetgraphicsitem/element.h"
|
||||
#include "qetgraphicsitem/elementtextitemgroup.h"
|
||||
@@ -50,7 +48,6 @@ PasteDiagramCommand::PasteDiagramCommand( Diagram *dia, const DiagramContent &c,
|
||||
PasteDiagramCommand::~PasteDiagramCommand()
|
||||
{
|
||||
diagram -> qgiManager().release(content.items(filter));
|
||||
delete m_break_cmd;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,10 +58,6 @@ void PasteDiagramCommand::undo()
|
||||
{
|
||||
diagram -> showMe();
|
||||
|
||||
//Undo auto-break before removing items, so terminals are still on scene
|
||||
if (m_break_cmd)
|
||||
m_break_cmd->undo();
|
||||
|
||||
foreach(QGraphicsItem *item, content.items(filter))
|
||||
diagram->removeItem(item);
|
||||
}
|
||||
@@ -82,6 +75,13 @@ void PasteDiagramCommand::redo()
|
||||
{
|
||||
first_redo = false;
|
||||
|
||||
//make new uuid for every pasted conductor, because old uuid are
|
||||
//the uuid of the copied conductor
|
||||
const QList <Conductor *> all_pasted_conductors = content.conductors();
|
||||
for (Conductor *c : all_pasted_conductors) {
|
||||
c -> newUuid();
|
||||
}
|
||||
|
||||
//this is the first paste, we do some actions for the new element
|
||||
const QList <Element *> elmts_list = content.m_elements;
|
||||
for (Element *e : elmts_list)
|
||||
@@ -99,37 +99,22 @@ void PasteDiagramCommand::redo()
|
||||
dc.addValue("location", "");
|
||||
e->setElementInformations(dc);
|
||||
|
||||
//Reset the text of conductors
|
||||
//Reset the text of conductors, the same way the label/comment/
|
||||
//location above are reset to "" rather than to some other
|
||||
//value - "erase on copy" means erase, not "replace with the
|
||||
//project's default new-conductor text" (which happens to
|
||||
//default to a literal "_" character, unrelated to whether the
|
||||
//user wanted this copy's old label kept or cleared; see
|
||||
//issue #413).
|
||||
const QList <Conductor *> conductors_list = content.m_conductors_to_move;
|
||||
for (Conductor *c : conductors_list)
|
||||
{
|
||||
ConductorProperties cp = c -> properties();
|
||||
cp.text = c->diagram() ? c -> diagram() -> defaultConductorProperties.text : "_";
|
||||
cp.text = "";
|
||||
c -> setProperties(cp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Auto-break conductors on first paste. Items are already on the
|
||||
//scene at this point (added before this command was created).
|
||||
if (diagram->project()->autoBreakConductor())
|
||||
{
|
||||
m_break_cmd = new QUndoCommand();
|
||||
QList<Conductor *> conductors_handled;
|
||||
QSet<Terminal *> used_terminals;
|
||||
for (Element *e : content.m_elements) {
|
||||
autoBreakConductors(diagram, e, m_break_cmd,
|
||||
conductors_handled, used_terminals);
|
||||
}
|
||||
if (m_break_cmd->childCount() == 0) {
|
||||
delete m_break_cmd;
|
||||
m_break_cmd = nullptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_break_cmd->redo();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -137,10 +122,6 @@ void PasteDiagramCommand::redo()
|
||||
for (QGraphicsItem *item : qgis_list) {
|
||||
diagram->addItem(item);
|
||||
}
|
||||
|
||||
//Re-execute the stored break commands (items are back on scene)
|
||||
if (m_break_cmd)
|
||||
m_break_cmd->redo();
|
||||
}
|
||||
|
||||
const QList<QGraphicsItem *> qgis_list = content.items();
|
||||
|
||||
@@ -53,8 +53,6 @@ class PasteDiagramCommand : public QUndoCommand {
|
||||
int filter;
|
||||
/// prevent the first call to redo()
|
||||
bool first_redo;
|
||||
/// auto-break conductor sub-commands (created on first redo)
|
||||
QUndoCommand *m_break_cmd = nullptr;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -43,11 +43,7 @@ m_preview_item(nullptr)
|
||||
QString file_name = (last_slash != -1) ? path.mid(last_slash + 1) : path;
|
||||
|
||||
if (!dir_path.isEmpty()) {
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
|
||||
QStringList parts = dir_path.split('/', QString::SkipEmptyParts);
|
||||
#else
|
||||
QStringList parts = dir_path.split('/', Qt::SkipEmptyParts);
|
||||
#endif
|
||||
QString current_path = "";
|
||||
for (const QString &part : parts) {
|
||||
QString parent_path = current_path;
|
||||
@@ -202,11 +198,7 @@ bool DiagramEventAddMacro::loadMacro()
|
||||
QString file_name = (last_slash != -1) ? path.mid(last_slash + 1) : path;
|
||||
|
||||
if (!dir_path.isEmpty()) {
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
|
||||
QStringList parts = dir_path.split('/', QString::SkipEmptyParts);
|
||||
#else
|
||||
QStringList parts = dir_path.split('/', Qt::SkipEmptyParts);
|
||||
#endif
|
||||
QString current_path = "";
|
||||
for (const QString &part : parts) {
|
||||
QString parent_path = current_path;
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
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 "diagrameventaddpdf.h"
|
||||
|
||||
// Whole file is a no-op unless QtPdf is available (see diagrameventaddpdf.h).
|
||||
#ifdef QET_HAS_QTPDF
|
||||
|
||||
#include "../qetapp.h"
|
||||
#include "../diagram.h"
|
||||
#include "../undocommand/addgraphicsobjectcommand.h"
|
||||
#include "../qetgraphicsitem/diagramimageitem.h"
|
||||
#include "../ui/pdfpagesdialog.h"
|
||||
|
||||
#include <QPdfDocument>
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPdf::DiagramEventAddPdf
|
||||
Constructor
|
||||
@param diagram the diagram where this event operates
|
||||
*/
|
||||
DiagramEventAddPdf::DiagramEventAddPdf(Diagram *diagram) :
|
||||
DiagramEventInterface(diagram),
|
||||
m_image(nullptr),
|
||||
m_is_added(false)
|
||||
{
|
||||
openDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPdf::~DiagramEventAddPdf
|
||||
Destructor
|
||||
*/
|
||||
DiagramEventAddPdf::~DiagramEventAddPdf()
|
||||
{
|
||||
if (m_running || m_abort)
|
||||
{
|
||||
if (m_is_added) m_diagram->removeItem(m_image);
|
||||
delete m_image;
|
||||
}
|
||||
|
||||
foreach (QGraphicsView *view, m_diagram->views())
|
||||
view->setContextMenuPolicy((Qt::DefaultContextMenu));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPdf::mousePressEvent
|
||||
Action when mouse is pressed
|
||||
@param event event of mouse pressed
|
||||
*/
|
||||
void DiagramEventAddPdf::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (m_image && event->button() == Qt::LeftButton)
|
||||
{
|
||||
QPointF pos = event->scenePos();
|
||||
pos.rx() -= m_image->boundingRect().width()/2;
|
||||
pos.ry() -= m_image->boundingRect().height()/2;
|
||||
m_diagram->undoStack().push(new AddGraphicsObjectCommand(m_image, m_diagram, pos));
|
||||
|
||||
for (QGraphicsView *view : m_diagram->views()) {
|
||||
view->setContextMenuPolicy((Qt::DefaultContextMenu));
|
||||
}
|
||||
|
||||
m_running = false;
|
||||
emit finish();
|
||||
event->setAccepted(true);
|
||||
}
|
||||
else if (m_image && event->button() == Qt::RightButton)
|
||||
{
|
||||
m_image->setRotation(m_image->rotation() + 90);
|
||||
event->setAccepted(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPdf::mouseMoveEvent
|
||||
Action when mouse moves
|
||||
@param event event of mouse move
|
||||
*/
|
||||
void DiagramEventAddPdf::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
|
||||
{
|
||||
if (!m_image || event->buttons() != Qt::NoButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
QPointF pos = event->scenePos();
|
||||
|
||||
if (!m_is_added)
|
||||
{
|
||||
for (QGraphicsView *view : m_diagram->views()) {
|
||||
view->setContextMenuPolicy((Qt::NoContextMenu));
|
||||
}
|
||||
|
||||
m_diagram->addItem(m_image);
|
||||
m_is_added = true;
|
||||
}
|
||||
|
||||
m_image->setPos(pos - m_image->boundingRect().center());
|
||||
event->setAccepted(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPdf::mouseDoubleClickEvent
|
||||
Overwrite double click to prevent opening properties dialog.
|
||||
@param event event of mouse double click
|
||||
*/
|
||||
void DiagramEventAddPdf::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) {
|
||||
event->setAccepted(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPdf::wheelEvent
|
||||
Action when mouse wheel is rotated (CTRL+wheel to scale)
|
||||
@param event event of mouse wheel
|
||||
*/
|
||||
void DiagramEventAddPdf::wheelEvent(QGraphicsSceneWheelEvent *event)
|
||||
{
|
||||
if (!m_is_added || !m_image || event->modifiers() != Qt::CTRL) {
|
||||
return;
|
||||
}
|
||||
|
||||
qreal scaling = m_image->scale();
|
||||
event->delta() > 1 ? scaling += 0.01 : scaling -= 0.01;
|
||||
if (scaling > 0.01 && scaling <= 2) {
|
||||
m_image->setScale(scaling);
|
||||
}
|
||||
|
||||
event->setAccepted(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPdf::isNull
|
||||
@return true if the PDF image couldn't be loaded, false otherwise
|
||||
*/
|
||||
bool DiagramEventAddPdf::isNull() const
|
||||
{
|
||||
if (!m_image) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramEventAddPdf::openDialog
|
||||
Opens a file dialog to select a PDF file, then opens a page selection
|
||||
dialog with DPI options. The selected page is rendered to a QImage at
|
||||
the chosen DPI and converted to a DiagramImageItem.
|
||||
*/
|
||||
void DiagramEventAddPdf::openDialog()
|
||||
{
|
||||
if (m_diagram->isReadOnly()) return;
|
||||
|
||||
// Open file dialog to select a PDF file
|
||||
QString pathPDFs = QETApp::documentDir();
|
||||
QString fileName = QFileDialog::getOpenFileName(
|
||||
m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first(),
|
||||
QObject::tr("Sélectionner un fichier PDF..."),
|
||||
pathPDFs,
|
||||
QObject::tr("Fichiers PDF (*.pdf)")
|
||||
);
|
||||
|
||||
if (fileName.isEmpty()) return;
|
||||
|
||||
// Load the PDF document
|
||||
QPdfDocument document;
|
||||
document.load(fileName);
|
||||
|
||||
if (document.status() != QPdfDocument::Status::Ready)
|
||||
{
|
||||
QMessageBox::critical(
|
||||
m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first(),
|
||||
QObject::tr("Erreur"),
|
||||
QObject::tr("Impossible de charger le fichier PDF.")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
int pageCount = document.pageCount();
|
||||
if (pageCount <= 0)
|
||||
{
|
||||
QMessageBox::critical(
|
||||
m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first(),
|
||||
QObject::tr("Erreur"),
|
||||
QObject::tr("Le fichier PDF ne contient aucune page.")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Always show the dialog so the user can choose page and DPI
|
||||
PdfPagesDialog dialog(document,
|
||||
m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first());
|
||||
if (dialog.exec() != QDialog::Accepted) return;
|
||||
int pageIndex = dialog.selectedPage() - 1; // Convert to 0-based index
|
||||
int dpi = dialog.selectedDpi();
|
||||
|
||||
// Calculate pixel size from PDF points at the user-selected DPI
|
||||
// PDF point = 1/72 inch
|
||||
QSizeF pageSize = document.pagePointSize(pageIndex);
|
||||
int pixelWidth = qRound((pageSize.width() / 72.0) * dpi);
|
||||
int pixelHeight = qRound((pageSize.height() / 72.0) * dpi);
|
||||
|
||||
if (pixelWidth <= 0 || pixelHeight <= 0)
|
||||
{
|
||||
QMessageBox::critical(
|
||||
m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first(),
|
||||
QObject::tr("Erreur"),
|
||||
QObject::tr("Impossible de déterminer la taille de la page PDF.")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Render PDF page to QImage
|
||||
QImage image = document.render(pageIndex, QSize(pixelWidth, pixelHeight));
|
||||
if (image.isNull())
|
||||
{
|
||||
QMessageBox::critical(
|
||||
m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first(),
|
||||
QObject::tr("Erreur"),
|
||||
QObject::tr("Impossible de rendre la page PDF.")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill white background to handle transparent PDFs
|
||||
QImage background(image.size(), QImage::Format_ARGB32_Premultiplied);
|
||||
background.fill(Qt::white);
|
||||
QPainter painter(&background);
|
||||
painter.drawImage(0, 0, image);
|
||||
painter.end();
|
||||
|
||||
m_image = new DiagramImageItem(QPixmap::fromImage(background));
|
||||
m_running = true;
|
||||
}
|
||||
|
||||
#endif // QET_HAS_QTPDF
|
||||
@@ -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 DIAGRAMEVENTADDPDF_H
|
||||
#define DIAGRAMEVENTADDPDF_H
|
||||
|
||||
// PDF page import needs the QtPdf module (QET_HAS_QTPDF, set by CMake
|
||||
// only when QtPdf is present AND Qt >= 6.4, for QPdfDocument::pagePointSize()).
|
||||
#ifdef QET_HAS_QTPDF
|
||||
|
||||
#include "diagrameventinterface.h"
|
||||
|
||||
class Diagram;
|
||||
class DiagramImageItem;
|
||||
|
||||
/**
|
||||
@brief The DiagramEventAddPdf class
|
||||
This diagram event handles the adding of a PDF page as an image in a diagram.
|
||||
The selected PDF page is rendered to a QImage at 150 DPI and added as a
|
||||
DiagramImageItem. The placement, rotation and scaling behavior is identical
|
||||
to the existing image import.
|
||||
*/
|
||||
class DiagramEventAddPdf : public DiagramEventInterface
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DiagramEventAddPdf(Diagram *diagram);
|
||||
~DiagramEventAddPdf() override;
|
||||
|
||||
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void mouseDoubleClickEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
void wheelEvent (QGraphicsSceneWheelEvent *event) override;
|
||||
|
||||
bool isNull() const;
|
||||
|
||||
private:
|
||||
void openDialog();
|
||||
|
||||
DiagramImageItem *m_image;
|
||||
bool m_is_added;
|
||||
};
|
||||
|
||||
#endif // QET_HAS_QTPDF
|
||||
|
||||
#endif // DIAGRAMEVENTADDPDF_H
|
||||
+7
-21
@@ -334,6 +334,13 @@ void DiagramView::setSelectionMode()
|
||||
*/
|
||||
void DiagramView::zoom(const qreal zoom_factor)
|
||||
{
|
||||
// clamp the resulting scale so a repeated wheel-zoom cannot drive the view
|
||||
// transform to floating-point overflow and crash the editor (issue #798)
|
||||
const qreal target = transform().m11() * zoom_factor;
|
||||
if (target < m_min_zoom || target > m_max_zoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (zoom_factor >= 1){
|
||||
scale(zoom_factor, zoom_factor);
|
||||
}
|
||||
@@ -458,14 +465,7 @@ void DiagramView::mousePressEvent(QMouseEvent *e)
|
||||
if (m_event_interface && m_event_interface->mousePressEvent(e)) return;
|
||||
|
||||
//Start drag view when hold the middle button
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||
if (e->button() == Qt::MidButton)
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 6 or later")
|
||||
#endif
|
||||
if (e->button() == Qt::MiddleButton)
|
||||
#endif
|
||||
{
|
||||
m_drag_last_pos = e->pos();
|
||||
viewport()->setCursor(Qt::ClosedHandCursor);
|
||||
@@ -515,14 +515,7 @@ void DiagramView::mouseMoveEvent(QMouseEvent *e)
|
||||
if (m_event_interface && m_event_interface->mouseMoveEvent(e)) return;
|
||||
|
||||
// Drag the view
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||
if (e->buttons() == Qt::MidButton)
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 6 or later")
|
||||
#endif
|
||||
if (e->buttons() == Qt::MiddleButton)
|
||||
#endif
|
||||
{
|
||||
QScrollBar *h = horizontalScrollBar();
|
||||
QScrollBar *v = verticalScrollBar();
|
||||
@@ -583,14 +576,7 @@ void DiagramView::mouseReleaseEvent(QMouseEvent *e)
|
||||
if (m_event_interface && m_event_interface->mouseReleaseEvent(e)) return;
|
||||
|
||||
// Stop drag view
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||
if (e->button() == Qt::MidButton)
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 6 or later")
|
||||
#endif
|
||||
if (e->button() == Qt::MiddleButton)
|
||||
#endif
|
||||
{
|
||||
viewport()->setCursor(Qt::ArrowCursor);
|
||||
}
|
||||
|
||||
@@ -103,6 +103,12 @@ class DiagramView : public QGraphicsView
|
||||
bool mustIntegrateTitleBlockTemplate(const TitleBlockTemplateLocation &) const;
|
||||
bool gestures() const;
|
||||
|
||||
/// Lowest and highest allowed value of the view transform scale (m11).
|
||||
/// Prevents wheel-zoom from driving the transform to overflow, which
|
||||
/// crashes the editor (see GitHub issue #798, same class of bug).
|
||||
static constexpr qreal m_min_zoom = 0.01;
|
||||
static constexpr qreal m_max_zoom = 200.0;
|
||||
|
||||
signals:
|
||||
/// Signal emitted after the selection mode changed
|
||||
void modeChanged();
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
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 "dxfpaintdevice.h"
|
||||
#include "createdxf.h"
|
||||
|
||||
#include <QPainterPath>
|
||||
#include <QTextItem>
|
||||
#include <QtMath>
|
||||
#include <QDebug>
|
||||
|
||||
/**
|
||||
@brief DxfPaintEngine::DxfPaintEngine
|
||||
@param filepath path of the already-open DXF file entities are
|
||||
appended to (see Createdxf::dxfBegin()/dxfEnd()).
|
||||
*/
|
||||
DxfPaintEngine::DxfPaintEngine(const QString &filepath) :
|
||||
QPaintEngine(QPaintEngine::AllFeatures),
|
||||
m_filepath(filepath)
|
||||
{
|
||||
}
|
||||
|
||||
bool DxfPaintEngine::begin(QPaintDevice *pdev)
|
||||
{
|
||||
Q_UNUSED(pdev)
|
||||
setActive(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DxfPaintEngine::end()
|
||||
{
|
||||
setActive(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DxfPaintEngine::updateState
|
||||
Track the properties this engine cares about. Everything drawn is
|
||||
mapped through the transform in effect at draw time - this is what
|
||||
lets an item's paint() be replayed unmodified at whatever scene
|
||||
position/rotation it actually has, exactly as QPainter does for
|
||||
on-screen painting.
|
||||
*/
|
||||
void DxfPaintEngine::updateState(const QPaintEngineState &state)
|
||||
{
|
||||
QPaintEngine::DirtyFlags flags = state.state();
|
||||
|
||||
if (flags & QPaintEngine::DirtyTransform)
|
||||
m_world_transform = state.transform();
|
||||
if (flags & QPaintEngine::DirtyPen)
|
||||
m_pen = state.pen();
|
||||
if (flags & QPaintEngine::DirtyBrush)
|
||||
m_brush = state.brush();
|
||||
if (flags & QPaintEngine::DirtyFont)
|
||||
m_font = state.font();
|
||||
}
|
||||
|
||||
QPointF DxfPaintEngine::toDxf(const QPointF &local_point) const
|
||||
{
|
||||
// Createdxf's QRectF/QLineF/QPolygonF/QPointF convenience overloads
|
||||
// each apply xScale/yScale and the sheetHeight Y-flip internally, so
|
||||
// this engine's whole job is composing the world transform - not
|
||||
// touching DXF units at all.
|
||||
return m_world_transform.map(local_point);
|
||||
}
|
||||
|
||||
void DxfPaintEngine::strokePolygon(const QPolygonF &poly, bool close)
|
||||
{
|
||||
if (poly.size() < 2)
|
||||
return;
|
||||
if (m_pen.style() == Qt::NoPen)
|
||||
return;
|
||||
|
||||
QPolygonF mapped;
|
||||
mapped.reserve(poly.size());
|
||||
for (const QPointF &p : poly)
|
||||
mapped << toDxf(p);
|
||||
|
||||
if (close && mapped.first() != mapped.last())
|
||||
mapped << mapped.first();
|
||||
|
||||
Createdxf::drawPolyline(m_filepath, mapped, Createdxf::dxfColor(m_pen));
|
||||
}
|
||||
|
||||
void DxfPaintEngine::drawLines(const QLineF *lines, int lineCount)
|
||||
{
|
||||
if (m_pen.style() == Qt::NoPen)
|
||||
return;
|
||||
for (int i = 0; i < lineCount; ++i)
|
||||
{
|
||||
QLineF mapped(toDxf(lines[i].p1()), toDxf(lines[i].p2()));
|
||||
Createdxf::drawLine(m_filepath, mapped, Createdxf::dxfColor(m_pen));
|
||||
}
|
||||
}
|
||||
|
||||
void DxfPaintEngine::drawRects(const QRectF *rects, int rectCount)
|
||||
{
|
||||
for (int i = 0; i < rectCount; ++i)
|
||||
{
|
||||
QPolygonF corners;
|
||||
corners << rects[i].topLeft() << rects[i].topRight()
|
||||
<< rects[i].bottomRight() << rects[i].bottomLeft();
|
||||
|
||||
// No HATCH support in v1 (see the design note in the header) - a
|
||||
// filled rect is emitted as its outline only, same as an unfilled
|
||||
// one. Still emitted (not skipped) when there's no pen but there
|
||||
// is a brush, so a filled-only shape isn't dropped entirely.
|
||||
if (m_pen.style() != Qt::NoPen || m_brush.style() != Qt::NoBrush)
|
||||
strokePolygon(corners, true);
|
||||
}
|
||||
}
|
||||
|
||||
void DxfPaintEngine::drawEllipse(const QRectF &rect)
|
||||
{
|
||||
if (m_pen.style() == Qt::NoPen && m_brush.style() == Qt::NoBrush)
|
||||
return;
|
||||
|
||||
// A non axis-aligned ellipse (rotation baked into m_world_transform)
|
||||
// can't be expressed as Createdxf::drawEllipse(), which only accepts
|
||||
// an axis-aligned QRectF. Approximate with a flattened polygon in
|
||||
// that case; use the exact CIRCLE/ELLIPSE entity when the transform
|
||||
// is axis-aligned, matching what the existing hand-written exporters
|
||||
// already produce for unrotated shapes.
|
||||
const bool axis_aligned = qFuzzyIsNull(m_world_transform.m12())
|
||||
&& qFuzzyIsNull(m_world_transform.m21());
|
||||
if (axis_aligned)
|
||||
{
|
||||
QRectF mapped = m_world_transform.mapRect(rect);
|
||||
Createdxf::drawEllipse(m_filepath, mapped, Createdxf::dxfColor(m_pen));
|
||||
return;
|
||||
}
|
||||
|
||||
QPainterPath path;
|
||||
path.addEllipse(rect);
|
||||
const auto polygons = path.toSubpathPolygons();
|
||||
for (const QPolygonF &poly : polygons)
|
||||
strokePolygon(poly, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DxfPaintEngine::drawPath
|
||||
Handles drawArc()/drawPie() (QPainterPath::Arc elements - emitted as
|
||||
chord segments, since Createdxf's ARC-equivalent
|
||||
(drawArcEllipse()) only takes an axis-aligned ellipse + angle pair,
|
||||
which doesn't compose with an arbitrary path transform) and
|
||||
fillPath()/generic paths (flattened to polylines).
|
||||
*/
|
||||
void DxfPaintEngine::drawPath(const QPainterPath &path)
|
||||
{
|
||||
if (m_pen.style() == Qt::NoPen && m_brush.style() == Qt::NoBrush)
|
||||
return;
|
||||
|
||||
const auto polygons = path.toSubpathPolygons();
|
||||
for (const QPolygonF &poly : polygons)
|
||||
strokePolygon(poly, path.isEmpty() ? false : true);
|
||||
}
|
||||
|
||||
void DxfPaintEngine::drawPolygon(const QPointF *points, int pointCount, PolygonDrawMode mode)
|
||||
{
|
||||
if (pointCount < 2)
|
||||
return;
|
||||
QPolygonF poly;
|
||||
poly.reserve(pointCount);
|
||||
for (int i = 0; i < pointCount; ++i)
|
||||
poly << points[i];
|
||||
|
||||
strokePolygon(poly, mode != PolylineMode);
|
||||
}
|
||||
|
||||
void DxfPaintEngine::drawTextItem(const QPointF &p, const QTextItem &textItem)
|
||||
{
|
||||
qreal font_size = textItem.font().pointSizeF();
|
||||
if (font_size < 0)
|
||||
font_size = textItem.font().pixelSize();
|
||||
|
||||
// Rotation: extract the angle baked into the world transform by
|
||||
// mapping a unit vector, the same approach used to compute rotation
|
||||
// for the existing element-text DXF export in exportdialog.cpp.
|
||||
QPointF origin = m_world_transform.map(p);
|
||||
QPointF right = m_world_transform.map(p + QPointF(1, 0));
|
||||
qreal angle = qRadiansToDegrees(qAtan2(-(right.y() - origin.y()), right.x() - origin.x()));
|
||||
|
||||
Createdxf::drawText(m_filepath, textItem.text(), toDxf(p), font_size,
|
||||
angle, Createdxf::dxfColor(m_pen), 0.72);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DxfPaintEngine::drawPixmap
|
||||
Not implemented in v1 - out of scope per the design note in the
|
||||
header (CrossRefItem, the only item exported through this engine so
|
||||
far, never draws a pixmap). qWarning() rather than a hard failure, so
|
||||
an item that does call this in the future degrades to "one entity
|
||||
missing" instead of crashing the whole export.
|
||||
*/
|
||||
void DxfPaintEngine::drawPixmap(const QRectF &r, const QPixmap &pm, const QRectF &sr)
|
||||
{
|
||||
Q_UNUSED(r)
|
||||
Q_UNUSED(pm)
|
||||
Q_UNUSED(sr)
|
||||
qWarning() << "DxfPaintEngine::drawPixmap: not supported, entity skipped";
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DxfPaintDevice::DxfPaintDevice
|
||||
@param filepath path of the already-open DXF file (see
|
||||
Createdxf::dxfBegin()/dxfEnd() - this device only appends entities in
|
||||
between, it does not open/close the file itself).
|
||||
*/
|
||||
DxfPaintDevice::DxfPaintDevice(const QString &filepath) :
|
||||
QPaintDevice(),
|
||||
m_filepath(filepath),
|
||||
m_engine(new DxfPaintEngine(filepath))
|
||||
{
|
||||
}
|
||||
|
||||
DxfPaintDevice::~DxfPaintDevice()
|
||||
{
|
||||
delete m_engine;
|
||||
}
|
||||
|
||||
QPaintEngine *DxfPaintDevice::paintEngine() const
|
||||
{
|
||||
return m_engine;
|
||||
}
|
||||
|
||||
int DxfPaintDevice::metric(PaintDeviceMetric metric) const
|
||||
{
|
||||
// A DXF document has no pixel grid or physical size in the sense
|
||||
// QPaintDevice::metric() expects - large-but-finite values here are
|
||||
// enough to keep QPainter's own bookkeeping (e.g. clip region setup)
|
||||
// from misbehaving; nothing in this engine actually consults them.
|
||||
switch (metric)
|
||||
{
|
||||
case PdmWidth:
|
||||
case PdmHeight:
|
||||
return 1000000;
|
||||
case PdmDpiX:
|
||||
case PdmDpiY:
|
||||
case PdmPhysicalDpiX:
|
||||
case PdmPhysicalDpiY:
|
||||
return 96;
|
||||
case PdmWidthMM:
|
||||
case PdmHeightMM:
|
||||
return 1000000;
|
||||
case PdmNumColors:
|
||||
return 16777216;
|
||||
case PdmDepth:
|
||||
return 24;
|
||||
case PdmDevicePixelRatio:
|
||||
case PdmDevicePixelRatioScaled:
|
||||
return 1;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
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 DXFPAINTDEVICE_H
|
||||
#define DXFPAINTDEVICE_H
|
||||
|
||||
#include <QPaintDevice>
|
||||
#include <QPaintEngine>
|
||||
#include <QPen>
|
||||
#include <QBrush>
|
||||
#include <QFont>
|
||||
#include <QString>
|
||||
|
||||
/**
|
||||
@brief The DxfPaintEngine class
|
||||
A QPaintEngine that translates the small set of QPainter calls made by
|
||||
QGraphicsItem::paint() implementations (drawLines, drawRects,
|
||||
drawEllipse, drawPolygon/drawPolyline, drawTextItem) into DXF entities
|
||||
written through Createdxf, instead of pixels.
|
||||
|
||||
This exists so that an item's existing, already-correct paint() code
|
||||
can be reused unmodified to produce a DXF export: construct a QPainter
|
||||
on a DxfPaintDevice targeting the item, call painter.begin()/end()
|
||||
around item->paint(&painter, ...), and every primitive the item draws
|
||||
becomes a DXF entity at the item's scene position instead of a pixel
|
||||
on screen.
|
||||
|
||||
Scope (deliberately not the full QPainter surface - only what
|
||||
QElectroTech's own paint() implementations are observed to call):
|
||||
- drawLines -> LINE
|
||||
- drawRects -> LWPOLYLINE (closed 4-point outline; DXF has no
|
||||
filled-rect primitive in the AC1006 dialect this
|
||||
exporter targets, so brush fill is not emitted -
|
||||
see fillPath below)
|
||||
- drawEllipse -> Createdxf::drawArcEllipse (full sweep) or CIRCLE
|
||||
- drawPath -> arcs (from QPainterPath::Arc elements, used by
|
||||
drawArc/drawPie) become a sequence of LINE chords;
|
||||
anything else in the path is flattened to
|
||||
polyline segments via QPainterPath::toSubpathPolygons
|
||||
- drawPolygon -> LWPOLYLINE
|
||||
- drawTextItem -> TEXT (drawText() calls route through this)
|
||||
- fillPath -> same outline-only handling as drawRects; no HATCH
|
||||
support in v1 (see design note in the PR)
|
||||
|
||||
Anything outside this list (images, gradients, etc.) is intentionally
|
||||
unimplemented and asserts in debug builds rather than silently
|
||||
producing an incomplete drawing - callers should know immediately if
|
||||
an item they're exporting uses something this engine doesn't cover
|
||||
yet, rather than getting a DXF file quietly missing content.
|
||||
*/
|
||||
class DxfPaintEngine : public QPaintEngine
|
||||
{
|
||||
public:
|
||||
explicit DxfPaintEngine(const QString &filepath);
|
||||
|
||||
bool begin(QPaintDevice *pdev) override;
|
||||
bool end() override;
|
||||
void updateState(const QPaintEngineState &state) override;
|
||||
|
||||
void drawLines(const QLineF *lines, int lineCount) override;
|
||||
void drawRects(const QRectF *rects, int rectCount) override;
|
||||
void drawEllipse(const QRectF &rect) override;
|
||||
void drawPath(const QPainterPath &path) override;
|
||||
void drawPolygon(const QPointF *points, int pointCount, PolygonDrawMode mode) override;
|
||||
void drawTextItem(const QPointF &p, const QTextItem &textItem) override;
|
||||
void drawPixmap(const QRectF &r, const QPixmap &pm, const QRectF &sr) override;
|
||||
|
||||
Type type() const override { return QPaintEngine::User; }
|
||||
|
||||
private:
|
||||
QPointF toDxf(const QPointF &scene_point) const;
|
||||
void strokePolygon(const QPolygonF &poly, bool close);
|
||||
|
||||
QString m_filepath;
|
||||
QTransform m_world_transform;
|
||||
QPen m_pen;
|
||||
QBrush m_brush;
|
||||
QFont m_font;
|
||||
};
|
||||
|
||||
/**
|
||||
@brief The DxfPaintDevice class
|
||||
Pairs with DxfPaintEngine. One instance targets one already-open DXF
|
||||
file (Createdxf::dxfBegin()/dxfEnd() bracket the whole export, same as
|
||||
today - this device only ever appends entities in between).
|
||||
*/
|
||||
class DxfPaintDevice : public QPaintDevice
|
||||
{
|
||||
public:
|
||||
explicit DxfPaintDevice(const QString &filepath);
|
||||
~DxfPaintDevice() override;
|
||||
|
||||
QPaintEngine *paintEngine() const override;
|
||||
|
||||
protected:
|
||||
int metric(PaintDeviceMetric metric) const override;
|
||||
|
||||
private:
|
||||
QString m_filepath;
|
||||
DxfPaintEngine *m_engine;
|
||||
};
|
||||
|
||||
#endif // DXFPAINTDEVICE_H
|
||||
@@ -276,14 +276,7 @@ void ChangeZValueCommand::applyRaise(const QList<QGraphicsItem *> &items_list) {
|
||||
for (int i = my_items_list.count() - 2 ; i >= 0 ; -- i) {
|
||||
if (my_items_list[i] -> isSelected()) {
|
||||
if (!my_items_list[i +1] -> isSelected()) {
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0) // ### Qt 6: remove
|
||||
my_items_list.swap(i, i + 1);
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 5.13 or later")
|
||||
#endif
|
||||
my_items_list.swapItemsAt(i, i + 1);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,14 +294,7 @@ void ChangeZValueCommand::applyLower(const QList<QGraphicsItem *> &items_list) {
|
||||
for (int i = 1 ; i < my_items_list.count() ; ++ i) {
|
||||
if (my_items_list[i] -> isSelected()) {
|
||||
if (!my_items_list[i - 1] -> isSelected()) {
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0) // ### Qt 6: remove
|
||||
my_items_list.swap(i, i - 1);
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 5.13 or later")
|
||||
#endif
|
||||
my_items_list.swapItemsAt(i, i - 1);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ void ElementScene::mouseMoveEvent(QGraphicsSceneMouseEvent *e)
|
||||
{
|
||||
if (m_event_interface) {
|
||||
if (m_event_interface -> mouseMoveEvent(e)) {
|
||||
emit mouseMoved(e -> scenePos());
|
||||
emit mouseMoved(snapToGrid(e->scenePos()));
|
||||
if (m_event_interface->isFinish()) {
|
||||
delete m_event_interface;
|
||||
m_event_interface = nullptr;
|
||||
|
||||
@@ -100,13 +100,30 @@ void ElementView::setSelectionMode()
|
||||
emit(modeChanged());
|
||||
}
|
||||
|
||||
/**
|
||||
Applique un facteur d'echelle a la vue en bornant l'echelle resultante
|
||||
entre m_min_zoom et m_max_zoom. Sans cette borne, un zoom repete (molette)
|
||||
finit par faire deborder la transformation de la vue et fait planter
|
||||
l'editeur (issue #798).
|
||||
@param factor facteur d'echelle a appliquer
|
||||
*/
|
||||
void ElementView::scaleClamped(qreal factor)
|
||||
{
|
||||
const qreal current = transform().m11();
|
||||
const qreal target = current * factor;
|
||||
if (target < m_min_zoom || target > m_max_zoom) {
|
||||
return;
|
||||
}
|
||||
scale(factor, factor);
|
||||
}
|
||||
|
||||
/**
|
||||
Agrandit le schema (+33% = inverse des -25 % de zoomMoins())
|
||||
*/
|
||||
void ElementView::zoomIn()
|
||||
{
|
||||
adjustSceneRect();
|
||||
scale(4.0/3.0, 4.0/3.0);
|
||||
scaleClamped(4.0/3.0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,7 +132,7 @@ void ElementView::zoomIn()
|
||||
void ElementView::zoomOut()
|
||||
{
|
||||
adjustSceneRect();
|
||||
scale(0.75, 0.75);
|
||||
scaleClamped(0.75);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,7 +140,7 @@ void ElementView::zoomOut()
|
||||
*/
|
||||
void ElementView::zoomInSlowly()
|
||||
{
|
||||
scale(1.02, 1.02);
|
||||
scaleClamped(1.02);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,7 +148,7 @@ void ElementView::zoomInSlowly()
|
||||
*/
|
||||
void ElementView::zoomOutSlowly()
|
||||
{
|
||||
scale(0.98, 0.98);
|
||||
scaleClamped(0.98);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -372,14 +389,7 @@ ElementContent ElementView::pasteWithOffset(const QDomDocument &xml_document) {
|
||||
*/
|
||||
void ElementView::mousePressEvent(QMouseEvent* e)
|
||||
{
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||
if (e->button() == Qt::MidButton)
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 6 or later")
|
||||
#endif
|
||||
if (e->button() == Qt::MiddleButton)
|
||||
#endif
|
||||
{
|
||||
setCursor( (Qt::ClosedHandCursor));
|
||||
reference_view_ = e->pos();
|
||||
@@ -394,14 +404,7 @@ void ElementView::mousePressEvent(QMouseEvent* e)
|
||||
*/
|
||||
void ElementView::mouseMoveEvent(QMouseEvent* e)
|
||||
{
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||
if (e->buttons() == Qt::MidButton)
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 6 or later")
|
||||
#endif
|
||||
if (e->buttons() == Qt::MiddleButton)
|
||||
#endif
|
||||
{
|
||||
QScrollBar *h = horizontalScrollBar();
|
||||
QScrollBar *v = verticalScrollBar();
|
||||
@@ -420,14 +423,7 @@ void ElementView::mouseMoveEvent(QMouseEvent* e)
|
||||
*/
|
||||
void ElementView::mouseReleaseEvent(QMouseEvent* e)
|
||||
{
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||
if (e->button() == Qt::MidButton)
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 6 or later")
|
||||
#endif
|
||||
if (e->button() == Qt::MiddleButton)
|
||||
#endif
|
||||
{
|
||||
setCursor(Qt::ArrowCursor);
|
||||
adjustSceneRect();
|
||||
|
||||
@@ -53,6 +53,13 @@ class ElementView : public QGraphicsView {
|
||||
|
||||
private:
|
||||
QRectF applyMovement(const QRectF &, const QPointF &);
|
||||
void scaleClamped(qreal factor);
|
||||
|
||||
/// Lowest and highest allowed value of the view transform scale (m11).
|
||||
/// Prevents the wheel-zoom from driving the transform to overflow, which
|
||||
/// crashes the editor (bugtracker / GitHub issue #798).
|
||||
static constexpr qreal m_min_zoom = 0.1;
|
||||
static constexpr qreal m_max_zoom = 200.0;
|
||||
|
||||
public slots:
|
||||
void setVisualisationMode();
|
||||
|
||||
@@ -521,14 +521,7 @@ void CustomElementGraphicPart::stylesFromXml(const QDomElement &qde)
|
||||
resetStyles();
|
||||
|
||||
//Get the list of pair style/value
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
|
||||
QStringList styles = qde.attribute("style").split(";", QString::SkipEmptyParts);
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 5.14 or later")
|
||||
#endif
|
||||
QStringList styles = qde.attribute("style").split(";", Qt::SkipEmptyParts);
|
||||
#endif
|
||||
|
||||
//Check each pair of style
|
||||
QRegularExpression rx("^\\s*([a-z-]+)\\s*:\\s*([a-zA-Z-]+)\\s*$");
|
||||
|
||||
@@ -440,6 +440,14 @@ void PartText::setFont(const QFont &font) {
|
||||
// at a different spot after save/reopen (the position recomputes from
|
||||
// the saved font on load). See #158.
|
||||
adjustItemPosition();
|
||||
// Keep real_font_size_ in sync with the actual font. It's the base
|
||||
// size startUserTransformation()/handleUserTransformation() scale
|
||||
// from when the user drags a resize handle, and flip() also reads it
|
||||
// to reposition the item. Left stale here, either one computes from
|
||||
// whatever size the item had when it was first created, ignoring any
|
||||
// size set since (toolbar, property editor, or loaded from XML) -
|
||||
// found investigating #158.
|
||||
real_font_size_ = font.pointSize();
|
||||
emit fontChanged(font);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include <QColorDialog>
|
||||
#include <QGraphicsItem>
|
||||
#include <QPointer>
|
||||
#include <QSignalBlocker>
|
||||
|
||||
DynamicTextFieldEditor::DynamicTextFieldEditor(QETElementEditor *editor,
|
||||
PartDynamicTextField *text_field,
|
||||
@@ -141,9 +142,20 @@ void DynamicTextFieldEditor::updateForm()
|
||||
ui -> m_size_sb -> setValue(m_text_field.data() -> font().pointSize());
|
||||
ui->m_keep_visual_rotation_cb->setChecked(m_text_field.data()->keepVisualRotation());
|
||||
ui->m_rotation_point_center_cb->setChecked(m_text_field.data()->rotationPointCenter());
|
||||
#ifdef BUILD_WITHOUT_KF5
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
#else
|
||||
m_color_kpb -> setColor(m_text_field.data() -> color());
|
||||
//Block signals while loading the colour into the button.
|
||||
//KColorButton::changed fires on a programmatic setColor() as well
|
||||
//as on user interaction, and m_color_kpb_changed() applies the new
|
||||
//colour to *every* selected part -- so merely showing the first
|
||||
//part's colour would overwrite the colour of all the others.
|
||||
//The other widgets above are immune because they are wired to
|
||||
//user-only signals (editingFinished, clicked), which setValue()
|
||||
//and setChecked() do not emit.
|
||||
{
|
||||
const QSignalBlocker blocker(m_color_kpb);
|
||||
m_color_kpb -> setColor(m_text_field.data() -> color());
|
||||
}
|
||||
#endif
|
||||
ui -> m_width_sb -> setValue(m_text_field.data() -> textWidth());
|
||||
ui -> m_font_pb -> setText(m_text_field -> font().family());
|
||||
@@ -170,7 +182,7 @@ void DynamicTextFieldEditor::updateForm()
|
||||
|
||||
void DynamicTextFieldEditor::setupWidget()
|
||||
{
|
||||
#ifdef BUILD_WITHOUT_KF5
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
#else
|
||||
m_color_kpb = new KColorButton(this);
|
||||
m_color_kpb->setObjectName(QString::fromUtf8("m_color_kpb"));
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
#include "../elementitemeditor.h"
|
||||
#include "../graphicspart/partdynamictextfield.h"
|
||||
|
||||
#ifdef BUILD_WITHOUT_KF5
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
#else
|
||||
# include <KColorButton>
|
||||
#endif
|
||||
@@ -79,7 +79,7 @@ class DynamicTextFieldEditor : public ElementItemEditor {
|
||||
QList<PartDynamicTextField*> m_parts;
|
||||
QList<QMetaObject::Connection> m_connection_list;
|
||||
|
||||
#ifdef BUILD_WITHOUT_KF5
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
#else
|
||||
KColorButton* m_color_kpb = nullptr;
|
||||
#endif
|
||||
|
||||
@@ -423,13 +423,11 @@ void ElementPropertiesEditorWidget::on_m_base_type_cb_currentIndexChanged(int in
|
||||
ui->m_master_gb->setVisible(master);
|
||||
ui->m_terminal_gb->setVisible(terminal);
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
|
||||
ui->tabWidget->setTabVisible(1,
|
||||
(type_ == ElementData::Simple ||
|
||||
type_ == ElementData::Master ||
|
||||
type_ == ElementData::Slave ||
|
||||
type_ == ElementData::Terminal));
|
||||
#endif
|
||||
|
||||
updateTree();
|
||||
}
|
||||
@@ -456,6 +454,9 @@ void ElementPropertiesEditorWidget::on_m_slave_groups_checkbox_toggled(bool chec
|
||||
|
||||
if (checked && !ui->max_slaves_checkbox->isChecked()) {
|
||||
ui->max_slaves_checkbox->setChecked(true);
|
||||
if (!m_data.m_slave_contact_groups.isEmpty()) {
|
||||
ui->max_slaves_spinbox->setValue(m_data.m_slave_contact_groups.size());
|
||||
}
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
@@ -691,7 +692,7 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
|
||||
m_plc_table->horizontalHeader()->resizeSection(2, 150);
|
||||
m_plc_table->horizontalHeader()->resizeSection(3, 150);
|
||||
m_plc_table->horizontalHeader()->resizeSection(4, 100);
|
||||
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems);
|
||||
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
m_plc_table->setMinimumHeight(200);
|
||||
m_plc_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
#include <QActionGroup>
|
||||
#include <QFileDialog>
|
||||
#include <QSvgGenerator>
|
||||
#include <QHBoxLayout>
|
||||
|
||||
/**
|
||||
* @brief QETElementEditor::QETElementEditor
|
||||
@@ -77,8 +78,9 @@ QETElementEditor::QETElementEditor(QWidget *parent) :
|
||||
//ui->m_display_menu->insertMenu(ui->m_zoom_in_action, menu);
|
||||
|
||||
setWindowState(Qt::WindowMaximized);
|
||||
readSettings();
|
||||
readSettings(); // restoreGeometry before show()
|
||||
show();
|
||||
readSettingsState(); // restoreState() must be called after show() in Qt6
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -950,16 +952,28 @@ void QETElementEditor::readSettings()
|
||||
restoreGeometry(geometry.toByteArray());
|
||||
}
|
||||
|
||||
QVariant state = settings.value("elementeditor/state");
|
||||
if (state.isValid()) {
|
||||
restoreState(state.toByteArray());
|
||||
}
|
||||
|
||||
auto data = m_elmt_scene->elementData();
|
||||
data.m_drawing_information = settings.value("elementeditor/default-informations", "").toString();
|
||||
m_elmt_scene->setElementData(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief QETElementEditor::readSettingsState
|
||||
* Restore the window state (docks, toolbars).
|
||||
* Must be called AFTER show() in Qt6 for restoreState() to work correctly.
|
||||
*/
|
||||
void QETElementEditor::readSettingsState()
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
QVariant state = settings.value("elementeditor/state");
|
||||
if (state.isValid()) {
|
||||
if (!restoreState(state.toByteArray())) {
|
||||
settings.remove("elementeditor/state");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief QETElementEditor::writeSettings
|
||||
* Write some setting of this widget in the
|
||||
@@ -1200,7 +1214,18 @@ void QETElementEditor::initGui()
|
||||
//Live cursor position readout, in the same scene coordinates as the parts' X/Y properties
|
||||
m_position_label = new QLabel(this);
|
||||
m_position_label->setMinimumWidth(120);
|
||||
statusBar()->addPermanentWidget(m_position_label);
|
||||
|
||||
// Layout
|
||||
QHBoxLayout *coordDisplayLayout = new QHBoxLayout();
|
||||
coordDisplayLayout->setContentsMargins(0, 0, 0, 0);
|
||||
coordDisplayLayout->addWidget(m_position_label);
|
||||
coordDisplayLayout->addStretch();
|
||||
// Widget
|
||||
QWidget *coordDisplay = new QWidget;
|
||||
coordDisplay->setLayout(coordDisplayLayout);
|
||||
|
||||
statusBar()->addPermanentWidget(coordDisplay);
|
||||
|
||||
connect(m_elmt_scene, &ElementScene::mouseMoved, this, [this](const QPointF &pos) {
|
||||
m_position_label->setText(tr("X: %1 Y: %2")
|
||||
.arg(pos.x(), 0, 'f', 1)
|
||||
|
||||
@@ -116,6 +116,7 @@ class QETElementEditor : public QMainWindow
|
||||
private:
|
||||
bool canClose();
|
||||
void readSettings();
|
||||
void readSettingsState();
|
||||
void writeSettings() const;
|
||||
void setupActions();
|
||||
void updateAction();
|
||||
|
||||
@@ -40,7 +40,7 @@ TerminalEditor::TerminalEditor(QETElementEditor *editor, QWidget *parent) :
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
#ifdef BUILD_WITHOUT_KF5
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
m_color_pb = new QPushButton(this);
|
||||
m_color_pb->setMinimumSize(40, 24);
|
||||
connect(m_color_pb, &QPushButton::clicked, this, &TerminalEditor::labelColorClicked);
|
||||
@@ -93,7 +93,7 @@ void TerminalEditor::updateForm()
|
||||
ui->m_label_rotation_sb->setValue(static_cast<int>(m_part->labelRotation()));
|
||||
ui->m_label_frame_cb->setChecked(m_part->labelFrame());
|
||||
|
||||
#ifdef BUILD_WITHOUT_KF5
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
QPixmap px(16, 16);
|
||||
px.fill(m_part->labelColor());
|
||||
m_color_pb->setIcon(QIcon(px));
|
||||
@@ -410,7 +410,7 @@ void TerminalEditor::labelColorClicked()
|
||||
if (m_locked) return;
|
||||
m_locked = true;
|
||||
|
||||
#ifdef BUILD_WITHOUT_KF5
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
QColor new_color = QColorDialog::getColor(m_part->labelColor(), this);
|
||||
if (new_color.isValid() && m_part->labelColor() != new_color) {
|
||||
auto undo = new QPropertyUndoCommand(m_part, "label_color", m_part->labelColor(), new_color);
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
#include <QWidget>
|
||||
#include "../elementitemeditor.h"
|
||||
|
||||
#ifdef BUILD_WITHOUT_KF5
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
#include <QPushButton>
|
||||
#else
|
||||
#include <KColorButton>
|
||||
@@ -78,7 +78,7 @@ class TerminalEditor : public ElementItemEditor
|
||||
m_change_connections;
|
||||
PartTerminal *m_part = nullptr;
|
||||
bool m_locked = false;
|
||||
#ifdef BUILD_WITHOUT_KF5
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
QPushButton *m_color_pb;
|
||||
#else
|
||||
KColorButton *m_color_pb;
|
||||
|
||||
@@ -63,7 +63,7 @@ void TextEditor::updateForm()
|
||||
m_rotation_sb -> setValue(m_text -> rotation());
|
||||
m_size_sb -> setValue(m_text -> font().pointSize());
|
||||
m_font_pb -> setText(m_text -> font().family());
|
||||
#ifdef BUILD_WITHOUT_KF5
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
#else
|
||||
m_color_pb -> setColor(m_text -> defaultTextColor());
|
||||
#endif
|
||||
@@ -346,7 +346,7 @@ void TextEditor::setUpWidget(QWidget *parent)
|
||||
m_line_edit->setPlaceholderText(tr("Entrer votre texte ici"));
|
||||
|
||||
gridLayout->addWidget(m_line_edit, 0, 0, 1, 6);
|
||||
#ifdef BUILD_WITHOUT_KF5
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
#else
|
||||
m_color_pb = new KColorButton(parent);
|
||||
m_color_pb->setObjectName(QString::fromUtf8("m_color_pb"));
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include <QSpinBox>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#ifdef BUILD_WITHOUT_KF5
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
#else
|
||||
#include <KColorButton>
|
||||
#endif
|
||||
@@ -65,7 +65,7 @@ class TextEditor : public ElementItemEditor {
|
||||
QSpinBox *m_size_sb;
|
||||
QLineEdit *m_line_edit;
|
||||
QPushButton *m_font_pb;
|
||||
#ifdef BUILD_WITHOUT_KF5
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
#else
|
||||
KColorButton *m_color_pb;
|
||||
#endif
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
#include "elementspanelwidget.h"
|
||||
#include "diagram.h"
|
||||
#include "qetgraphicsitem/conductor.h"
|
||||
#include "editor/ui/qetelementeditor.h"
|
||||
#include "elementscategoryeditor.h"
|
||||
#include "qetapp.h"
|
||||
@@ -684,6 +685,13 @@ void ElementsPanelWidget::duplicateDiagram()
|
||||
elmt->newUuid();
|
||||
new_diagram->restoreText(elmt);
|
||||
}
|
||||
else if (Conductor *cond = dynamic_cast<Conductor *>(item)) {
|
||||
// Same reasoning for conductors: conductor.uuid is the PRIMARY
|
||||
// KEY of the conductor table, and its insert is a plain INSERT,
|
||||
// so a duplicated uuid fails and the wire silently disappears
|
||||
// from the wiring list and the per-element wire count.
|
||||
cond->newUuid();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,10 @@
|
||||
#include "exportpropertieswidget.h"
|
||||
#include "factory/elementpicturefactory.h"
|
||||
#include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
|
||||
#include "dxfpaintdevice.h"
|
||||
#include "qetgraphicsitem/conductor.h"
|
||||
#include "qetgraphicsitem/conductortextitem.h"
|
||||
#include "qetgraphicsitem/crossrefitem.h"
|
||||
#include "qetgraphicsitem/diagramimageitem.h"
|
||||
#include "qetgraphicsitem/diagramtextitem.h"
|
||||
#include "qetgraphicsitem/dynamicelementtextitem.h"
|
||||
@@ -140,21 +142,12 @@ QWidget *ExportDialog::initDiagramsListPart()
|
||||
reset_mapper_ = new QSignalMapper(this);
|
||||
clipboard_mapper_ = new QSignalMapper(this);
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // TODO Qt6 only: remove, mappedInt() always available
|
||||
connect(preview_mapper_, SIGNAL(mapped(int)), this, SLOT(slot_previewDiagram(int)));
|
||||
connect(width_mapper_, SIGNAL(mapped(int)), this, SLOT(slot_correctHeight(int)));
|
||||
connect(height_mapper_, SIGNAL(mapped(int)), this, SLOT(slot_correctWidth(int)));
|
||||
connect(ratio_mapper_, SIGNAL(mapped(int)), this, SLOT(slot_keepRatioChanged(int)));
|
||||
connect(reset_mapper_, SIGNAL(mapped(int)), this, SLOT(slot_resetSize(int)));
|
||||
connect(clipboard_mapper_, SIGNAL(mapped(int)), this, SLOT(slot_exportToClipBoard(int)));
|
||||
#else
|
||||
connect(preview_mapper_, &QSignalMapper::mappedInt, this, &ExportDialog::slot_previewDiagram);
|
||||
connect(width_mapper_, &QSignalMapper::mappedInt, this, &ExportDialog::slot_correctHeight);
|
||||
connect(height_mapper_, &QSignalMapper::mappedInt, this, &ExportDialog::slot_correctWidth);
|
||||
connect(ratio_mapper_, &QSignalMapper::mappedInt, this, &ExportDialog::slot_keepRatioChanged);
|
||||
connect(reset_mapper_, &QSignalMapper::mappedInt, this, &ExportDialog::slot_resetSize);
|
||||
connect(clipboard_mapper_, &QSignalMapper::mappedInt, this, &ExportDialog::slot_exportToClipBoard);
|
||||
#endif
|
||||
|
||||
diagrams_list_layout_ = new QGridLayout();
|
||||
|
||||
@@ -472,6 +465,16 @@ void ExportDialog::generateDxf(
|
||||
QList<Conductor *> list_conductors;
|
||||
QList<DiagramTextItem *> list_texts;
|
||||
QList<DiagramImageItem *> list_images;
|
||||
//Slave cross-reference labels. They hang off a DynamicElementTextItem
|
||||
//as plain QGraphicsTextItem children, so neither cast below picks them
|
||||
//up and they were missing from the DXF entirely.
|
||||
QList<QGraphicsTextItem *> list_xref_texts;
|
||||
//Master-side cross-reference item (the table/cross drawn next to a
|
||||
//report/master element). It paints itself with hand-written
|
||||
//QPainter code across three modes (drawAsCross/drawAsContacts/
|
||||
//drawAsPlcTable), so instead of hand-porting each one it's replayed
|
||||
//through DxfPaintEngine, which reuses paint() unmodified.
|
||||
QList<CrossRefItem *> list_master_xrefs;
|
||||
QList<QLineF *> list_lines;
|
||||
QList<QRectF *> list_rectangles;
|
||||
//QList<QRectF *> list_ellipses;
|
||||
@@ -493,8 +496,13 @@ void ExportDialog::generateDxf(
|
||||
list_shapes << dii;
|
||||
} else if (DynamicElementTextItem *deti = qgraphicsitem_cast<DynamicElementTextItem *>(qgi)) {
|
||||
list_texts << deti;
|
||||
if (QGraphicsTextItem *xref = deti->slaveXrefItem()) {
|
||||
list_xref_texts << xref;
|
||||
}
|
||||
} else if (QetGraphicsTableItem *gti = qgraphicsitem_cast<QetGraphicsTableItem *>(qgi)) {
|
||||
list_tables << gti;
|
||||
} else if (CrossRefItem *xref = qgraphicsitem_cast<CrossRefItem *>(qgi)) {
|
||||
list_master_xrefs << xref;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -688,6 +696,54 @@ void ExportDialog::generateDxf(
|
||||
}
|
||||
}
|
||||
|
||||
//Draw the slave cross-reference labels
|
||||
for (QGraphicsTextItem *xref : std::as_const(list_xref_texts))
|
||||
{
|
||||
qreal fontSize = xref->font().pointSizeF();
|
||||
if (fontSize < 0)
|
||||
fontSize = xref->font().pixelSize();
|
||||
|
||||
qreal angle = xref->rotation();
|
||||
QGraphicsItem *parent = xref->parentItem();
|
||||
while (parent) {
|
||||
angle += parent->rotation();
|
||||
parent = parent->parentItem();
|
||||
}
|
||||
|
||||
qreal angler = angle * M_PI/180;
|
||||
int xdir = -sin(angler);
|
||||
int ydir = -cos(angler);
|
||||
qreal x = xref->scenePos().x()
|
||||
+ xdir * fontSize * 1.8
|
||||
- ydir * fontSize;
|
||||
qreal y = xref->scenePos().y()
|
||||
- ydir * fontSize * 1.8
|
||||
- xdir * fontSize * 0.9;
|
||||
|
||||
const QStringList lines = xref->toPlainText().split('\n');
|
||||
const qreal offset = fontSize * 1.6;
|
||||
for (const QString &line : lines) {
|
||||
if (line.size() > 0 && line != QLatin1String("_")) {
|
||||
Createdxf::drawText(file_path, line, QPointF(x, y), fontSize,
|
||||
360-angle, Createdxf::dxfColor(xref->defaultTextColor()), 0.72);
|
||||
}
|
||||
x += offset * xdir;
|
||||
y -= offset * ydir;
|
||||
}
|
||||
}
|
||||
|
||||
//Draw the master-side cross-reference items (table/cross), replaying
|
||||
//their existing paint() unmodified through DxfPaintEngine instead of
|
||||
//hand-porting drawAsCross()/drawAsContacts()/drawAsPlcTable().
|
||||
for (CrossRefItem *xref : std::as_const(list_master_xrefs))
|
||||
{
|
||||
DxfPaintDevice dxf_device(file_path);
|
||||
QPainter painter(&dxf_device);
|
||||
painter.setWorldTransform(xref->sceneTransform());
|
||||
xref->paintForExport(&painter);
|
||||
painter.end();
|
||||
}
|
||||
|
||||
Createdxf::dxfEnd(file_path);
|
||||
|
||||
saveReloadDiagramParameters(diagram, false);
|
||||
|
||||
@@ -99,7 +99,14 @@ QPixmap ElementPictureFactory::pixmap(const ElementsLocation &location)
|
||||
int hsy = qMin(doc.document_element().attribute("hotspot_y").as_int(), h);
|
||||
|
||||
QPixmap pix(w, h);
|
||||
pix.fill(QColor(255, 255, 255, 0));
|
||||
//Element definitions almost always draw with a hardcoded black
|
||||
//stroke color, on the assumption of the white diagram sheet they
|
||||
//are normally placed on. A transparent background here makes
|
||||
//that stroke disappear against a dark widget/tree-view background
|
||||
//(bugtracker #335). Give it an opaque white background instead -
|
||||
//exactly what the element already assumes visually, in every
|
||||
//context this pixmap is used (tree icons, drag icon, previews).
|
||||
pix.fill(Qt::white);
|
||||
|
||||
QPainter painter(&pix);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
@@ -612,14 +619,7 @@ void ElementPictureFactory::setPainterStyle(const QDomElement &dom, QPainter &pa
|
||||
pen.setCapStyle(Qt::SquareCap);
|
||||
|
||||
//Get the couples style/value
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
|
||||
const QStringList styles = dom.attribute("style").split(";", QString::SkipEmptyParts);
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 5.14 or later")
|
||||
#endif
|
||||
const QStringList styles = dom.attribute("style").split(";", Qt::SkipEmptyParts);
|
||||
#endif
|
||||
|
||||
QRegularExpression rx("^(?<name>[a-z-]+):(?<value>[a-zA-Z-]+)$");
|
||||
if (!rx.isValid())
|
||||
|
||||
@@ -730,11 +730,25 @@ void GenericPanel::projectInformationsChanged(QETProject *project) {
|
||||
/**
|
||||
@brief GenericPanel::diagramAdded
|
||||
@param project
|
||||
@param diagram (unused)
|
||||
@param diagram the newly added diagram to select in the tree
|
||||
*/
|
||||
void GenericPanel::diagramAdded(QETProject *project, Diagram *diagram) {
|
||||
Q_UNUSED(diagram)
|
||||
addProject(project, nullptr, GenericPanel::AddChildDiagrams);
|
||||
if (diagram) {
|
||||
QTreeWidgetItem *projectItem = itemForProject(project);
|
||||
if (projectItem) {
|
||||
for (int i = 0; i < projectItem->childCount(); ++i) {
|
||||
QTreeWidgetItem *child = projectItem->child(i);
|
||||
if (child && child->type() == QET::Diagram
|
||||
&& valueForItem<Diagram *>(child) == diagram) {
|
||||
clearSelection();
|
||||
setCurrentItem(child);
|
||||
child->setSelected(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
emit(panelContentChanged());
|
||||
}
|
||||
|
||||
|
||||
@@ -45,11 +45,11 @@ QList<QPair<QString, QString>> langDict(const QString &value)
|
||||
for (int i = 0; i < marks.size(); ++i) {
|
||||
const QRegularExpressionMatch &m = marks.at(i);
|
||||
const QString code = value.mid(m.capturedStart(),
|
||||
m.capturedLength() - 1); // drop '@'
|
||||
m.capturedLength() - 1); // drop '@'
|
||||
const int text_start = m.capturedEnd();
|
||||
const int text_end = (i + 1 < marks.size())
|
||||
? marks.at(i + 1).capturedStart()
|
||||
: value.size();
|
||||
? marks.at(i + 1).capturedStart()
|
||||
: value.size();
|
||||
QString text = value.mid(text_start, text_end - text_start);
|
||||
while (!text.isEmpty() && text.back().isSpace()) {
|
||||
text.chop(1);
|
||||
@@ -77,7 +77,7 @@ QString pickLang(const QString &value)
|
||||
return value.trimmed();
|
||||
}
|
||||
for (const QString &code : {QStringLiteral("en_EN"), QStringLiteral("en_US"),
|
||||
QStringLiteral("en_GB")}) {
|
||||
QStringLiteral("en_GB")}) {
|
||||
for (const auto &p : d) {
|
||||
if (p.first == code && !p.second.isEmpty()) {
|
||||
return p.second;
|
||||
@@ -158,11 +158,20 @@ bool EdzPart::parse(const QString &part_xml_path)
|
||||
return false;
|
||||
}
|
||||
QDomDocument doc;
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0) // TODO Qt 6.5: remove
|
||||
QString parse_err;
|
||||
int line = 0, col = 0;
|
||||
if (!doc.setContent(&file, &parse_err, &line, &col)) {
|
||||
m_error = QStringLiteral("Malformed part.xml (line %1): %2")
|
||||
.arg(line).arg(parse_err);
|
||||
#else
|
||||
const auto result = doc.setContent(&file);
|
||||
if (!result) {
|
||||
m_error = QStringLiteral("Malformed part.xml (line %1, column %2): %3")
|
||||
.arg(result.errorLine)
|
||||
.arg(result.errorColumn)
|
||||
.arg(result.errorMessage);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
file.close();
|
||||
|
||||
+1
-10
@@ -103,24 +103,15 @@ int main(int argc, char **argv)
|
||||
QDomImplementation::ReturnNullNode);
|
||||
//Creation and execution of the application
|
||||
//HighDPI
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
|
||||
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 6 or later")
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
#if QT_VERSION > QT_VERSION_CHECK(5, 7, 0) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
|
||||
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
|
||||
#endif
|
||||
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
qputenv("QT_ENABLE_HIGHDPI_SCALING", "1");
|
||||
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFactorRoundingPolicy());
|
||||
#endif
|
||||
|
||||
|
||||
// Headless command-line export: render a project to PDF/PNG/SVG without
|
||||
|
||||
@@ -252,4 +252,10 @@ void NewElementWizard::createNewElement()
|
||||
loc_.addToPath(m_chosen_file);
|
||||
edit_new_element -> setLocation(loc_);
|
||||
edit_new_element -> show();
|
||||
// Without this, the just-closed wizard can leave the main window
|
||||
// as the active/key window (bugtracker #281, reported on macOS):
|
||||
// the new editor is created and shown, but stays behind the main
|
||||
// window and doesn't surface in the Dock/Windows menu.
|
||||
edit_new_element -> raise();
|
||||
edit_new_element -> activateWindow();
|
||||
}
|
||||
|
||||
+277
-3
@@ -23,8 +23,8 @@
|
||||
#include "qetgraphicsitem/element.h"
|
||||
#include "qetgraphicsitem/elementtextitemgroup.h"
|
||||
|
||||
// Private Qt PDF engine for drawHyperlink() — not public API, stable since Qt4.
|
||||
// Requires QT += gui-private in qelectrotech.pro / gui-private in CMake.
|
||||
// Private Qt PDF engine for drawHyperlink() — not public API.
|
||||
// Availability of Qt::GuiPrivate is verified at configure time in CMakeLists.txt.
|
||||
#include <private/qpdf_p.h>
|
||||
|
||||
#include <QByteArray>
|
||||
@@ -228,7 +228,7 @@ void convertUriToGoTo(const QString &pdfPath)
|
||||
|
||||
// Find closing ')' of the URI value
|
||||
int uriStart = found + sUri.size();
|
||||
int closeParen = data.indexOf(')', uriStart);
|
||||
int closeParen = data.indexOf(")\n", uriStart);
|
||||
if (closeParen == -1) {
|
||||
// Malformed — copy rest verbatim
|
||||
out.append(data.mid(found));
|
||||
@@ -238,6 +238,15 @@ void convertUriToGoTo(const QString &pdfPath)
|
||||
|
||||
QByteArray uriVal = data.mid(uriStart, closeParen - uriStart);
|
||||
|
||||
// Skip component-info annotations — handled by
|
||||
// convertComponentInfoAnnotations() in a separate pass.
|
||||
if (uriVal.startsWith("componentinfo://")
|
||||
|| uriVal.startsWith("http://componentinfo.local/")) {
|
||||
out.append(data.mid(found, closeParen + 1 - found));
|
||||
pos = closeParen + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract page number: look for #page=N or bare page=N
|
||||
int pageNum = -1;
|
||||
int hashPos = uriVal.lastIndexOf("#page=");
|
||||
@@ -379,4 +388,269 @@ void convertUriToGoTo(const QString &pdfPath)
|
||||
out.close();
|
||||
}
|
||||
|
||||
void convertComponentInfoAnnotations(const QString &pdfPath,
|
||||
const QList<ComponentInfo> &annotations)
|
||||
{
|
||||
if (annotations.isEmpty()) return;
|
||||
|
||||
QFile f(pdfPath);
|
||||
if (!f.open(QIODevice::ReadOnly)) return;
|
||||
QByteArray data = f.readAll();
|
||||
f.close();
|
||||
|
||||
const QByteArray markerPrefix("http://componentinfo.local/");
|
||||
if (!data.contains(markerPrefix)) return;
|
||||
|
||||
int xrefStart = data.lastIndexOf("\nxref\n");
|
||||
if (xrefStart == -1) xrefStart = data.lastIndexOf("\nxref ");
|
||||
if (xrefStart == -1) return;
|
||||
++xrefStart;
|
||||
QByteArray body = data.left(xrefStart);
|
||||
|
||||
// Pre-scan body for highest object number
|
||||
int maxObjNum = 0;
|
||||
{
|
||||
const QByteArray objMarker(" 0 obj");
|
||||
int p = 0;
|
||||
while ((p = body.indexOf(objMarker, p)) != -1) {
|
||||
int numStart = p - 1;
|
||||
while (numStart > 0 && body[numStart - 1] != '\n' && body[numStart - 1] != '\r')
|
||||
--numStart;
|
||||
QByteArray numStr = body.mid(numStart, p - numStart).trimmed();
|
||||
bool ok = false;
|
||||
int num = numStr.toInt(&ok);
|
||||
if (ok && num > maxObjNum)
|
||||
maxObjNum = num;
|
||||
++p;
|
||||
}
|
||||
}
|
||||
// Empty Form XObject used as invisible appearance for all annotations.
|
||||
// All annotations reference this single object.
|
||||
int emptyXObjNum = maxObjNum + 1;
|
||||
|
||||
QByteArray out;
|
||||
out.reserve(data.size());
|
||||
|
||||
int pos = 0;
|
||||
bool anyConverted = false;
|
||||
|
||||
while (pos < body.size()) {
|
||||
// Find next indexed marker: http://componentinfo.local/<N>
|
||||
int markerPos = body.indexOf(markerPrefix, pos);
|
||||
if (markerPos == -1) {
|
||||
out.append(body.mid(pos));
|
||||
break;
|
||||
}
|
||||
|
||||
// Extract index from marker URL
|
||||
int idxStart = markerPos + markerPrefix.size();
|
||||
int idxEnd = idxStart;
|
||||
while (idxEnd < body.size() && body[idxEnd] >= '0' && body[idxEnd] <= '9')
|
||||
++idxEnd;
|
||||
if (idxEnd == idxStart) {
|
||||
// No index — skip malformed marker
|
||||
out.append(body.mid(pos, markerPos + markerPrefix.size() - pos));
|
||||
pos = markerPos + markerPrefix.size();
|
||||
continue;
|
||||
}
|
||||
int annotIndex = body.mid(idxStart, idxEnd - idxStart).toInt();
|
||||
|
||||
int uriOpen = body.lastIndexOf("/URI (", markerPos);
|
||||
int closeParen = (uriOpen != -1) ? body.indexOf(')', markerPos) : -1;
|
||||
if (uriOpen == -1 || closeParen == -1 || uriOpen < pos) {
|
||||
out.append(body.mid(pos, idxEnd - pos));
|
||||
pos = idxEnd;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find /S /URI before /URI (
|
||||
int sUriPos = body.lastIndexOf("/S /URI", uriOpen);
|
||||
|
||||
// Find the /A << that opens the action dict containing /S /URI.
|
||||
// Qt always writes: /A <<\n/S /URI\n/URI (...)\n>>\n>>
|
||||
int aDictOpen = (sUriPos != -1)
|
||||
? body.lastIndexOf("/A <<", sUriPos)
|
||||
: -1;
|
||||
|
||||
// If /A << is not found, leave this annotation untouched
|
||||
if (aDictOpen == -1 || aDictOpen < pos) {
|
||||
out.append(body.mid(pos, closeParen + 1 - pos));
|
||||
pos = closeParen + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate index
|
||||
if (annotIndex < 0 || annotIndex >= annotations.size()) {
|
||||
// Index out of range — keep original annotation intact
|
||||
out.append(body.mid(pos, closeParen + 1 - pos));
|
||||
pos = closeParen + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Copy annotation dict header up to /A <<
|
||||
out.append(body.mid(pos, aDictOpen - pos));
|
||||
|
||||
QByteArray contents = annotations[annotIndex].contents.toUtf8();
|
||||
|
||||
// Replace /Subtype /Link with /Subtype /Text, bounded to the enclosing
|
||||
// PDF object. Anchoring to " 0 obj" prevents hitting /Subtype /Link
|
||||
// from a cross-ref annotation that sits between the previous marker
|
||||
// and the current annotation on the same page.
|
||||
int objStart = body.lastIndexOf(" 0 obj", aDictOpen);
|
||||
int subTypeInBody = (objStart != -1)
|
||||
? body.indexOf("/Subtype /Link", objStart) : -1;
|
||||
int subTypePos = (subTypeInBody != -1 && subTypeInBody < aDictOpen)
|
||||
? out.size() - (aDictOpen - subTypeInBody) : -1;
|
||||
if (subTypePos != -1)
|
||||
// Acrobat draws its own sticky-note icon for /Subtype /Text
|
||||
// whatever /AP says (verified on Reader 5.0). /Square with no
|
||||
// /C and no /IC draws nothing anywhere, and is still a markup
|
||||
// annotation, so the /Contents popup is unaffected.
|
||||
out.replace(subTypePos, 14, "/Subtype /Square");
|
||||
|
||||
// Encode as UTF-16BE hex with BOM for proper Unicode support (Umlauten etc.).
|
||||
// PDF spec: hex strings starting with FE FF are interpreted as UTF-16BE.
|
||||
QByteArray utf16be;
|
||||
utf16be.append('\xfe');
|
||||
utf16be.append('\xff');
|
||||
{
|
||||
for (int i = 0; i < contents.size(); ) {
|
||||
ushort cp = 0;
|
||||
uchar c = static_cast<uchar>(contents.at(i));
|
||||
if (c < 0x80) {
|
||||
cp = c;
|
||||
++i;
|
||||
} else if ((c & 0xE0) == 0xC0 && i + 1 < contents.size()) {
|
||||
cp = ((c & 0x1F) << 6)
|
||||
| (static_cast<uchar>(contents.at(i + 1)) & 0x3F);
|
||||
i += 2;
|
||||
} else if ((c & 0xF0) == 0xE0 && i + 2 < contents.size()) {
|
||||
cp = ((c & 0x0F) << 12)
|
||||
| ((static_cast<uchar>(contents.at(i + 1)) & 0x3F) << 6)
|
||||
| (static_cast<uchar>(contents.at(i + 2)) & 0x3F);
|
||||
i += 3;
|
||||
} else {
|
||||
cp = 0xFFFD; // replacement character
|
||||
++i;
|
||||
}
|
||||
utf16be.append(static_cast<char>((cp >> 8) & 0xFF));
|
||||
utf16be.append(static_cast<char>(cp & 0xFF));
|
||||
}
|
||||
}
|
||||
|
||||
out += "/Contents <";
|
||||
out += utf16be.toHex();
|
||||
out += ">\n/AP << /N " + QByteArray::number(emptyXObjNum) + " 0 R >>\n";
|
||||
|
||||
// Skip the entire /A << ... >> action dict.
|
||||
// After closeParen ')' we have: \n>> (closes /A dict) \n>> (closes annot dict)
|
||||
int actionDictClose = body.indexOf(">>", closeParen + 1);
|
||||
if (actionDictClose != -1) {
|
||||
pos = actionDictClose + 2; // skip past >> that closes /A dict
|
||||
} else {
|
||||
pos = closeParen + 1;
|
||||
}
|
||||
anyConverted = true;
|
||||
}
|
||||
|
||||
if (!anyConverted) return;
|
||||
|
||||
// Append empty Form XObject (shared by all annotations for invisible appearance)
|
||||
QByteArray emptyXObj;
|
||||
emptyXObj += QByteArray::number(emptyXObjNum) + " 0 obj\n";
|
||||
emptyXObj += "<< /Type /XObject /Subtype /Form /BBox [0 0 0 0] /Length 0 >>\n";
|
||||
emptyXObj += "stream\nendstream\n";
|
||||
emptyXObj += "endobj\n";
|
||||
out += emptyXObj;
|
||||
|
||||
// Rebuild xref table
|
||||
QMap<int, int> offsets;
|
||||
{
|
||||
const QByteArray objMarker(" 0 obj");
|
||||
int p = 0;
|
||||
while ((p = out.indexOf(objMarker, p)) != -1) {
|
||||
int numStart = p - 1;
|
||||
while (numStart > 0 && out[numStart - 1] != '\n' && out[numStart - 1] != '\r')
|
||||
--numStart;
|
||||
QByteArray numStr = out.mid(numStart, p - numStart).trimmed();
|
||||
bool ok = false;
|
||||
int objNum = numStr.toInt(&ok);
|
||||
if (ok && objNum > 0)
|
||||
offsets[objNum] = numStart;
|
||||
++p;
|
||||
}
|
||||
}
|
||||
|
||||
if (offsets.isEmpty()) return;
|
||||
|
||||
int maxObj = offsets.lastKey();
|
||||
|
||||
QByteArray xref;
|
||||
xref += "xref\n";
|
||||
xref += "0 " + QByteArray::number(maxObj + 1) + "\n";
|
||||
xref += "0000000000 65535 f \n";
|
||||
for (int i = 1; i <= maxObj; ++i) {
|
||||
if (offsets.contains(i)) {
|
||||
xref += QByteArray::number(offsets[i]).rightJustified(10, '0')
|
||||
+ " 00000 n \n";
|
||||
} else {
|
||||
xref += "0000000000 65535 f \n";
|
||||
}
|
||||
}
|
||||
|
||||
// Copy trailer and bump /Size to account for the new XObject
|
||||
QByteArray trailer;
|
||||
{
|
||||
int tPos = data.indexOf("trailer", xrefStart);
|
||||
if (tPos != -1) {
|
||||
int tEnd = data.indexOf("%%EOF", tPos);
|
||||
if (tEnd != -1) tEnd += 5;
|
||||
if (tEnd != -1)
|
||||
trailer = data.mid(tPos, tEnd - tPos);
|
||||
}
|
||||
}
|
||||
if (trailer.isEmpty())
|
||||
trailer = "trailer\n<<>>\n%%EOF";
|
||||
|
||||
// Bump /Size: original was maxObjNum+1, now it's emptyXObjNum+1
|
||||
{
|
||||
int sizePos = trailer.indexOf("/Size ");
|
||||
if (sizePos != -1) {
|
||||
int numStart = sizePos + 6;
|
||||
int numEnd = numStart;
|
||||
while (numEnd < trailer.size() && trailer[numEnd] >= '0' && trailer[numEnd] <= '9')
|
||||
++numEnd;
|
||||
if (numEnd > numStart) {
|
||||
trailer.replace(numStart, numEnd - numStart,
|
||||
QByteArray::number(emptyXObjNum + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicate startxref if present in copied trailer
|
||||
{
|
||||
int stPos = trailer.indexOf("\nstartxref\n");
|
||||
if (stPos != -1)
|
||||
trailer = trailer.left(stPos);
|
||||
// Ensure trailer ends with %%EOF
|
||||
if (!trailer.endsWith("%%EOF\n"))
|
||||
trailer += "\n%%EOF\n";
|
||||
}
|
||||
|
||||
QByteArray result;
|
||||
result.reserve(out.size() + xref.size() + trailer.size() + 64);
|
||||
result += out;
|
||||
int newXrefOffset = out.size();
|
||||
result += xref;
|
||||
result += trailer;
|
||||
result += "\nstartxref\n";
|
||||
result += QByteArray::number(newXrefOffset);
|
||||
result += "\n%%EOF\n";
|
||||
|
||||
QFile outF(pdfPath);
|
||||
if (!outF.open(QIODevice::WriteOnly | QIODevice::Truncate)) return;
|
||||
outF.write(result);
|
||||
outF.close();
|
||||
}
|
||||
|
||||
} // namespace PdfLinks
|
||||
|
||||
@@ -74,6 +74,18 @@ namespace PdfLinks {
|
||||
*/
|
||||
void convertUriToGoTo(const QString &pdfPath);
|
||||
|
||||
struct ComponentInfo {
|
||||
QString contents;
|
||||
};
|
||||
|
||||
/**
|
||||
Post-process a Qt-generated PDF file: convert component-info placeholder
|
||||
link annotations (http://componentinfo.local/<N>) into invisible text
|
||||
annotations with the actual component info as /Contents.
|
||||
*/
|
||||
void convertComponentInfoAnnotations(const QString &pdfPath,
|
||||
const QList<ComponentInfo> &annotations);
|
||||
|
||||
}
|
||||
|
||||
#endif // PDF_LINKS_H
|
||||
|
||||
@@ -20,16 +20,18 @@
|
||||
#include "../diagram.h"
|
||||
#include "../pdf_links.h"
|
||||
#include "../qeticons.h"
|
||||
#include "../qetinformation.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../qetversion.h"
|
||||
#include "../qetgraphicsitem/crossrefitem.h"
|
||||
#include "../qetgraphicsitem/dynamicelementtextitem.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "../qetgraphicsitem/elementtextitemgroup.h"
|
||||
|
||||
#include "ui_projectprintwindow.h"
|
||||
|
||||
// Private Qt PDF engine for drawHyperlink() — not public API, stable since Qt4
|
||||
// Requires QT += gui-private in qelectrotech.pro
|
||||
// Private Qt PDF engine for drawHyperlink() is not a public API.
|
||||
// Availability of Qt::GuiPrivate is verified at configure time in CMakeLists.txt.
|
||||
#include <private/qpdf_p.h>
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
|
||||
@@ -63,14 +65,7 @@ void ProjectPrintWindow::launchDialog(QETProject *project, QPrinter::OutputForma
|
||||
auto printer_ = new QPrinter();
|
||||
QPrinter printer(QPrinter::HighResolution);
|
||||
printer_->setDocName(ProjectPrintWindow::docName(project));
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||
printer_->setOrientation(QPrinter::Landscape);
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 6 or later")
|
||||
# endif
|
||||
printer_->setPageOrientation(QPageLayout::Landscape);
|
||||
#endif
|
||||
|
||||
if (format == QPrinter::NativeFormat) //To physical printer
|
||||
{
|
||||
@@ -79,10 +74,14 @@ void ProjectPrintWindow::launchDialog(QETProject *project, QPrinter::OutputForma
|
||||
print_dialog.setWindowFlags(Qt::Sheet);
|
||||
#endif
|
||||
print_dialog.setWindowTitle(tr("Options d'impression", "window title"));
|
||||
// setOptions() is the modern spelling of the Qt4-era
|
||||
// setEnabledOptions() (removed in Qt 6): replace the enabled
|
||||
// option set with just PrintShowPageSize, on Qt 5 and 6 alike.
|
||||
print_dialog.setOptions(QAbstractPrintDialog::PrintShowPageSize);
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
|
||||
print_dialog.setEnabledOptions(QAbstractPrintDialog::PrintShowPageSize);
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 6 or later")
|
||||
#endif
|
||||
qDebug()<<"Help code for QT 6 or later";
|
||||
#endif
|
||||
if (print_dialog.exec() == QDialog::Rejected) {
|
||||
delete printer_;
|
||||
return;
|
||||
@@ -108,7 +107,7 @@ QString ProjectPrintWindow::docName(QETProject *project)
|
||||
{
|
||||
QString doc_name;
|
||||
if (!project->filePath().isEmpty()) {
|
||||
doc_name = QFileInfo(project->filePath()).baseName();
|
||||
doc_name = QFileInfo(project->filePath()).completeBaseName();
|
||||
} else if (!project->title().isEmpty()) {
|
||||
doc_name = project->title();
|
||||
doc_name = QET::stringToFileName(doc_name);
|
||||
@@ -156,6 +155,7 @@ ProjectPrintWindow::ProjectPrintWindow(QETProject *project, QPrinter *printer, Q
|
||||
ui->m_button_box->addButton(pdf_button, QDialogButtonBox::ActionRole);
|
||||
connect(pdf_button, &QPushButton::clicked, this, &ProjectPrintWindow::exportToPDF);
|
||||
}
|
||||
ui->m_component_info_cb->setVisible(m_printer->outputFormat() == QPrinter::PdfFormat);
|
||||
|
||||
auto exp = ExportProperties::defaultPrintProperties();
|
||||
ui->m_draw_border_cb->setChecked(exp.draw_border);
|
||||
@@ -200,7 +200,6 @@ ProjectPrintWindow::~ProjectPrintWindow()
|
||||
*/
|
||||
void ProjectPrintWindow::requestPaint()
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
|
||||
#ifdef Q_OS_WIN
|
||||
#ifdef QT_DEBUG
|
||||
qDebug() << "--";
|
||||
@@ -225,7 +224,6 @@ void ProjectPrintWindow::requestPaint()
|
||||
qDebug() << "--";
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
if (!m_project->diagrams().count()) {
|
||||
return;
|
||||
@@ -253,6 +251,7 @@ void ProjectPrintWindow::requestPaint()
|
||||
|
||||
bool first = true;
|
||||
QPainter painter(m_printer);
|
||||
int annotIndex = 0;
|
||||
|
||||
// A real PDF export uses the QPdfEngine; the on-screen preview uses a
|
||||
// preview paint engine. We only post-process when actually writing a PDF.
|
||||
@@ -266,7 +265,7 @@ void ProjectPrintWindow::requestPaint()
|
||||
for (auto diagram : selectedDiagram())
|
||||
{
|
||||
first ? first = false : m_printer->newPage();
|
||||
printDiagram(diagram, ui->m_fit_in_page_cb->isChecked(), &painter, m_printer, diagramPageMap);
|
||||
printDiagram(diagram, ui->m_fit_in_page_cb->isChecked(), &painter, m_printer, diagramPageMap, annotIndex);
|
||||
}
|
||||
|
||||
// Note: do NOT call painter.end() or pdfConvertUriToGoTo() here.
|
||||
@@ -284,7 +283,7 @@ void ProjectPrintWindow::requestPaint()
|
||||
* @param fit_page
|
||||
* @param printer
|
||||
*/
|
||||
void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap<Diagram*, int> &diagramPageMap)
|
||||
void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap<Diagram*, int> &diagramPageMap, int &annotIndex)
|
||||
{
|
||||
|
||||
////Prepare the print////
|
||||
@@ -313,16 +312,10 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter
|
||||
diagram->render(painter, QRectF(), diagram_rect, Qt::KeepAspectRatio);
|
||||
} else {
|
||||
// Print on one or several pages
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||
auto printed_rect = full_page ? printer->paperRect() : printer->pageRect();
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 6 or later")
|
||||
#endif
|
||||
qDebug()<<"Help code for QT 6 or later";
|
||||
|
||||
auto printed_rect = full_page ? printer->paperRect(QPrinter::Millimeter) :
|
||||
printer->pageRect(QPrinter::Millimeter);
|
||||
#endif
|
||||
|
||||
auto used_width = printed_rect.width();
|
||||
auto used_height = printed_rect.height();
|
||||
auto h_pages_count = horizontalPagesCount(diagram, option, full_page);
|
||||
@@ -426,6 +419,53 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter
|
||||
PdfLinks::injectCrossRefLinks(
|
||||
pdfEngine, diagram, geom, diagramPageMap,
|
||||
printer->outputFileName());
|
||||
|
||||
////Collect component info for popup annotations////
|
||||
if (ui->m_component_info_cb->isChecked()) {
|
||||
for (auto *item : diagram->items()) {
|
||||
auto *el = qgraphicsitem_cast<Element*>(item);
|
||||
if (!el) continue;
|
||||
|
||||
// Skip reports and slaves
|
||||
auto lt = el->linkType();
|
||||
if (lt & Element::AllReport) continue;
|
||||
if (lt == Element::Slave) continue;
|
||||
|
||||
auto info = el->elementInformations();
|
||||
if (info.count() == 0) continue;
|
||||
|
||||
// Build info text
|
||||
QStringList lines;
|
||||
for (const auto &key : {QStringLiteral("label"),
|
||||
QStringLiteral("manufacturer"),
|
||||
QStringLiteral("designation"),
|
||||
QStringLiteral("description")}) {
|
||||
if (info.contains(key) && !info.value(key).toString().isEmpty())
|
||||
lines << QETInformation::translatedInfoKey(key) + ": " + info.value(key).toString();
|
||||
}
|
||||
for (const QString &key : info.keys()) {
|
||||
if (key == "formula") continue;
|
||||
QString translated = QETInformation::translatedInfoKey(key);
|
||||
if (lines.contains(translated + ": " + info.value(key).toString())) continue;
|
||||
if (info.value(key).toString().isEmpty()) continue;
|
||||
lines << translated + ": " + info.value(key).toString();
|
||||
}
|
||||
if (lines.isEmpty()) continue;
|
||||
|
||||
// Compute element rect in device pixels
|
||||
QRectF elemScene = el->mapRectToScene(el->boundingRect());
|
||||
QRectF devRect = fit.mapRect(elemScene);
|
||||
|
||||
PdfLinks::ComponentInfo ci;
|
||||
ci.contents = lines.join("\n");
|
||||
m_componentInfoList.append(ci);
|
||||
|
||||
// Create a link annotation as placeholder — post-processing
|
||||
// will convert it to an invisible text annotation with the actual content
|
||||
pdfEngine->drawHyperlink(devRect, QUrl(QString("http://componentinfo.local/%1").arg(annotIndex++)));
|
||||
}
|
||||
}
|
||||
////Component info end////
|
||||
}
|
||||
}
|
||||
////PDF links end////
|
||||
@@ -450,8 +490,11 @@ QRect ProjectPrintWindow::diagramRect(Diagram *diagram, const ExportProperties &
|
||||
{
|
||||
auto diagram_rect = diagram->border_and_titleblock.borderAndTitleBlockRect();
|
||||
if (!option.draw_titleblock) {
|
||||
auto titleblock_height = diagram->border_and_titleblock.titleBlockRect().height();
|
||||
diagram_rect.setHeight(diagram_rect.height() - titleblock_height);
|
||||
auto titleblock_rect = diagram->border_and_titleblock.titleBlockRect();
|
||||
if (diagram->border_and_titleblock.titleBlockEdge() == Qt::BottomEdge)
|
||||
diagram_rect.setHeight(diagram_rect.height() - titleblock_rect.height());
|
||||
else
|
||||
diagram_rect.setWidth(diagram_rect.width() - titleblock_rect.width());
|
||||
}
|
||||
|
||||
//Adjust the border of diagram to 1px (width of the line)
|
||||
@@ -472,17 +515,12 @@ int ProjectPrintWindow::horizontalPagesCount(
|
||||
Diagram *diagram, const ExportProperties &option, bool full_page) const
|
||||
{
|
||||
QRect printable_area;
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||
printable_area = full_page ? m_printer->paperRect() : m_printer->pageRect();
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 6 or later")
|
||||
# endif
|
||||
|
||||
printable_area =
|
||||
full_page ?
|
||||
m_printer->pageLayout().fullRectPixels(m_printer->resolution()) :
|
||||
m_printer->pageLayout().paintRectPixels(m_printer->resolution());
|
||||
#endif
|
||||
|
||||
QRect diagram_rect = diagramRect(diagram, option);
|
||||
|
||||
int h_pages_count = int(ceil(qreal(diagram_rect.width()) / qreal(printable_area.width())));
|
||||
@@ -501,17 +539,12 @@ int ProjectPrintWindow::verticalPagesCount(
|
||||
Diagram *diagram, const ExportProperties &option, bool full_page) const
|
||||
{
|
||||
QRect printable_area;
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||
printable_area = full_page ? m_printer->paperRect() : m_printer->pageRect();
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 6 or later")
|
||||
# endif
|
||||
|
||||
printable_area =
|
||||
full_page ?
|
||||
m_printer->pageLayout().fullRectPixels(m_printer->resolution()) :
|
||||
m_printer->pageLayout().paintRectPixels(m_printer->resolution());
|
||||
#endif
|
||||
|
||||
QRect diagram_rect = diagramRect(diagram, option);
|
||||
|
||||
int v_pages_count = int(ceil(qreal(diagram_rect.height()) / qreal(printable_area.height())));
|
||||
@@ -584,43 +617,7 @@ void ProjectPrintWindow::loadPageSetupForCurrentPrinter()
|
||||
}
|
||||
|
||||
settings.beginGroup(printer_section);
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||
if (settings.contains("orientation")) {
|
||||
QString value = settings.value("orientation", "landscape").toString();
|
||||
m_printer -> setOrientation(value == "landscape" ? QPrinter::Landscape : QPrinter::Portrait);
|
||||
}
|
||||
if (settings.contains("papersize")) {
|
||||
int value = settings.value("papersize", QPrinter::A4).toInt();
|
||||
if (value == QPrinter::Custom) {
|
||||
bool w_ok, h_ok;
|
||||
int w = settings.value("customwidthmm", -1).toInt(&w_ok);
|
||||
int h = settings.value("customheightmm", -1).toInt(&h_ok);
|
||||
if (w_ok && h_ok && w != -1 && h != -1) {
|
||||
m_printer -> setPaperSize(QSizeF(w, h), QPrinter::Millimeter);
|
||||
}
|
||||
} else if (value < QPrinter::Custom) {
|
||||
m_printer -> setPaperSize(static_cast<QPrinter::PaperSize>(value));
|
||||
}
|
||||
}
|
||||
|
||||
qreal margins[4];
|
||||
m_printer -> getPageMargins(&margins[0], &margins[1], &margins[2], &margins[3], QPrinter::Millimeter);
|
||||
QStringList margins_names(QStringList() << "left" << "top" << "right" << "bottom");
|
||||
for (int i = 0 ; i < 4 ; ++ i) {
|
||||
bool conv_ok;
|
||||
qreal value = settings.value("margin" + margins_names.at(i), -1.0).toReal(&conv_ok);
|
||||
if (conv_ok && value != -1.0) margins[i] = value;
|
||||
}
|
||||
m_printer->setPageMargins(
|
||||
margins[0],
|
||||
margins[1],
|
||||
margins[2],
|
||||
margins[3],
|
||||
QPrinter::Millimeter);
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 6 or later")
|
||||
# endif
|
||||
if (settings.contains("orientation"))
|
||||
{
|
||||
QString value = settings.value("orientation", "landscape").toString();
|
||||
@@ -673,7 +670,7 @@ void ProjectPrintWindow::loadPageSetupForCurrentPrinter()
|
||||
m_printer->setPageMargins(
|
||||
QMarginsF(margins[0], margins[1], margins[2], margins[3]),
|
||||
QPageLayout::Millimeter);
|
||||
#endif
|
||||
|
||||
m_printer->setFullPage(
|
||||
settings.value("fullpage", "false").toString() == "true");
|
||||
|
||||
@@ -690,35 +687,6 @@ void ProjectPrintWindow::savePageSetupForCurrentPrinter()
|
||||
settings.beginGroup("printers");
|
||||
settings.beginGroup(printer_section);
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
|
||||
settings.setValue("orientation", m_printer -> orientation() == QPrinter::Portrait ? "portrait" : "landscape");
|
||||
settings.setValue("papersize", int(m_printer -> paperSize()));
|
||||
if (m_printer -> paperSize() == QPrinter::Custom) {
|
||||
QSizeF size = m_printer -> paperSize(QPrinter::Millimeter);
|
||||
settings.setValue("customwidthmm", size.width());
|
||||
settings.setValue("customheightmm", size.height());
|
||||
} else {
|
||||
settings.remove("customwidthmm");
|
||||
settings.remove("customheightmm");
|
||||
}
|
||||
qreal left, top, right, bottom;
|
||||
m_printer
|
||||
->getPageMargins(&left, &top, &right, &bottom, QPrinter::Millimeter);
|
||||
settings.setValue("marginleft", left);
|
||||
settings.setValue("margintop", top);
|
||||
settings.setValue("marginright", right);
|
||||
settings.setValue("marginbottom", bottom);
|
||||
settings.setValue("fullpage", m_printer->fullPage() ? "true" : "false");
|
||||
settings.endGroup();
|
||||
settings.endGroup();
|
||||
settings.sync();
|
||||
|
||||
#else
|
||||
# if TODO_LIST
|
||||
# pragma message("@TODO remove code for QT 6 or later")
|
||||
# endif
|
||||
qDebug() << "Help code for QT 6 or later";
|
||||
|
||||
settings.setValue(
|
||||
"orientation",
|
||||
m_printer->pageLayout().orientation() == QPageLayout::Portrait ?
|
||||
@@ -749,7 +717,6 @@ void ProjectPrintWindow::savePageSetupForCurrentPrinter()
|
||||
settings.endGroup();
|
||||
settings.endGroup();
|
||||
settings.sync();
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -799,6 +766,7 @@ void ProjectPrintWindow::on_m_draw_titleblock_cb_clicked() { m_preview->upd
|
||||
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_component_info_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()
|
||||
{
|
||||
@@ -903,9 +871,16 @@ void ProjectPrintWindow::print()
|
||||
// which prevents the black screen observed on Apple Silicon under
|
||||
// macOS Sequoia (QPrintPreviewWidget + CALayer timing issue).
|
||||
QTimer::singleShot(0, this, [this, pdfFile]() {
|
||||
// Convert component-info link annotations into invisible text annotations
|
||||
if (ui->m_component_info_cb->isChecked() && !m_componentInfoList.isEmpty()) {
|
||||
PdfLinks::convertComponentInfoAnnotations(pdfFile, m_componentInfoList);
|
||||
m_componentInfoList.clear();
|
||||
}
|
||||
|
||||
// Convert URI link annotations into native internal GoTo/FitR
|
||||
// actions so cross-references jump inside the document.
|
||||
PdfLinks::convertUriToGoTo(pdfFile);
|
||||
|
||||
this->close();
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#define PROJECTPRINTWINDOW_H
|
||||
|
||||
#include "../exportproperties.h"
|
||||
#include "../pdf_links.h"
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QMap>
|
||||
@@ -56,6 +57,7 @@ class ProjectPrintWindow : public QMainWindow
|
||||
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_component_info_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();
|
||||
@@ -81,7 +83,7 @@ class ProjectPrintWindow : public QMainWindow
|
||||
|
||||
private:
|
||||
void requestPaint();
|
||||
void printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap<Diagram*, int> &diagramPageMap = {});
|
||||
void printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap<Diagram*, int> &diagramPageMap, int &annotIndex);
|
||||
QRect diagramRect(Diagram *diagram, const ExportProperties &option) const;
|
||||
int horizontalPagesCount(Diagram *diagram, const ExportProperties &option, bool full_page) const;
|
||||
int verticalPagesCount(Diagram *diagram, const ExportProperties &option, bool full_page) const;
|
||||
@@ -102,6 +104,7 @@ class ProjectPrintWindow : public QMainWindow
|
||||
QPrintPreviewWidget *m_preview=nullptr;
|
||||
QColor m_backup_diagram_background_color;
|
||||
QHash<Diagram *, QCheckBox *> m_diagram_list_hash;
|
||||
QList<PdfLinks::ComponentInfo> m_componentInfoList;
|
||||
};
|
||||
|
||||
#endif // PROJECTPRINTWINDOW_H
|
||||
|
||||
@@ -183,17 +183,27 @@
|
||||
</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>
|
||||
<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>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="m_component_info_cb">
|
||||
<property name="text">
|
||||
<string>Inscrire les informations des composants</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
||||
+18
-6
@@ -210,10 +210,14 @@ void ProjectView::changeFirstTab()
|
||||
}
|
||||
|
||||
/**
|
||||
@return first folio of current project
|
||||
@return first folio of current project, or nullptr if the project
|
||||
has no diagram.
|
||||
*/
|
||||
DiagramView *ProjectView::firstDiagram()
|
||||
{
|
||||
if (m_diagram_ids.isEmpty()) {
|
||||
return(nullptr);
|
||||
}
|
||||
return(m_diagram_ids.first());
|
||||
}
|
||||
|
||||
@@ -345,11 +349,19 @@ QString ProjectView::askUserForFilePath(bool assign) {
|
||||
// if no filepath is provided, return an empty string
|
||||
if (filepath.isEmpty()) return(filepath);
|
||||
|
||||
// if the name does not end with the .qet extension and we're _not_ using xdg-desktop-portal, append it
|
||||
bool usesPortal =
|
||||
qEnvironmentVariableIsSet("FLATPAK_ID") ||
|
||||
qEnvironmentVariableIsSet("SNAP_NAME");
|
||||
if (!filepath.endsWith(".qet", Qt::CaseInsensitive) && !usesPortal) filepath += ".qet";
|
||||
// Ensure the path ends with exactly one .qet extension, regardless of
|
||||
// whether the active file dialog already appended one. Whether it does
|
||||
// depends on which backend actually shows the dialog (Qt's own vs. the
|
||||
// xdg-desktop-portal used by sandboxed Snap/Flatpak builds), and that
|
||||
// isn't reliably predictable from environment variables alone -- an
|
||||
// earlier attempt at that (only appending when *not* Snap/Flatpak,
|
||||
// assuming the portal always appends it) left Snap saves with no
|
||||
// extension at all whenever the portal didn't (bugtracker #270).
|
||||
// Stripping any existing suffix first and re-appending it once is
|
||||
// correct either way.
|
||||
if (filepath.endsWith(".qet", Qt::CaseInsensitive))
|
||||
filepath.chop(4);
|
||||
filepath += ".qet";
|
||||
|
||||
if (assign) {
|
||||
// assign the provided filepath to the currently edited project
|
||||
|
||||
+16
-16
@@ -35,28 +35,28 @@ public:
|
||||
: QTabWidget(parent)
|
||||
{}
|
||||
|
||||
double temp_index = 0;
|
||||
|
||||
void wheelEvent(QWheelEvent *event) override
|
||||
{
|
||||
int index = currentIndex();
|
||||
double delta = 0;
|
||||
double scale_factor = 0.005; // Decrease or increase speed of mouse wheel (0.04 = decrease)
|
||||
if (event->modifiers() & Qt::ControlModifier) {
|
||||
if (index != -1) {
|
||||
delta = event->delta() * scale_factor; // Read and scale the scroll value
|
||||
if (delta > 0 && (temp_index > -1)) temp_index = temp_index - abs(delta);
|
||||
if (delta < 0 && (temp_index < count())) temp_index = temp_index + abs(delta);
|
||||
// Only intercept if Ctrl is held down
|
||||
if (event->modifiers() & Qt::ControlModifier) {
|
||||
int index = currentIndex();
|
||||
if (index != -1) {
|
||||
// angleDelta().y() is usually 120 per click.
|
||||
// Dividing by 120 gives you exactly +1 (scroll up) or -1 (scroll down)
|
||||
int steps = event->angleDelta().y() / 120;
|
||||
|
||||
index = int (temp_index);
|
||||
qDebug() << "index" << index << "temp_index" << temp_index << " " << event->delta() << delta;
|
||||
if (steps != 0) {
|
||||
int newIndex = index - steps;
|
||||
|
||||
if (index >= 0 && index < count())
|
||||
setCurrentIndex(index);
|
||||
|
||||
// qDebug() << currentIndex();
|
||||
if (newIndex >= 0 && newIndex < count())
|
||||
setCurrentIndex(newIndex);
|
||||
}
|
||||
}
|
||||
event->accept(); // Mark event handled so parent widgets don't process it
|
||||
} else {
|
||||
// Allow the default QTabWidget wheel behavior to process if Ctrl isn't held
|
||||
QTabWidget::wheelEvent(event);
|
||||
}
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -99,6 +99,19 @@ class ElementData : public PropertiesInterface
|
||||
int terminalCount = 1; ///< Number of terminals for this IO (1-4)
|
||||
QStringList terminals; ///< Terminal values T1, T2, ... (size = terminalCount)
|
||||
|
||||
/**
|
||||
* @brief Return terminal labels, generating defaults (T1, T2...) if empty
|
||||
*/
|
||||
QStringList effectiveTerminals() const {
|
||||
if (!terminals.isEmpty())
|
||||
return terminals;
|
||||
int count = qMax(terminalCount, 1);
|
||||
QStringList defaults;
|
||||
for (int i = 0; i < count && i < 4; ++i)
|
||||
defaults << QStringLiteral("T%1").arg(i + 1);
|
||||
return defaults;
|
||||
}
|
||||
|
||||
bool operator==(const PlcIO &other) const {
|
||||
return type == other.type
|
||||
&& address == other.address
|
||||
|
||||
@@ -90,7 +90,7 @@ void XRefProperties::fromSettings(const QSettings &settings,
|
||||
m_slave_label = settings.value(prefix % "slave_label", "(%f-%l%c)").toString();
|
||||
|
||||
QMetaEnum var = QMetaEnum::fromType<Qt::Alignment>();
|
||||
m_xref_pos = Qt::AlignmentFlag(var.keyToValue((settings.value(prefix % "xrefpos").toString()).toStdString().data()));
|
||||
m_xref_pos = Qt::AlignmentFlag(var.keyToValue((settings.value(prefix % "xrefpos", "AlignBottom").toString()).toStdString().data()));
|
||||
|
||||
for (QString key : m_prefix_keys) {
|
||||
m_prefix.insert(key, settings.value(prefix + key % "prefix").toString());
|
||||
|
||||
+4
-20
@@ -184,16 +184,8 @@ bool QET::orthogonalProjection(
|
||||
|
||||
// determine le point d'intersection des deux droites = le projete orthogonal
|
||||
QPointF intersection_point;
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 5.14 or later")
|
||||
#endif
|
||||
QLineF::IntersectType it = line.
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
|
||||
intersect // ### Qt 6: remove
|
||||
#else
|
||||
intersects
|
||||
#endif
|
||||
(perpendicular_line, &intersection_point);
|
||||
|
||||
QLineF::IntersectType it = line.intersects(perpendicular_line, &intersection_point);
|
||||
|
||||
// ne devrait pas arriver (mais bon...)
|
||||
if (it == QLineF::NoIntersection) return(false);
|
||||
@@ -546,16 +538,8 @@ QString QET::joinWithSpaces(const QStringList &string_list) {
|
||||
QStringList QET::splitWithSpaces(const QString &string) {
|
||||
// les chaines sont separees par des espaces non echappes
|
||||
// = avec un nombre nul ou pair de backslashes devant
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 5.14 or later")
|
||||
#endif
|
||||
QStringList escaped_strings = string.split(QRegularExpression("[^\\]?(?:\\\\)* "),
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
|
||||
QString
|
||||
#else
|
||||
Qt
|
||||
#endif
|
||||
::SkipEmptyParts);
|
||||
|
||||
QStringList escaped_strings = string.split(QRegularExpression("[^\\]?(?:\\\\)* "),Qt::SkipEmptyParts);
|
||||
|
||||
QStringList returned_list;
|
||||
foreach(QString escaped_string, escaped_strings) {
|
||||
|
||||
+4
-5
@@ -50,7 +50,7 @@
|
||||
#include <QFontDatabase>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QRegularExpression>
|
||||
#ifdef BUILD_WITHOUT_KF5
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
# include "ui/nokde/kautosavefile.h"
|
||||
#else
|
||||
# include <KAutoSaveFile>
|
||||
@@ -124,11 +124,8 @@ QETApp::QETApp() :
|
||||
initSplashScreen();
|
||||
initSystemTray();
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // TODO Qt6 only: remove, mappedObject() always available
|
||||
connect(&signal_map, SIGNAL(mapped(QWidget *)), this, SLOT(invertMainWindowVisibility(QWidget *)));
|
||||
#else
|
||||
connect(&signal_map, &QSignalMapper::mappedObject, this, [this](QObject *object) { invertMainWindowVisibility(qobject_cast<QWidget *>(object)); });
|
||||
#endif
|
||||
|
||||
qApp->setQuitOnLastWindowClosed(false);
|
||||
connect(qApp, &QApplication::lastWindowClosed,
|
||||
this, &QETApp::checkRemainingWindows);
|
||||
@@ -1978,6 +1975,7 @@ void QETApp::openTitleBlockTemplate(const TitleBlockTemplateLocation &location,
|
||||
qet_template_editor -> setOpenForDuplication(duplicate);
|
||||
qet_template_editor -> edit(location);
|
||||
qet_template_editor -> show();
|
||||
qet_template_editor -> readSettingsState(); // must run after show() in Qt6
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1989,6 +1987,7 @@ void QETApp::openTitleBlockTemplate(const QString &filepath) {
|
||||
QETTitleBlockTemplateEditor *qet_template_editor = new QETTitleBlockTemplateEditor();
|
||||
qet_template_editor -> edit(filepath);
|
||||
qet_template_editor -> show();
|
||||
qet_template_editor -> readSettingsState(); // must run after show() in Qt6
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
#include "conductornumexport.h"
|
||||
#include "diagramcommands.h"
|
||||
#include "diagramevent/diagrameventaddimage.h"
|
||||
#ifdef QET_HAS_QTPDF
|
||||
#include "diagramevent/diagrameventaddpdf.h"
|
||||
#endif
|
||||
#include "diagramevent/diagrameventaddshape.h"
|
||||
#include "diagramevent/diagrameventaddtext.h"
|
||||
#include "diagramview.h"
|
||||
@@ -57,7 +60,7 @@
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#ifdef BUILD_WITHOUT_KF5
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
# include "ui/nokde/kautosavefile.h"
|
||||
#else
|
||||
# include <KAutoSaveFile>
|
||||
@@ -107,11 +110,7 @@ QETDiagramEditor::QETDiagramEditor(const QStringList &files, QWidget *parent) :
|
||||
m_workspace.setTabsClosable(true);
|
||||
|
||||
//Set the signal mapper
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // TODO Qt6 only: remove, mappedObject() always available
|
||||
connect(&windowMapper, SIGNAL(mapped(QWidget *)), this, SLOT(activateWidget(QWidget *)));
|
||||
#else
|
||||
connect(&windowMapper, &QSignalMapper::mappedObject, this, [this](QObject *object) { activateWidget(qobject_cast<QWidget *>(object)); });
|
||||
#endif
|
||||
|
||||
setWindowTitle(tr("QElectroTech", "window title"));
|
||||
setWindowIcon(QET::Icons::QETLogo);
|
||||
@@ -136,8 +135,9 @@ QETDiagramEditor::QETDiagramEditor(const QStringList &files, QWidget *parent) :
|
||||
connect(&m_workspace, &QMdiArea::subWindowActivated, this, &QETDiagramEditor::subWindowActivated);
|
||||
connect(QApplication::clipboard(), &QClipboard::dataChanged, this, &QETDiagramEditor::slot_updatePasteAction);
|
||||
|
||||
readSettings();
|
||||
readSettings(); // restoreGeometry before show()
|
||||
show();
|
||||
readSettingsState(); // restoreState() must be called after show() in Qt6
|
||||
|
||||
//If valid file path is given as arguments
|
||||
uint opened_projects = 0;
|
||||
@@ -720,6 +720,9 @@ void QETDiagramEditor::setUpActions()
|
||||
//Adding action (add text, image, shape...)
|
||||
QAction *add_text = m_add_item_actions_group.addAction(QET::Icons::PartTextField, tr("Ajouter un champ de texte"));
|
||||
QAction *add_image = m_add_item_actions_group.addAction(QET::Icons::adding_image, tr("Ajouter une image"));
|
||||
#ifdef QET_HAS_QTPDF
|
||||
QAction *add_pdf = m_add_item_actions_group.addAction(QET::Icons::adding_pdf, tr("Ajouter un PDF"));
|
||||
#endif
|
||||
QAction *add_line = m_add_item_actions_group.addAction(QET::Icons::PartLine, tr("Ajouter une ligne", "Draw line"));
|
||||
QAction *add_rectangle = m_add_item_actions_group.addAction(QET::Icons::PartRectangle, tr("Ajouter un rectangle"));
|
||||
QAction *add_ellipse = m_add_item_actions_group.addAction(QET::Icons::PartEllipse, tr("Ajouter une ellipse"));
|
||||
@@ -728,6 +731,9 @@ void QETDiagramEditor::setUpActions()
|
||||
|
||||
add_text ->setStatusTip(tr("Ajoute un champ de texte sur le folio actuel"));
|
||||
add_image ->setStatusTip(tr("Ajoute une image sur le folio actuel"));
|
||||
#ifdef QET_HAS_QTPDF
|
||||
add_pdf ->setStatusTip(tr("Ajoute une page PDF sur le folio actuel"));
|
||||
#endif
|
||||
add_line ->setStatusTip(tr("Ajoute une ligne sur le folio actuel"));
|
||||
add_rectangle->setStatusTip(tr("Ajoute un rectangle sur le folio actuel"));
|
||||
add_ellipse ->setStatusTip(tr("Ajoute une ellipse sur le folio actuel"));
|
||||
@@ -736,6 +742,9 @@ void QETDiagramEditor::setUpActions()
|
||||
|
||||
add_text ->setData(QStringLiteral("text"));
|
||||
add_image ->setData(QStringLiteral("image"));
|
||||
#ifdef QET_HAS_QTPDF
|
||||
add_pdf ->setData(QStringLiteral("pdf"));
|
||||
#endif
|
||||
add_line ->setData(QStringLiteral("line"));
|
||||
add_rectangle->setData(QStringLiteral("rectangle"));
|
||||
add_ellipse ->setData(QStringLiteral("ellipse"));
|
||||
@@ -770,7 +779,7 @@ void QETDiagramEditor::setUpActions()
|
||||
});
|
||||
|
||||
m_jump_to_element = new QAction(tr("Atteindre un élément"), this);
|
||||
m_jump_to_element->setShortcut(Qt::CTRL | Qt::Key_G);
|
||||
ShortcutManager::instance().registerAction(m_jump_to_element, "diagrameditor.jump_to_element", tr("Éditeur de schémas"), Qt::CTRL | Qt::Key_G);
|
||||
m_jump_to_element->setStatusTip(tr("Recherche et sélectionne rapidement un élément du folio", "status bar tip"));
|
||||
connect(m_jump_to_element, &QAction::triggered, [this]()
|
||||
{
|
||||
@@ -1573,6 +1582,19 @@ void QETDiagramEditor::addItemGroupTriggered(QAction *action)
|
||||
else
|
||||
diagram_event = deai;
|
||||
}
|
||||
#ifdef QET_HAS_QTPDF
|
||||
else if (value == "pdf")
|
||||
{
|
||||
DiagramEventAddPdf *deap = new DiagramEventAddPdf(d);
|
||||
if (deap->isNull())
|
||||
{
|
||||
delete deap;
|
||||
return;
|
||||
}
|
||||
else
|
||||
diagram_event = deap;
|
||||
}
|
||||
#endif
|
||||
else if (value == "text")
|
||||
{
|
||||
diagram_event = new DiagramEventAddText(d);
|
||||
@@ -2206,10 +2228,6 @@ void QETDiagramEditor::readSettings()
|
||||
QVariant geometry = settings.value("diagrameditor/geometry");
|
||||
if (geometry.isValid()) restoreGeometry(geometry.toByteArray());
|
||||
|
||||
// etat de la fenetre (barres d'outils, docks...)
|
||||
QVariant state = settings.value("diagrameditor/state");
|
||||
if (state.isValid()) restoreState(state.toByteArray());
|
||||
|
||||
// gestion des projets (onglets ou fenetres)
|
||||
bool tabbed = settings.value("diagrameditor/viewmode", "tabbed") == "tabbed";
|
||||
if (tabbed) {
|
||||
@@ -2219,6 +2237,24 @@ void QETDiagramEditor::readSettings()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETDiagramEditor::readSettingsState
|
||||
Restore the window state (docks, toolbars).
|
||||
Must be called AFTER show() in Qt6 for restoreState() to work correctly.
|
||||
*/
|
||||
void QETDiagramEditor::readSettingsState()
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
// etat de la fenetre (barres d'outils, docks...)
|
||||
QVariant state = settings.value("diagrameditor/state");
|
||||
if (state.isValid()) {
|
||||
if (!restoreState(state.toByteArray())) {
|
||||
settings.remove("diagrameditor/state");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETDiagramEditor::writeSettings
|
||||
Write the settings
|
||||
@@ -2799,11 +2835,11 @@ void QETDiagramEditor::generateTerminalBlock()
|
||||
* Opens the dialog for automatic terminal numbering and applies the generated undo command.
|
||||
*/
|
||||
void QETDiagramEditor::slot_terminalNumbering() {
|
||||
TerminalNumberingDialog dialog(this);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
QETProject *project = currentProject();
|
||||
if (!project) return;
|
||||
QETProject *project = currentProject();
|
||||
if (!project) return;
|
||||
|
||||
TerminalNumberingDialog dialog(this, project);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
// Fetch the generated undo command from the dialog logic
|
||||
QUndoCommand *macro = dialog.getUndoCommand(project);
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@ class QETDiagramEditor : public QETMainWindow
|
||||
void setWindowedMode();
|
||||
void setTabbedMode();
|
||||
void readSettings();
|
||||
void readSettingsState();
|
||||
void writeSettings();
|
||||
void activateProject(QETProject *);
|
||||
void activateProject(ProjectView *);
|
||||
|
||||
@@ -146,7 +146,7 @@ bool ProjectDBModel::setData(const QModelIndex &index, const QVariant &value, in
|
||||
return false;
|
||||
}
|
||||
m_index_0_0_data.insert(role, value);
|
||||
emit dataChanged(index, index, QVector<int>(role));
|
||||
emit dataChanged(index, index, {role});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ void ProjectDBModel::dataBaseUpdated()
|
||||
auto row = m_record.size();
|
||||
auto col = row ? m_record.first().count() : 1;
|
||||
|
||||
emit dataChanged(this->index(0,0), this->index(row-1, col-1), QVector<int>(Qt::DisplayRole));
|
||||
emit dataChanged(this->index(0,0), this->index(row-1, col-1), {Qt::DisplayRole});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -451,16 +451,8 @@ void GraphicsTablePropertiesEditor::setUpEditConnection()
|
||||
m_edit_connection << connect(ui->m_table_left_margin, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply);
|
||||
m_edit_connection << connect(ui->m_table_right_margin, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply);
|
||||
m_edit_connection << connect(ui->m_table_bottom_margin, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply);
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // ### Qt 6: remove
|
||||
m_edit_connection << connect(m_table_button_group, QOverload<int>::of(&QButtonGroup::buttonClicked), this, &GraphicsTablePropertiesEditor::apply);
|
||||
m_edit_connection << connect(m_header_button_group, QOverload<int>::of(&QButtonGroup::buttonClicked), this, &GraphicsTablePropertiesEditor::apply);
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 5.15 or later")
|
||||
#endif
|
||||
m_edit_connection << connect(m_table_button_group, QOverload<int>::of(&QButtonGroup::idClicked), this, &GraphicsTablePropertiesEditor::apply);
|
||||
m_edit_connection << connect(m_header_button_group, QOverload<int>::of(&QButtonGroup::idClicked), this, &GraphicsTablePropertiesEditor::apply);
|
||||
#endif
|
||||
m_edit_connection << connect(ui->m_display_n_row_sb, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::apply);
|
||||
m_edit_connection << connect(ui->m_display_n_row_sb, QOverload<int>::of(&QSpinBox::valueChanged), this, &GraphicsTablePropertiesEditor::updateInfoLabel);
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ Conductor::Conductor(Terminal *p1, Terminal* p2) :
|
||||
//set Zvalue at 11 to be upper than the DiagramImageItem and element
|
||||
setZValue(11);
|
||||
m_previous_z_value = zValue();
|
||||
m_uuid = QUuid::createUuid();
|
||||
|
||||
//Add this conductor to the list of conductors of each of the two terminals
|
||||
bool ajout_p1 = terminal1 -> addConductor(this);
|
||||
@@ -1005,6 +1006,18 @@ void Conductor::pointsToSegments(const QList<QPointF>& points_list) {
|
||||
*/
|
||||
bool Conductor::fromXml(QDomElement &dom_element)
|
||||
{
|
||||
//Older project files have no conductor uuid attribute at all --
|
||||
//generate one on load, same treatment terminal uuids got when
|
||||
//that field was introduced (see terminal1/terminal2 handling in
|
||||
//toXml() below).
|
||||
m_uuid = QUuid(dom_element.attribute(QStringLiteral("uuid")));
|
||||
if (m_uuid.isNull()) {
|
||||
//Absent, empty or malformed: mint one. A null uuid is not a usable
|
||||
//identity -- every conductor carrying one would collide with every
|
||||
//other on the conductor table's primary key.
|
||||
m_uuid = QUuid::createUuid();
|
||||
}
|
||||
|
||||
setPos(dom_element.attribute("x", nullptr).toDouble(),
|
||||
dom_element.attribute("y", nullptr).toDouble());
|
||||
|
||||
@@ -1043,6 +1056,7 @@ QDomElement Conductor::toXml(QDomDocument &dom_document,
|
||||
{
|
||||
QDomElement dom_element = dom_document.createElement("conductor");
|
||||
|
||||
dom_element.setAttribute("uuid", m_uuid.toString());
|
||||
dom_element.setAttribute("x", QString::number(pos().x()));
|
||||
dom_element.setAttribute("y", QString::number(pos().y()));
|
||||
|
||||
@@ -1725,14 +1739,8 @@ QSet<Conductor *> Conductor::relatedPotentialConductors(const bool all_diagram,
|
||||
for (Conductor *c : other_conductors_list_t) {
|
||||
other_conductors += c->relatedPotentialConductors(all_diagram, t_list);
|
||||
}
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
|
||||
other_conductors += other_conductors_list_t.toSet();
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 5.14 or later")
|
||||
#endif
|
||||
|
||||
other_conductors += QSet<Conductor*>(other_conductors_list_t.begin(),other_conductors_list_t.end());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "../conductorproperties.h"
|
||||
|
||||
#include <QGraphicsPathItem>
|
||||
#include <QUuid>
|
||||
|
||||
class ConductorProfile;
|
||||
class ConductorSegmentProfile;
|
||||
@@ -77,6 +78,8 @@ class Conductor : public QGraphicsObject
|
||||
int type() const override { return Type; }
|
||||
Diagram *diagram() const;
|
||||
ConductorTextItem *textItem() const;
|
||||
QUuid uuid() const {return m_uuid;}
|
||||
void newUuid() {m_uuid = QUuid::createUuid();} //create new uuid for this conductor
|
||||
void updatePath(const QRectF & = QRectF());
|
||||
|
||||
//This method do nothing, it's only made to be used with Q_PROPERTY
|
||||
@@ -205,6 +208,7 @@ class Conductor : public QGraphicsObject
|
||||
Highlight must_highlight_;
|
||||
bool m_valid;
|
||||
bool m_freeze_label = false;
|
||||
QUuid m_uuid;
|
||||
|
||||
/// QPen et QBrush objects used to draw conductors
|
||||
static QPen conductor_pen;
|
||||
|
||||
@@ -117,6 +117,10 @@ void CrossRefItem::setUpConnection()
|
||||
m_update_connection << connect(m_element, &Element::rotationChanged, this, &CrossRefItem::autoPos);
|
||||
set=true;
|
||||
}
|
||||
// For PLC masters, always set up connections for update notifications
|
||||
// (page reorder, diagram removal, etc.)
|
||||
if (!set && m_element->elementData().m_master_type == ElementData::PLC && !m_text && !m_group)
|
||||
set = true;
|
||||
|
||||
if(set)
|
||||
{
|
||||
@@ -234,9 +238,24 @@ void CrossRefItem::updateLabel()
|
||||
qp.setPen(pen_);
|
||||
qp.setFont(QETApp::diagramTextsFont(5));
|
||||
|
||||
// PLC table is drawn by Element::drawPlcTable(), not by CrossRefItem
|
||||
// PLC table is drawn and managed entirely by CrossRefItem
|
||||
if (m_element->elementData().m_master_type == ElementData::PLC)
|
||||
{
|
||||
// Position at the PLC table position from the .elmt definition
|
||||
QList<QPointF> positions = m_element->plcTablePositions();
|
||||
QPointF pos = positions.isEmpty() ? QPointF(0, 0) : positions.first();
|
||||
setPos(pos);
|
||||
|
||||
// Populate m_hovered_contacts_map using drawAsPlcTable on a
|
||||
// dummy painter (m_update_map=true).
|
||||
m_update_map = true;
|
||||
drawAsPlcTable(qp);
|
||||
m_update_map = false;
|
||||
|
||||
update();
|
||||
QTimer::singleShot(0, this, [this]{ update(); });
|
||||
return;
|
||||
}
|
||||
//Draw cross or contact, only if master element is linked.
|
||||
else if (! m_element->linkedElements().isEmpty())
|
||||
{
|
||||
@@ -263,6 +282,11 @@ void CrossRefItem::updateLabel()
|
||||
*/
|
||||
void CrossRefItem::autoPos()
|
||||
{
|
||||
// For PLC masters, position is set by updateLabel() based on
|
||||
// m_plc_table_positions - don't override it here.
|
||||
if (m_element->elementData().m_master_type == ElementData::PLC)
|
||||
return;
|
||||
|
||||
//We calculate the position according to the snapTo of the xrefproperties
|
||||
if (m_properties.snapTo() == XRefProperties::Bottom)
|
||||
QGIUtility::centerToBottomDiagram(this,
|
||||
@@ -328,7 +352,9 @@ void CrossRefItem::paint(
|
||||
// confirmed by analysis of 19+ coredumps.
|
||||
// m_update_map=false: draw functions do not overwrite m_hovered_contacts_map.
|
||||
|
||||
// PLC table is drawn by Element::drawPlcTable(), not by CrossRefItem
|
||||
// PLC: do not draw here (Element::drawPlcTable handles visual rendering).
|
||||
// The m_hovered_contacts_map was populated in updateLabel() for
|
||||
// click navigation and PDF hyperlink injection.
|
||||
if (m_element->elementData().m_master_type == ElementData::PLC)
|
||||
return;
|
||||
|
||||
@@ -1378,11 +1404,24 @@ void CrossRefItem::drawAsPlcTable(QPainter &painter)
|
||||
headers[COL_COMMENT] = QObject::tr("Commentaire");
|
||||
headers[COL_CROSSREF] = QObject::tr("Réf. croisée");
|
||||
|
||||
// Build list of visible columns
|
||||
// Build visible columns (must match Element::drawPlcTable logic)
|
||||
QList<int> visible_cols;
|
||||
for (int i = 0; i < COL_COUNT; ++i) {
|
||||
if (plc_data.colVisible.value(i, true))
|
||||
visible_cols.append(i);
|
||||
if (!plc_data.columnOrder.isEmpty()) {
|
||||
for (int logical : plc_data.columnOrder) {
|
||||
if (logical >= 0 && logical < COL_COUNT
|
||||
&& plc_data.colVisible.value(logical, true)
|
||||
&& !visible_cols.contains(logical))
|
||||
visible_cols.append(logical);
|
||||
}
|
||||
for (int i = 0; i < COL_COUNT; ++i) {
|
||||
if (plc_data.colVisible.value(i, true) && !visible_cols.contains(i))
|
||||
visible_cols.append(i);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < COL_COUNT; ++i) {
|
||||
if (plc_data.colVisible.value(i, true))
|
||||
visible_cols.append(i);
|
||||
}
|
||||
}
|
||||
if (visible_cols.isEmpty())
|
||||
return;
|
||||
@@ -1405,7 +1444,7 @@ void CrossRefItem::drawAsPlcTable(QPainter &painter)
|
||||
}
|
||||
|
||||
qreal row_h = plc_data.rowHeight > 0 ? plc_data.rowHeight : 8.0;
|
||||
qreal header_h = row_h + 2.0;
|
||||
qreal header_h = plc_data.showHeaders ? (row_h + 2.0) : 0;
|
||||
|
||||
// Calculate total width
|
||||
qreal total_width = 0;
|
||||
@@ -1431,6 +1470,7 @@ void CrossRefItem::drawAsPlcTable(QPainter &painter)
|
||||
|
||||
qreal total_height;
|
||||
int block_count = block_starts.size();
|
||||
qreal block_total_width = total_width; // width of one block, before multi-block scaling
|
||||
|
||||
if (block_count > 1) {
|
||||
int max_rows = 0;
|
||||
@@ -1455,7 +1495,7 @@ void CrossRefItem::drawAsPlcTable(QPainter &painter)
|
||||
// Draw header row
|
||||
|
||||
for (int block = 0; block < block_count; ++block) {
|
||||
qreal block_x = block * (col_widths.value(visible_cols.first(), 30) * visible_cols.size() + 3);
|
||||
qreal block_x = block * (block_total_width + 3);
|
||||
qreal cx = block_x;
|
||||
|
||||
// Draw column headers
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include <QGraphicsObject>
|
||||
#include <QMultiMap>
|
||||
#include <QStyleOptionGraphicsItem>
|
||||
|
||||
class Element;
|
||||
class DynamicElementTextItem;
|
||||
@@ -89,6 +90,19 @@ class CrossRefItem : public QGraphicsObject
|
||||
void updateLabel();
|
||||
void autoPos();
|
||||
|
||||
public:
|
||||
/// DXF export: replay this item's paint() on an arbitrary QPainter
|
||||
/// (e.g. one targeting DxfPaintDevice). paint() itself stays
|
||||
/// protected, as it should for the normal
|
||||
/// QGraphicsScene/QGraphicsView paint contract - this is a
|
||||
/// deliberate, narrow escape hatch for exporters, not a general
|
||||
/// relaxation of that contract.
|
||||
void paintForExport(QPainter *painter)
|
||||
{
|
||||
QStyleOptionGraphicsItem option;
|
||||
paint(painter, &option, nullptr);
|
||||
}
|
||||
|
||||
protected:
|
||||
bool sceneEvent(QEvent *event) override;
|
||||
void paint(QPainter *painter,
|
||||
|
||||
@@ -221,6 +221,21 @@ void DiagramTextItem::setHtml(const QString &text)
|
||||
{
|
||||
QGraphicsTextItem::setHtml(text);
|
||||
m_is_html = true;
|
||||
|
||||
m_non_left_alignment = false;
|
||||
QTextBlock block = document()->begin();
|
||||
while (block.isValid()) {
|
||||
Qt::Alignment a = block.blockFormat().alignment();
|
||||
if (a & Qt::AlignHCenter || a & Qt::AlignRight || a & Qt::AlignJustify) {
|
||||
m_non_left_alignment = true;
|
||||
break;
|
||||
}
|
||||
block = block.next();
|
||||
}
|
||||
|
||||
if (m_non_left_alignment) {
|
||||
document()->setTextWidth(document()->idealWidth() + 40.0);
|
||||
}
|
||||
}
|
||||
|
||||
void DiagramTextItem::setPlainText(const QString &text)
|
||||
@@ -245,6 +260,7 @@ bool DiagramTextItem::isHtml() const
|
||||
void DiagramTextItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
|
||||
{
|
||||
painter -> setRenderHint(QPainter::Antialiasing, false);
|
||||
|
||||
QGraphicsTextItem::paint(painter, option, widget);
|
||||
|
||||
if (m_mouse_hover && !isSelected())
|
||||
@@ -466,9 +482,34 @@ void DiagramTextItem::edit()
|
||||
parent = scene()->views().first();
|
||||
|
||||
qdesigner_internal::RichTextEditorDialog editor(parent);
|
||||
connect(&editor, &qdesigner_internal::RichTextEditorDialog::applyEditText, [this](QString text) {this->setHtml(text);});
|
||||
editor.setText(toHtml());
|
||||
|
||||
QVector<Qt::Alignment> alignments;
|
||||
connect(&editor, &qdesigner_internal::RichTextEditorDialog::applyEditText,
|
||||
this, [this, &editor, &alignments](const QString &text) {
|
||||
alignments = editor.blockAlignments();
|
||||
this->setHtml(text);
|
||||
});
|
||||
editor.exec();
|
||||
|
||||
if (!alignments.isEmpty()) {
|
||||
QTextCursor cursor(document());
|
||||
cursor.beginEditBlock();
|
||||
QTextBlock block = document()->begin();
|
||||
int i = 0;
|
||||
while (block.isValid() && i < alignments.size()) {
|
||||
Qt::Alignment align = alignments.at(i);
|
||||
if (align != Qt::AlignLeft && align != Qt::Alignment()) {
|
||||
QTextBlockFormat fmt;
|
||||
fmt.setAlignment(align);
|
||||
cursor.setPosition(block.position());
|
||||
cursor.setBlockFormat(fmt);
|
||||
}
|
||||
block = block.next();
|
||||
i++;
|
||||
}
|
||||
cursor.endEditBlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -79,8 +79,8 @@ class DiagramTextItem : public QGraphicsTextItem
|
||||
bool m_block_alignment = false;
|
||||
|
||||
QRectF frameRect() const;
|
||||
|
||||
void setHtml(const QString &text);
|
||||
|
||||
void setHtml(const QString &text);
|
||||
void setPlainText(const QString &text);
|
||||
bool isHtml() const;
|
||||
|
||||
@@ -110,7 +110,8 @@ class DiagramTextItem : public QGraphicsTextItem
|
||||
m_mouse_hover = false,
|
||||
m_first_move = true,
|
||||
m_no_editable,
|
||||
m_is_html = false;
|
||||
m_is_html = false,
|
||||
m_non_left_alignment = false;
|
||||
|
||||
QString
|
||||
m_previous_html_text,
|
||||
|
||||
@@ -252,6 +252,29 @@ ElementTextItemGroup *DynamicElementTextItem::parentGroup() const
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DynamicElementTextItem::masterElement
|
||||
@return the master element for slave elements, or the parent element for others.
|
||||
For PLC slaves, returns the actual master from linkedElements() instead of
|
||||
m_master_element (which points to self because elementUseForInfo() returns
|
||||
self for PLC slaves to resolve %{plc_*} variables).
|
||||
*/
|
||||
Element *DynamicElementTextItem::masterElement() const
|
||||
{
|
||||
Element *elmt = parentElement();
|
||||
if (!elmt)
|
||||
return m_master_element.data();
|
||||
|
||||
if (elmt->linkType() == Element::Slave &&
|
||||
elmt->elementData().m_slave_type == ElementData::PLCSlave &&
|
||||
!elmt->linkedElements().isEmpty())
|
||||
{
|
||||
return elmt->linkedElements().first();
|
||||
}
|
||||
|
||||
return m_master_element.data();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DynamicElementTextItem::elementUseForInfo
|
||||
@return a pointer to the element we must use for the variable information.
|
||||
|
||||
@@ -89,7 +89,10 @@ class DynamicElementTextItem : public DiagramTextItem
|
||||
Element *parentElement() const;
|
||||
/// PDF export: slave cross-reference text item ("(folio-pos)") and its master target.
|
||||
QGraphicsTextItem *slaveXrefItem() const { return m_slave_Xref_item; }
|
||||
Element *masterElement() const { return m_master_element.data(); }
|
||||
/// DXF export: the master-side cross-reference item (the table/cross
|
||||
/// drawn next to a report/master element), if this text item has one.
|
||||
CrossRefItem *masterXrefItem() const { return m_Xref_item; }
|
||||
Element *masterElement() const;
|
||||
ElementTextItemGroup *parentGroup() const;
|
||||
Element *elementUseForInfo() const;
|
||||
void refreshLabelConnection();
|
||||
|
||||
@@ -38,6 +38,15 @@
|
||||
#include "dynamicelementtextitem.h"
|
||||
#include "elementtextitemgroup.h"
|
||||
#include "iostream"
|
||||
|
||||
#include <QCollator>
|
||||
|
||||
static const QString plcTerminalKeys[] = {
|
||||
QETInformation::ELMT_PLC_T1,
|
||||
QETInformation::ELMT_PLC_T2,
|
||||
QETInformation::ELMT_PLC_T3,
|
||||
QETInformation::ELMT_PLC_T4
|
||||
};
|
||||
#include "../qetxml.h"
|
||||
#include "../qetversion.h"
|
||||
#include "qgraphicsitemutility.h"
|
||||
@@ -797,6 +806,7 @@ bool Element::fromXml(QDomElement &e,
|
||||
setZValue(e.attribute(QStringLiteral("z"), QString::number(this->zValue())).toDouble());
|
||||
setFlags(QGraphicsItem::ItemIsMovable
|
||||
| QGraphicsItem::ItemIsSelectable);
|
||||
is_movable_ = e.attribute(QStringLiteral("is_movable"), QStringLiteral("1")).toInt();
|
||||
|
||||
// orientation
|
||||
bool conv_ok;
|
||||
@@ -928,6 +938,7 @@ QDomElement Element::toXml(
|
||||
element.setAttribute(QStringLiteral("y"), QString::number(pos().y()));
|
||||
element.setAttribute(QStringLiteral("z"), QString::number(this->zValue()));
|
||||
element.setAttribute(QStringLiteral("orientation"), QString::number(orientation()));
|
||||
element.setAttribute(QStringLiteral("is_movable"), bool(is_movable_));
|
||||
|
||||
/* get the first id to use for the bounds of this element
|
||||
* recupere le premier id a utiliser pour les bornes de cet element */
|
||||
@@ -1511,13 +1522,7 @@ void Element::setElementData(ElementData data)
|
||||
{
|
||||
QString val = (t < io.terminals.size())
|
||||
? io.terminals.at(t) : QString();
|
||||
ctx.addValue(
|
||||
QStringList({
|
||||
QETInformation::ELMT_PLC_T1,
|
||||
QETInformation::ELMT_PLC_T2,
|
||||
QETInformation::ELMT_PLC_T3,
|
||||
QETInformation::ELMT_PLC_T4
|
||||
}).at(t), val);
|
||||
ctx.addValue(plcTerminalKeys[t], val);
|
||||
}
|
||||
slave->setElementInformations(ctx);
|
||||
|
||||
|
||||
@@ -266,6 +266,10 @@ class Element : public QetGraphicsItem
|
||||
|
||||
void drawPlcTable(QPainter *painter);
|
||||
|
||||
public:
|
||||
/// Positions where the PLC IO table is drawn (from the .elmt file).
|
||||
QList<QPointF> plcTablePositions() const { return m_plc_table_positions; }
|
||||
|
||||
private:
|
||||
bool m_must_highlight = false;
|
||||
QSize dimensions;
|
||||
|
||||
@@ -158,6 +158,14 @@ QVariant MasterElement::itemChange(QGraphicsItem::GraphicsItemChange change, con
|
||||
{
|
||||
m_first_scene_change = false;
|
||||
connect(diagram()->project(), &QETProject::XRefPropertiesChanged, this, &MasterElement::xrefPropertiesChanged);
|
||||
|
||||
// For PLC masters, create the CrossRefItem immediately so the
|
||||
// IO table is visible even without linked slaves.
|
||||
if (m_data.m_master_type == ElementData::PLC && !m_Xref_item)
|
||||
{
|
||||
m_Xref_item = new CrossRefItem(this);
|
||||
m_Xref_item->updateLabel();
|
||||
}
|
||||
}
|
||||
return Element::itemChange(change, value);
|
||||
}
|
||||
|
||||
@@ -820,20 +820,72 @@ QUuid Terminal::uuid() const
|
||||
return d->m_uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Terminal::stableUuid
|
||||
An identity for this terminal that exists on every element, not only on
|
||||
those saved by a uuid-aware element editor.
|
||||
|
||||
uuid() comes from the catalog .elmt definition and is empty for every
|
||||
element authored before that field existed -- which is most of the
|
||||
installed base. Anything keyed on uuid() alone therefore cannot see those
|
||||
elements at all.
|
||||
|
||||
When there is no uuid, derive one from the terminal's local position and
|
||||
orientation inside its element. That is not an arbitrary choice: it is the
|
||||
same thing the project format itself uses to match a conductor back to a
|
||||
terminal ("each connection is made by using the local position of the
|
||||
terminal and a dynamic id" -- TerminalData::m_uuid). m_pos is the position
|
||||
read from the definition and is not touched by moving the element on the
|
||||
folio, so the result is stable across loads, saves and folio moves, and it
|
||||
is unique within an element except where a definition genuinely declares
|
||||
two terminals at the same point -- three cases in the whole example corpus,
|
||||
and harmless, because two terminals sharing a position and orientation are
|
||||
indistinguishable in every observable respect: they merge to one terminal
|
||||
row and every conductor on either of them still resolves to the right
|
||||
element and name.
|
||||
|
||||
Derived values are UUID v5 in a fixed namespace, so they are reproducible
|
||||
without being written to the file, and cannot collide with the v4 uuids
|
||||
the element editor generates.
|
||||
|
||||
@return the terminal's own uuid when it has one, otherwise a derived one
|
||||
*/
|
||||
QUuid Terminal::stableUuid() const
|
||||
{
|
||||
if (!d->m_uuid.isNull()) {
|
||||
return d->m_uuid;
|
||||
}
|
||||
|
||||
//Fixed namespace for terminal identities derived from geometry.
|
||||
static const QUuid derived_ns(QStringLiteral("{6b1f6d1e-6a1a-5f7e-9a3d-9c0a5b2d7e11}"));
|
||||
|
||||
//Position and orientation only. The name is deliberately excluded: it
|
||||
//is not stable across a save cycle -- QET rewrites a terminal named
|
||||
//"_" as unnamed, which would silently change the identity of 1421 of
|
||||
//industrial.qet's 1790 terminals on the first resave. It is also not
|
||||
//needed: keying on geometry alone produces exactly the same number of
|
||||
//collisions across the example corpus, and it means renaming a
|
||||
//terminal does not change what it is.
|
||||
const QString key = QStringLiteral("%1|%2|%3")
|
||||
.arg(d->m_pos.x(), 0, 'f', 4)
|
||||
.arg(d->m_pos.y(), 0, 'f', 4)
|
||||
.arg(static_cast<int>(d->m_orientation));
|
||||
|
||||
return QUuid::createUuidV5(derived_ns, key);
|
||||
}
|
||||
|
||||
QString Terminal::name() const
|
||||
{
|
||||
if (d->m_use_master_label && parent_element_) {
|
||||
// Find the master element in the slave's linked elements
|
||||
for (Element *elmt : parent_element_->linkedElements()) {
|
||||
if (elmt->linkType() == Element::Master) {
|
||||
int group_idx = elmt->groupIndexForElement(parent_element_);
|
||||
if (group_idx >= 0) {
|
||||
// For PLC masters, use io.terminals as labels
|
||||
if (elmt->elementData().m_master_type == ElementData::PLC) {
|
||||
const auto &plc_data = elmt->elementData().plcMasterData();
|
||||
if (group_idx < plc_data.ios.size()) {
|
||||
int label_idx = d->m_master_label_index;
|
||||
const QStringList &labels = plc_data.ios.at(group_idx).terminals;
|
||||
const QStringList labels = plc_data.ios.at(group_idx).effectiveTerminals();
|
||||
if (label_idx >= 0 && label_idx < labels.size()) {
|
||||
return labels.at(label_idx);
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ class Terminal : public QGraphicsObject
|
||||
Diagram *diagram () const;
|
||||
Element *parentElement () const;
|
||||
QUuid uuid () const;
|
||||
QUuid stableUuid () const;
|
||||
QString name () const;
|
||||
QString baseName () const;
|
||||
TerminalData::Type terminalType() const;
|
||||
|
||||
@@ -183,6 +183,7 @@ namespace QET {
|
||||
QIcon ZoomOriginal;
|
||||
QIcon ZoomOut;
|
||||
QIcon adding_image;
|
||||
QIcon adding_pdf;
|
||||
QIcon resize_image;
|
||||
QIcon sa;
|
||||
QIcon br;
|
||||
@@ -453,6 +454,7 @@ void QET::Icons::initIcons()
|
||||
EditClear .addFile(":/ico/22x22/edit-clear.png");
|
||||
EditText .addFile(":/ico/22x22/names.png");
|
||||
adding_image .addFile(":/ico/22x22/insert-image.png");
|
||||
adding_pdf .addFile(":/ico/22x22/pdf-import.png");
|
||||
|
||||
if (rtl) {
|
||||
EditClearLocationBar.addPixmap(QPixmap(":/ico/16x16/edit-clear-locationbar-ltr.png").transformed(reverse));
|
||||
|
||||
@@ -191,6 +191,7 @@ namespace QET {
|
||||
extern QIcon ZoomOriginal;
|
||||
extern QIcon ZoomOut;
|
||||
extern QIcon adding_image;
|
||||
extern QIcon adding_pdf;
|
||||
extern QIcon resize_image;
|
||||
extern QIcon sa;
|
||||
extern QIcon br;
|
||||
|
||||
@@ -155,6 +155,8 @@ void QETMainWindow::initCommonMenus()
|
||||
connect(settings_menu_, &QMenu::aboutToShow, this, &QETMainWindow::checkToolbarsmenu);
|
||||
|
||||
help_menu_ = new QMenu(tr("&Aide", "window menu"), this);
|
||||
help_menu_ -> addAction(diagnostics_action_);
|
||||
help_menu_ -> addSeparator();
|
||||
help_menu_ -> addAction(whatsthis_action_);
|
||||
help_menu_ -> addSeparator();
|
||||
help_menu_ -> addAction(manual_online_);
|
||||
@@ -164,8 +166,6 @@ 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);
|
||||
|
||||
@@ -174,6 +174,30 @@ QETProject::~QETProject()
|
||||
delete diagram;
|
||||
m_diagrams_list.removeOne(diagram);
|
||||
}
|
||||
|
||||
//A diagram can be detached from this project (detachDiagram(), used by
|
||||
//both removeDiagram() and RemoveDiagramCommand::redo()) and scheduled
|
||||
//for deferred deletion via deleteLater(), without that deletion having
|
||||
//actually run yet -- deleteLater() only fires on the next event-loop
|
||||
//iteration, and nothing guarantees one runs before this destructor
|
||||
//does. Such a diagram is no longer in m_diagrams_list (so the loop
|
||||
//above never touches it) but is still a QObject child of this project
|
||||
//(Diagram's constructor passes `project` straight to QGraphicsScene's
|
||||
//parent argument). Left alone, it is destroyed later by QObject's own
|
||||
//automatic child cleanup in ~QObject(), which runs AFTER m_data_base
|
||||
//(a plain value member, destroyed by ordinary C++ member teardown)
|
||||
//has already been destroyed -- and Diagram's destructor calls back
|
||||
//into dataBase()->removeElement() for each of its elements, so that
|
||||
//ordering is a use-after-free (confirmed by crash: SIGSEGV in
|
||||
//QSqlResult::exec(), called from Diagram::~Diagram() by way of
|
||||
//Diagram::removeItem(), by way of QObjectPrivate::deleteChildren()).
|
||||
//Delete any such stragglers now, synchronously, while m_data_base is
|
||||
//still alive. The deleteLater() event, if it is ever processed
|
||||
//afterward, is a safe no-op on an already-deleted QObject.
|
||||
const auto orphaned_diagrams = findChildren<Diagram *>(QString(), Qt::FindDirectChildrenOnly);
|
||||
for (Diagram *diagram : orphaned_diagrams) {
|
||||
delete diagram;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
#include "titleblock/templatescollection.h"
|
||||
#include "titleblockproperties.h"
|
||||
#include "diagram.h"
|
||||
#ifdef BUILD_WITHOUT_KF5
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
# include "ui/nokde/kautosavefile.h"
|
||||
#else
|
||||
# include <KAutoSaveFile>
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace QetVersion
|
||||
|
||||
QVersionNumber currentVersion()
|
||||
{
|
||||
return QVersionNumber{ 0, 100, 1 };
|
||||
return QVersionNumber{ 0, 200, 1 };
|
||||
}
|
||||
|
||||
QString displayedVersion()
|
||||
|
||||
@@ -78,11 +78,7 @@
|
||||
#include <QDialogButtonBox>
|
||||
#include <QActionGroup>
|
||||
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
|
||||
#include <QStringView>
|
||||
#else
|
||||
#define QStringView QStringRef
|
||||
#endif
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
@@ -105,8 +101,6 @@ namespace qdesigner_internal {
|
||||
QXmlStreamAttributes *atts,
|
||||
bool *paragraphAlignmentFound)
|
||||
{
|
||||
typedef QXmlStreamAttributes::iterator AttributeIt;
|
||||
|
||||
if (atts->isEmpty())
|
||||
return;
|
||||
|
||||
@@ -116,16 +110,31 @@ namespace qdesigner_internal {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean out everything except 'align' for 'p'
|
||||
// Clean out everything except text alignment for 'p'
|
||||
// Qt6 encodes alignment as style="text-align:center" instead of align="center"
|
||||
if (name == QLatin1String("p")) {
|
||||
for (AttributeIt it = atts->begin(); it != atts->end(); ) {
|
||||
if (it->name() == QLatin1String("align")) {
|
||||
++it;
|
||||
*paragraphAlignmentFound = true;
|
||||
} else {
|
||||
it = atts->erase(it);
|
||||
QString alignment;
|
||||
for (const auto &att : *atts) {
|
||||
if (att.name() == QLatin1String("align")) {
|
||||
alignment = att.value().toString();
|
||||
} else if (att.name() == QLatin1String("style")) {
|
||||
const QString style = att.value().toString();
|
||||
if (style.contains(QLatin1String("text-align:center")))
|
||||
alignment = QLatin1String("center");
|
||||
else if (style.contains(QLatin1String("text-align:right")))
|
||||
alignment = QLatin1String("right");
|
||||
else if (style.contains(QLatin1String("text-align:justify")))
|
||||
alignment = QLatin1String("justify");
|
||||
else if (style.contains(QLatin1String("text-align:left")))
|
||||
alignment = QLatin1String("left");
|
||||
}
|
||||
}
|
||||
atts->clear();
|
||||
if (!alignment.isEmpty()) {
|
||||
atts->append(QLatin1String("style"),
|
||||
QLatin1String("text-align:") + alignment + QLatin1Char(';'));
|
||||
*paragraphAlignmentFound = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -321,8 +330,8 @@ void HtmlTextEdit::contextMenuEvent(QContextMenuEvent *event)
|
||||
}
|
||||
|
||||
menu->addMenu(htmlMenu);
|
||||
connect(htmlMenu, SIGNAL(triggered(QAction*)),
|
||||
SLOT(actionTriggered(QAction*)));
|
||||
connect(htmlMenu, &QMenu::triggered,
|
||||
this, &HtmlTextEdit::actionTriggered);
|
||||
menu->exec(event->globalPos());
|
||||
delete menu;
|
||||
}
|
||||
@@ -357,7 +366,7 @@ ColorAction::ColorAction(QObject *parent):
|
||||
{
|
||||
setText(tr("Text Color"));
|
||||
setColor(Qt::black);
|
||||
connect(this, SIGNAL(triggered()), this, SLOT(chooseColor()));
|
||||
connect(this, &ColorAction::triggered, this, &ColorAction::chooseColor);
|
||||
}
|
||||
|
||||
void ColorAction::setColor(const QColor &color)
|
||||
@@ -422,8 +431,41 @@ class RichTextEditorToolBar : public QToolBar
|
||||
QPointer<RichTextEditor> m_editor;
|
||||
};
|
||||
|
||||
static QIcon createAlignmentIcon(Qt::Alignment alignment)
|
||||
{
|
||||
QPixmap pix(24, 24);
|
||||
pix.fill(Qt::transparent);
|
||||
QPainter painter(&pix);
|
||||
painter.setRenderHint(QPainter::Antialiasing, false);
|
||||
|
||||
const int lineCount = 4;
|
||||
const int lineSpacing = 5;
|
||||
const int startY = (24 - (lineCount * lineSpacing)) / 2 + 2;
|
||||
const int maxWidth = 18;
|
||||
const int minWidth = 6;
|
||||
|
||||
QPen pen(Qt::black, 2, Qt::SolidLine, Qt::FlatCap);
|
||||
painter.setPen(pen);
|
||||
|
||||
for (int i = 0; i < lineCount; ++i) {
|
||||
int y = startY + i * lineSpacing;
|
||||
int lineWidth = (i % 2 == 0) ? maxWidth : minWidth;
|
||||
int x;
|
||||
|
||||
if (alignment & Qt::AlignLeft)
|
||||
x = 3;
|
||||
else if (alignment & Qt::AlignRight)
|
||||
x = 24 - 3 - lineWidth;
|
||||
else
|
||||
x = (24 - lineWidth) / 2;
|
||||
|
||||
painter.drawLine(x, y, x + lineWidth, y);
|
||||
}
|
||||
|
||||
return QIcon(pix);
|
||||
}
|
||||
|
||||
static QAction *createCheckableAction(const QIcon &icon, const QString &text,
|
||||
QObject *receiver, const char *slot,
|
||||
QObject *parent = nullptr)
|
||||
{
|
||||
QAction *result = new QAction(parent);
|
||||
@@ -431,8 +473,20 @@ static QAction *createCheckableAction(const QIcon &icon, const QString &text,
|
||||
result->setText(text);
|
||||
result->setCheckable(true);
|
||||
result->setChecked(false);
|
||||
if (slot)
|
||||
QObject::connect(result, SIGNAL(triggered(bool)), receiver, slot);
|
||||
return result;
|
||||
}
|
||||
|
||||
template<typename Receiver, typename Slot>
|
||||
static QAction *createCheckableAction(const QIcon &icon, const QString &text,
|
||||
Receiver *receiver, Slot slot,
|
||||
QObject *parent = nullptr)
|
||||
{
|
||||
QAction *result = new QAction(parent);
|
||||
result->setIcon(icon);
|
||||
result->setText(text);
|
||||
result->setCheckable(true);
|
||||
result->setChecked(false);
|
||||
QObject::connect(result, &QAction::triggered, receiver, slot);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -452,8 +506,8 @@ RichTextEditorToolBar::RichTextEditorToolBar(RichTextEditor *editor,
|
||||
foreach (int font_size, font_sizes)
|
||||
m_font_size_input->addItem(QString::number(font_size));
|
||||
|
||||
connect(m_font_size_input, SIGNAL(activated(QString)),
|
||||
this, SLOT(sizeInputActivated(QString)));
|
||||
connect(m_font_size_input, &QComboBox::textActivated,
|
||||
this, &RichTextEditorToolBar::sizeInputActivated);
|
||||
addWidget(m_font_size_input);
|
||||
|
||||
|
||||
@@ -461,19 +515,19 @@ RichTextEditorToolBar::RichTextEditorToolBar(RichTextEditor *editor,
|
||||
|
||||
m_bold_action = createCheckableAction(
|
||||
QIcon(":/ico/32x32/format-text-bold.png"),
|
||||
tr("Texte en gras"), editor, SLOT(setFontBold(bool)), this);
|
||||
tr("Texte en gras"), editor, &RichTextEditor::setFontBold, this);
|
||||
ShortcutManager::instance().registerAction(m_bold_action, "richtext.bold", tr("Éditeur de texte"), Qt::CTRL | Qt::Key_B);
|
||||
addAction(m_bold_action);
|
||||
|
||||
m_italic_action = createCheckableAction(
|
||||
QIcon(":/ico/32x32/format-text-italic.png"),
|
||||
tr("Texte en italique"), editor, SLOT(setFontItalic(bool)), this);
|
||||
tr("Texte en italique"), editor, &RichTextEditor::setFontItalic, this);
|
||||
ShortcutManager::instance().registerAction(m_italic_action, "richtext.italic", tr("Éditeur de texte"), Qt::CTRL | Qt::Key_I);
|
||||
addAction(m_italic_action);
|
||||
|
||||
m_underline_action = createCheckableAction(
|
||||
QIcon(":/ico/32x32/format-text-underline.png"),
|
||||
tr("Texte souligé"), editor, SLOT(setFontUnderline(bool)), this);
|
||||
tr("Texte souligé"), editor, &RichTextEditor::setFontUnderline, this);
|
||||
ShortcutManager::instance().registerAction(m_underline_action, "richtext.underline", tr("Éditeur de texte"), Qt::CTRL | Qt::Key_U);
|
||||
addAction(m_underline_action);
|
||||
|
||||
@@ -481,46 +535,47 @@ RichTextEditorToolBar::RichTextEditorToolBar(RichTextEditor *editor,
|
||||
// Left, center, right and justified alignment buttons
|
||||
|
||||
QActionGroup *alignment_group = new QActionGroup(this);
|
||||
connect(alignment_group, SIGNAL(triggered(QAction*)),
|
||||
SLOT(alignmentActionTriggered(QAction*)));
|
||||
connect(alignment_group, &QActionGroup::triggered,
|
||||
this, &RichTextEditorToolBar::alignmentActionTriggered);
|
||||
|
||||
m_align_left_action = createCheckableAction(
|
||||
QIcon(),
|
||||
tr("Left Align"), editor, nullptr, alignment_group);
|
||||
createAlignmentIcon(Qt::AlignLeft),
|
||||
tr("Left Align"), alignment_group);
|
||||
addAction(m_align_left_action);
|
||||
|
||||
m_align_center_action = createCheckableAction(
|
||||
QIcon(),
|
||||
tr("Center"), editor, nullptr, alignment_group);
|
||||
createAlignmentIcon(Qt::AlignHCenter),
|
||||
tr("Center"), alignment_group);
|
||||
addAction(m_align_center_action);
|
||||
|
||||
m_align_right_action = createCheckableAction(
|
||||
QIcon(),
|
||||
tr("Right Align"), editor, nullptr, alignment_group);
|
||||
createAlignmentIcon(Qt::AlignRight),
|
||||
tr("Right Align"), alignment_group);
|
||||
addAction(m_align_right_action);
|
||||
|
||||
m_align_justify_action = createCheckableAction(
|
||||
QIcon(),
|
||||
tr("Justify"), editor, nullptr, alignment_group);
|
||||
tr("Justify"), alignment_group);
|
||||
addAction(m_align_justify_action);
|
||||
|
||||
m_align_left_action -> setVisible( true );
|
||||
m_align_center_action -> setVisible( true );
|
||||
m_align_right_action -> setVisible( true );
|
||||
m_align_justify_action -> setVisible( false );
|
||||
m_align_center_action -> setVisible( false );
|
||||
m_align_left_action -> setVisible( false );
|
||||
m_align_right_action -> setVisible( false );
|
||||
m_align_left_action -> setChecked( true );
|
||||
|
||||
// Superscript and subscript buttons
|
||||
|
||||
m_valign_sup_action = createCheckableAction(
|
||||
QIcon(":/ico/22x22/format-text-superscript.png"),
|
||||
tr("Superscript"),
|
||||
this, SLOT(setVAlignSuper(bool)), this);
|
||||
this, &RichTextEditorToolBar::setVAlignSuper, this);
|
||||
addAction(m_valign_sup_action);
|
||||
|
||||
m_valign_sub_action = createCheckableAction(
|
||||
QIcon(":/ico/22x22/format-text-subscript.png"),
|
||||
tr("Subscript"),
|
||||
this, SLOT(setVAlignSub(bool)), this);
|
||||
this, &RichTextEditorToolBar::setVAlignSub, this);
|
||||
addAction(m_valign_sub_action);
|
||||
|
||||
m_valign_sup_action -> setVisible( true );
|
||||
@@ -529,11 +584,11 @@ RichTextEditorToolBar::RichTextEditorToolBar(RichTextEditor *editor,
|
||||
// Insert hyperlink and image buttons
|
||||
|
||||
m_link_action->setText(tr("Insérer un lien"));
|
||||
connect(m_link_action, SIGNAL(triggered()), SLOT(insertLink()));
|
||||
connect(m_link_action, &QAction::triggered, this, &RichTextEditorToolBar::insertLink);
|
||||
addAction(m_link_action);
|
||||
|
||||
m_image_action->setText(tr("Insert &Image"));
|
||||
connect(m_image_action, SIGNAL(triggered()), SLOT(insertImage()));
|
||||
connect(m_image_action, &QAction::triggered, this, &RichTextEditorToolBar::insertImage);
|
||||
addAction(m_image_action);
|
||||
|
||||
m_image_action -> setVisible( false );
|
||||
@@ -547,13 +602,13 @@ RichTextEditorToolBar::RichTextEditorToolBar(RichTextEditor *editor,
|
||||
// Simplify rich text
|
||||
m_simplify_richtext_action = createCheckableAction(
|
||||
QIcon(":/ico/32x32/simplifyrichtext.png"),
|
||||
tr("Simplify Rich Text"), editor, SLOT(setSimplifyRichText(bool)),this);
|
||||
tr("Simplify Rich Text"), editor, &RichTextEditor::setSimplifyRichText, this);
|
||||
m_simplify_richtext_action->setChecked(editor->simplifyRichText());
|
||||
connect(m_editor, &RichTextEditor::simplifyRichTextChanged, m_simplify_richtext_action, &QAction::setChecked);
|
||||
addAction(m_simplify_richtext_action);
|
||||
|
||||
connect(editor, SIGNAL(textChanged()), this, SLOT(updateActions()));
|
||||
connect(editor, SIGNAL(stateChanged()), this, SLOT(updateActions()));
|
||||
connect(editor, &RichTextEditor::textChanged, this, &RichTextEditorToolBar::updateActions);
|
||||
connect(editor, &RichTextEditor::stateChanged, this, &RichTextEditorToolBar::updateActions);
|
||||
|
||||
updateActions();
|
||||
}
|
||||
@@ -677,10 +732,10 @@ void RichTextEditorToolBar::updateActions()
|
||||
RichTextEditor::RichTextEditor(QWidget *parent)
|
||||
: QTextEdit(parent)
|
||||
{
|
||||
connect(this, SIGNAL(currentCharFormatChanged(QTextCharFormat)),
|
||||
this, SIGNAL(stateChanged()));
|
||||
connect(this, SIGNAL(cursorPositionChanged()),
|
||||
this, SIGNAL(stateChanged()));
|
||||
connect(this, &RichTextEditor::currentCharFormatChanged,
|
||||
this, &RichTextEditor::stateChanged);
|
||||
connect(this, &RichTextEditor::cursorPositionChanged,
|
||||
this, &RichTextEditor::stateChanged);
|
||||
}
|
||||
|
||||
QToolBar *RichTextEditor::createToolBar(QWidget *parent)
|
||||
@@ -744,14 +799,8 @@ QString RichTextEditor::text(Qt::TextFormat format) const
|
||||
return m_simplifyRichText ? simplifyRichTextFilter(toHtml()) : toHtml();
|
||||
case Qt::AutoText:
|
||||
break;
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 5.14 or later")
|
||||
#endif
|
||||
case Qt::MarkdownText: //This enum value was added in Qt 5.14.
|
||||
case Qt::MarkdownText:
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
qInfo("(RichTextEditor::text) no valid switch: %d",format);
|
||||
break;
|
||||
@@ -776,9 +825,9 @@ RichTextEditorDialog::RichTextEditorDialog(QWidget *parent) :
|
||||
|
||||
m_text_edit->setAcceptRichText(false);
|
||||
|
||||
connect(m_editor, SIGNAL(textChanged()), this, SLOT(richTextChanged()));
|
||||
connect(m_editor, &RichTextEditor::textChanged, this, &RichTextEditorDialog::richTextChanged);
|
||||
connect(m_editor, &RichTextEditor::simplifyRichTextChanged, this, &RichTextEditorDialog::richTextChanged);
|
||||
connect(m_text_edit, SIGNAL(textChanged()), this, SLOT(sourceChanged()));
|
||||
connect(m_text_edit, &QTextEdit::textChanged, this, &RichTextEditorDialog::sourceChanged);
|
||||
|
||||
// The toolbar needs to be created after the RichTextEditor
|
||||
QToolBar *tool_bar = m_editor->createToolBar();
|
||||
@@ -812,6 +861,7 @@ RichTextEditorDialog::RichTextEditorDialog(QWidget *parent) :
|
||||
|
||||
m_editor->setFocus();
|
||||
|
||||
resize(600, 400);
|
||||
}
|
||||
|
||||
RichTextEditorDialog::~RichTextEditorDialog()
|
||||
@@ -893,6 +943,21 @@ void RichTextEditorDialog::tabIndexChanged(int newIndex)
|
||||
m_state = oldState; // Changed is triggered by setting the text
|
||||
}
|
||||
|
||||
/**
|
||||
@brief RichTextEditorDialog::blockAlignments
|
||||
Returns the text alignment of each block in the editor
|
||||
*/
|
||||
QVector<Qt::Alignment> RichTextEditorDialog::blockAlignments() const
|
||||
{
|
||||
QVector<Qt::Alignment> result;
|
||||
QTextBlock block = m_editor->document()->begin();
|
||||
while (block.isValid()) {
|
||||
result.append(block.blockFormat().alignment());
|
||||
block = block.next();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void RichTextEditorDialog::richTextChanged()
|
||||
{
|
||||
m_state = RichTextChanged;
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
|
||||
#include <QDialog>
|
||||
#include <QTextEdit>
|
||||
#include <QTextBlock>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
@@ -78,6 +79,7 @@ class RichTextEditorDialog : public QDialog
|
||||
void setDefaultFont(const QFont &font);
|
||||
void setText(const QString &text);
|
||||
QString text(Qt::TextFormat format = Qt::AutoText) const;
|
||||
QVector<Qt::Alignment> blockAlignments() const;
|
||||
|
||||
signals:
|
||||
void applyEditText(const QString &);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <QSettings>
|
||||
#include <QVariant>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
const QString SETTINGS_GROUP = QStringLiteral("shortcuts/");
|
||||
@@ -143,7 +144,7 @@ void ShortcutManager::setSequence(const QString &id, const QKeySequence &sequenc
|
||||
settings.setValue(key, sequence.toString());
|
||||
}
|
||||
|
||||
for (const QPointer<QObject> &target : qAsConst(it->targets)) {
|
||||
for (const QPointer<QObject> &target : std::as_const(it->targets)) {
|
||||
if (target) {
|
||||
target->setProperty("shortcut", QVariant::fromValue(sequence));
|
||||
}
|
||||
|
||||
@@ -358,6 +358,7 @@ void QETTitleBlockTemplateEditor::newTemplate()
|
||||
QETTitleBlockTemplateEditor *qet_template_editor = new QETTitleBlockTemplateEditor();
|
||||
qet_template_editor -> edit(TitleBlockTemplateLocation());
|
||||
qet_template_editor -> show();
|
||||
qet_template_editor -> readSettingsState(); // must run after show() in Qt6
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -645,10 +646,25 @@ void QETTitleBlockTemplateEditor::readSettings()
|
||||
// window size and position
|
||||
QVariant geometry = settings.value("titleblocktemplateeditor/geometry");
|
||||
if (geometry.isValid()) restoreGeometry(geometry.toByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETTitleBlockTemplateEditor::readSettingsState
|
||||
Restore the window state (docks, toolbars).
|
||||
Must be called AFTER show() in Qt6 for restoreState() to work correctly
|
||||
-- callers are responsible for calling this after show(), since this
|
||||
window isn't shown by its own constructor.
|
||||
*/
|
||||
void QETTitleBlockTemplateEditor::readSettingsState()
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
// window state (toolbars, docks...)
|
||||
QVariant state = settings.value("titleblocktemplateeditor/state");
|
||||
if (state.isValid()) restoreState(state.toByteArray());
|
||||
if (state.isValid()) {
|
||||
if (!restoreState(state.toByteArray())) {
|
||||
settings.remove("titleblocktemplateeditor/state");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -776,7 +792,20 @@ bool QETTitleBlockTemplateEditor::saveAs(const TitleBlockTemplateLocation &locat
|
||||
elmt.setAttribute("name", location.name());
|
||||
doc.appendChild(elmt);
|
||||
|
||||
collection -> setTemplateXmlDescription(location.name(), elmt);
|
||||
if (!collection -> setTemplateXmlDescription(location.name(), elmt)) {
|
||||
// Report the failure instead of marking the template "saved"
|
||||
// regardless (bugtracker #251) -- this return value used to
|
||||
// be discarded here.
|
||||
QET::QetMessageBox::critical(
|
||||
this,
|
||||
tr("Erreur", "message box title"),
|
||||
tr(
|
||||
"Impossible d'enregistrer le modèle « %1 ».",
|
||||
"message box content - %1 is a title block template name"
|
||||
).arg(location.name())
|
||||
);
|
||||
return(false);
|
||||
}
|
||||
|
||||
opened_from_file_ = false;
|
||||
location_ = location;
|
||||
@@ -880,6 +909,21 @@ bool QETTitleBlockTemplateEditor::saveAs()
|
||||
if (location.isValid()) {
|
||||
return(saveAs(location));
|
||||
}
|
||||
if (!location.name().isEmpty()) {
|
||||
// The name was rejected (e.g. contains a path separator or
|
||||
// other filesystem-reserved character) rather than the user
|
||||
// cancelling the dialog -- say so instead of silently doing
|
||||
// nothing (bugtracker #251).
|
||||
QET::QetMessageBox::critical(
|
||||
this,
|
||||
tr("Erreur", "message box title"),
|
||||
tr(
|
||||
"Le nom « %1 » n'est pas valide : il ne doit pas contenir "
|
||||
"les caractères suivants : \\ / : * ? \" < > |",
|
||||
"message box content - %1 is the rejected template name"
|
||||
).arg(location.name())
|
||||
);
|
||||
}
|
||||
return(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ class QETTitleBlockTemplateEditor : public QETMainWindow {
|
||||
|
||||
public slots:
|
||||
void readSettings();
|
||||
void readSettingsState();
|
||||
void writeSettings();
|
||||
void selectedCellsChanged(const QList<TitleBlockCell *>&);
|
||||
void duplicateCurrentLocation();
|
||||
|
||||
@@ -92,7 +92,13 @@ void TitleBlockTemplateLocation::setName(const QString &name) {
|
||||
*/
|
||||
bool TitleBlockTemplateLocation::isValid() const
|
||||
{
|
||||
return(!name_.isEmpty());
|
||||
// The name becomes (part of) a filename on disk (see
|
||||
// TitleBlockTemplatesFilesCollection::toFileName()). A name containing
|
||||
// a path separator or another filesystem-reserved character silently
|
||||
// fails to save instead of erroring, because it turns into an
|
||||
// unintended subpath rather than a plain filename (bugtracker #251).
|
||||
static const QRegularExpression invalid_chars_re(QStringLiteral("[\\\\/:*?\"<>|]"));
|
||||
return(!name_.isEmpty() && !name_.contains(invalid_chars_re));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -216,12 +216,7 @@ QString TitleBlockTemplateLogoManager::confirmLogoName(const QString &initial_na
|
||||
connect(replace_button, &QPushButton::clicked, signal_mapper, qOverload<>(&QSignalMapper::map));
|
||||
connect(rename_button, &QPushButton::clicked, signal_mapper, qOverload<>(&QSignalMapper::map));
|
||||
connect(cancel_button, &QPushButton::clicked, signal_mapper, qOverload<>(&QSignalMapper::map));
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // TODO Qt6 only: remove, mappedInt() always available
|
||||
connect(signal_mapper, SIGNAL(mapped(int)), rename_dialog, SLOT(done(int)));
|
||||
|
||||
#else
|
||||
connect(signal_mapper, &QSignalMapper::mappedInt, rename_dialog, &QDialog::done);
|
||||
#endif
|
||||
}
|
||||
rd_label -> setText(
|
||||
QString(tr(
|
||||
|
||||
@@ -396,16 +396,8 @@ void TitleBlockTemplate::parseRows(const QString &rows_string) {
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
bool conv_ok;
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
|
||||
QStringList rows_descriptions =
|
||||
rows_string.split(QChar(';'), QString::SkipEmptyParts);
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 5.14 or later")
|
||||
#endif
|
||||
QStringList rows_descriptions =
|
||||
rows_string.split(QChar(';'), Qt::SkipEmptyParts);
|
||||
#endif
|
||||
foreach (QString rows_description, rows_descriptions) {
|
||||
QRegularExpressionMatch match;
|
||||
match = row_size_format.match(rows_description);
|
||||
@@ -438,16 +430,8 @@ void TitleBlockTemplate::parseColumns(const QString &cols_string) {
|
||||
bool conv_ok;
|
||||
qDebug() <<"is QRegularExpression ok?";
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
|
||||
QStringList cols_descriptions =
|
||||
cols_string.split(QChar(';'), QString::SkipEmptyParts);
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 5.14 or later")
|
||||
#endif
|
||||
QStringList cols_descriptions =
|
||||
cols_string.split(QChar(';'), Qt::SkipEmptyParts);
|
||||
#endif
|
||||
foreach (QString cols_description, cols_descriptions) {
|
||||
QRegularExpressionMatch match_abc,match_rel;
|
||||
match_abc = abs_col_size_format.match(cols_description);
|
||||
|
||||
@@ -83,14 +83,7 @@ int BOMExportDialog::exec()
|
||||
if (file.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
{
|
||||
QTextStream stream(&file);
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // ### Qt 6: remove
|
||||
stream << getBom() << endl;
|
||||
#else
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO remove code for QT 5.15 or later")
|
||||
#endif
|
||||
stream << getBom() << &Qt::endl(stream);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,6 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
|
||||
QSettings settings;
|
||||
|
||||
//Appearance tab
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ###Qt 6:remove
|
||||
ui->m_hdpi_round_policy_widget->setDisabled(true);
|
||||
#else
|
||||
ui->m_hdpi_round_policy_cb->addItem(tr("Arrondi supérieur pour 0.5 et plus"), QLatin1String("Round"));
|
||||
ui->m_hdpi_round_policy_cb->addItem(tr("Toujours arrondi supérieur"), QLatin1String("Ceil"));
|
||||
ui->m_hdpi_round_policy_cb->addItem(tr("Toujours arrondi inférieur"), QLatin1String("Floor"));
|
||||
@@ -65,7 +62,7 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
|
||||
ui->m_hdpi_round_policy_cb->setCurrentIndex(4);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
ui->grid_startup_cb->setChecked(settings.value("diagrameditor/grid_display_startup", true).toBool());
|
||||
ui->guides_startup_cb->setChecked(settings.value("diagrameditor/guides_display_startup", false).toBool());
|
||||
ui->DiagramEditor_xGrid_sb->setValue(settings.value("diagrameditor/Xgrid", 10).toInt());
|
||||
@@ -215,10 +212,8 @@ void GeneralConfigurationPage::applyConf()
|
||||
settings.setValue("lang", ui->m_lang_cb->itemData(ui->m_lang_cb->currentIndex()).toString());
|
||||
|
||||
//hdpi
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
QetSettings::setHdpiScaleFactorRoundingPolicy(ui->m_hdpi_round_policy_cb->currentData().toString());
|
||||
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFactorRoundingPolicy());
|
||||
#endif
|
||||
|
||||
//ELEMENT EDITOR
|
||||
settings.setValue("elementeditor/default-informations", ui->m_default_elements_info->toPlainText());
|
||||
|
||||
@@ -396,29 +396,17 @@ void ProjectAutoNumConfigPage::buildConnections()
|
||||
//Conductor Tab
|
||||
connect(m_saw_conductor, &SelectAutonumW::applyPressed, this, &ProjectAutoNumConfigPage::saveContextConductor);
|
||||
connect(m_saw_conductor, &SelectAutonumW::removeClicked, this, &ProjectAutoNumConfigPage::removeContextConductor);
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // TODO Qt6 only: remove, textActivated() always available
|
||||
connect(m_saw_conductor->contextComboBox(), SIGNAL(activated(QString)), this, SLOT(updateContextConductor(QString)));
|
||||
#else
|
||||
connect(m_saw_conductor->contextComboBox(), &QComboBox::textActivated, this, &ProjectAutoNumConfigPage::updateContextConductor);
|
||||
#endif
|
||||
|
||||
//Element Tab
|
||||
connect(m_saw_element, &SelectAutonumW::applyPressed, this, &ProjectAutoNumConfigPage::saveContextElement);
|
||||
connect(m_saw_element, &SelectAutonumW::removeClicked, this, &ProjectAutoNumConfigPage::removeContextElement);
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // TODO Qt6 only: remove, textActivated() always available
|
||||
connect(m_saw_element->contextComboBox(), SIGNAL(activated(QString)), this, SLOT(updateContextElement(QString)));
|
||||
#else
|
||||
connect(m_saw_element->contextComboBox(), &QComboBox::textActivated, this, &ProjectAutoNumConfigPage::updateContextElement);
|
||||
#endif
|
||||
|
||||
//Folio Tab
|
||||
connect(m_saw_folio, &SelectAutonumW::applyPressed, this, &ProjectAutoNumConfigPage::saveContextFolio);
|
||||
connect(m_saw_folio, &SelectAutonumW::removeClicked, this, &ProjectAutoNumConfigPage::removeContextFolio);
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // TODO Qt6 only: remove, textActivated() always available
|
||||
connect(m_saw_folio->contextComboBox(), SIGNAL(activated(QString)), this, SLOT(updateContextFolio(QString)));
|
||||
#else
|
||||
connect(m_saw_folio->contextComboBox(), &QComboBox::textActivated, this, &ProjectAutoNumConfigPage::updateContextFolio);
|
||||
#endif
|
||||
|
||||
// Auto Folio Numbering
|
||||
connect(m_faw, &FolioAutonumberingW::applyPressed, this, &ProjectAutoNumConfigPage::applyAutoNum);
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "../../qeticons.h"
|
||||
#include "../../shortcutmanager.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
@@ -27,10 +28,33 @@
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QTableWidget>
|
||||
#include <QRegularExpression>
|
||||
#include <QToolButton>
|
||||
#include <QTreeWidget>
|
||||
#include <QTreeWidgetItem>
|
||||
#include <QVBoxLayout>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
@brief normalizedForSearch
|
||||
Decompose \a text and strip every non-spacing combining mark, then fold the
|
||||
case. Both the query terms and the row's searchable text are run through
|
||||
this, so "general" matches "Général" and "Ctrl+S" matches "ctrl+s"
|
||||
regardless of the keyboard layout the query was typed on.
|
||||
*/
|
||||
static QString normalizedForSearch(const QString &text)
|
||||
{
|
||||
const QString decomposed = text.normalized(QString::NormalizationForm_D);
|
||||
QString stripped;
|
||||
stripped.reserve(decomposed.size());
|
||||
for (const QChar &c : decomposed) {
|
||||
if (c.category() != QChar::Mark_NonSpacing) {
|
||||
stripped.append(c);
|
||||
}
|
||||
}
|
||||
return stripped.toCaseFolded();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ShortcutsConfigPage::ShortcutsConfigPage
|
||||
@@ -51,18 +75,33 @@ ShortcutsConfigPage::ShortcutsConfigPage(QWidget *parent) :
|
||||
m_filter_edit = new QLineEdit(this);
|
||||
m_filter_edit->setPlaceholderText(tr("Filtrer les raccourcis…"));
|
||||
connect(m_filter_edit, &QLineEdit::textChanged, this, &ShortcutsConfigPage::filterRows);
|
||||
vlayout->addWidget(m_filter_edit);
|
||||
|
||||
m_table = new QTableWidget(0, 4, this);
|
||||
m_table->setHorizontalHeaderLabels({tr("Catégorie"), tr("Action"), tr("Raccourci"), QString()});
|
||||
m_table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||||
m_table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
|
||||
m_table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
||||
m_table->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
|
||||
m_table->verticalHeader()->setVisible(false);
|
||||
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_table->setSelectionMode(QAbstractItemView::NoSelection);
|
||||
vlayout->addWidget(m_table);
|
||||
m_quick_filter = new QComboBox(this);
|
||||
m_quick_filter->setObjectName(QStringLiteral("quickFilterCombo"));
|
||||
m_quick_filter->addItem(tr("Tous"));
|
||||
m_quick_filter->addItem(tr("Attribués uniquement"));
|
||||
m_quick_filter->addItem(tr("Non attribués uniquement"));
|
||||
m_quick_filter->addItem(tr("Conflits uniquement"));
|
||||
connect(m_quick_filter, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &ShortcutsConfigPage::quickFilterChanged);
|
||||
|
||||
m_count_label = new QLabel(this);
|
||||
m_count_label->setObjectName(QStringLiteral("shortcutCountLabel"));
|
||||
|
||||
auto *filter_layout = new QHBoxLayout();
|
||||
filter_layout->addWidget(m_filter_edit, 1);
|
||||
filter_layout->addWidget(m_quick_filter);
|
||||
filter_layout->addWidget(m_count_label);
|
||||
vlayout->addLayout(filter_layout);
|
||||
|
||||
m_tree = new QTreeWidget(this);
|
||||
m_tree->setHeaderLabels({tr("Action"), tr("Raccourci"), QString()});
|
||||
m_tree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
m_tree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
|
||||
m_tree->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
||||
m_tree->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_tree->setSelectionMode(QAbstractItemView::NoSelection);
|
||||
vlayout->addWidget(m_tree);
|
||||
|
||||
auto *reset_all_button = new QPushButton(tr("Tout réinitialiser"), this);
|
||||
connect(reset_all_button, &QPushButton::clicked, this, &ShortcutsConfigPage::resetAllRows);
|
||||
@@ -75,6 +114,7 @@ ShortcutsConfigPage::ShortcutsConfigPage(QWidget *parent) :
|
||||
setLayout(vlayout);
|
||||
|
||||
populateTable();
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
ShortcutsConfigPage::~ShortcutsConfigPage()
|
||||
@@ -83,8 +123,8 @@ ShortcutsConfigPage::~ShortcutsConfigPage()
|
||||
|
||||
/**
|
||||
@brief ShortcutsConfigPage::populateTable
|
||||
Fill the table with one row per shortcut known to ShortcutManager, sorted
|
||||
by category then action name.
|
||||
Fill the tree with one collapsible top-level node per category and one child
|
||||
per shortcut known to ShortcutManager, sorted by category then action name.
|
||||
*/
|
||||
void ShortcutsConfigPage::populateTable()
|
||||
{
|
||||
@@ -97,33 +137,38 @@ void ShortcutsConfigPage::populateTable()
|
||||
return a.description < b.description;
|
||||
});
|
||||
|
||||
m_table->setRowCount(shortcuts.size());
|
||||
m_tree->clear();
|
||||
m_rows.clear();
|
||||
m_rows.reserve(shortcuts.size());
|
||||
|
||||
for (int row = 0; row < shortcuts.size(); ++row) {
|
||||
const ShortcutManager::ShortcutInfo &info = shortcuts.at(row);
|
||||
QHash<QString, QTreeWidgetItem *> category_nodes;
|
||||
|
||||
auto *category_item = new QTableWidgetItem(info.category);
|
||||
category_item->setFlags(category_item->flags() & ~Qt::ItemIsEditable);
|
||||
m_table->setItem(row, 0, category_item);
|
||||
for (const ShortcutManager::ShortcutInfo &info : shortcuts) {
|
||||
QTreeWidgetItem *category_item = category_nodes.value(info.category, nullptr);
|
||||
if (!category_item) {
|
||||
category_item = new QTreeWidgetItem(m_tree);
|
||||
category_item->setText(0, info.category);
|
||||
category_item->setFlags(category_item->flags() & ~Qt::ItemIsEditable);
|
||||
category_nodes.insert(info.category, category_item);
|
||||
}
|
||||
|
||||
auto *description_item = new QTableWidgetItem(info.description);
|
||||
description_item->setFlags(description_item->flags() & ~Qt::ItemIsEditable);
|
||||
m_table->setItem(row, 1, description_item);
|
||||
auto *child = new QTreeWidgetItem(category_item);
|
||||
child->setText(0, info.description);
|
||||
child->setFlags(child->flags() & ~Qt::ItemIsEditable);
|
||||
|
||||
auto *edit = new QKeySequenceEdit(info.current_sequence, m_table);
|
||||
auto *edit = new QKeySequenceEdit(info.current_sequence, m_tree);
|
||||
connect(edit, &QKeySequenceEdit::editingFinished, this, &ShortcutsConfigPage::checkConflicts);
|
||||
m_table->setCellWidget(row, 2, edit);
|
||||
m_tree->setItemWidget(child, 1, edit);
|
||||
|
||||
auto *reset_button = new QToolButton(m_table);
|
||||
auto *reset_button = new QToolButton(m_tree);
|
||||
reset_button->setIcon(QET::Icons::EditUndo);
|
||||
reset_button->setToolTip(tr("Réinitialiser ce raccourci"));
|
||||
reset_button->setAutoRaise(true);
|
||||
connect(reset_button, &QToolButton::clicked, this, [this, row]() { resetRow(row); });
|
||||
m_table->setCellWidget(row, 3, reset_button);
|
||||
const int row_index = m_rows.size();
|
||||
connect(reset_button, &QToolButton::clicked, this, [this, row_index]() { resetRow(row_index); });
|
||||
m_tree->setItemWidget(child, 2, reset_button);
|
||||
|
||||
m_rows << Row{info.id, info.default_sequence, edit};
|
||||
m_rows << Row{info.id, info.category, info.description, info.default_sequence, edit, child, false};
|
||||
}
|
||||
|
||||
checkConflicts();
|
||||
@@ -131,17 +176,97 @@ void ShortcutsConfigPage::populateTable()
|
||||
|
||||
/**
|
||||
@brief ShortcutsConfigPage::filterRows
|
||||
Hide every row whose category or action name doesn't contain \a filter_text.
|
||||
Re-run the combined text + quick filter whenever the search box changes.
|
||||
*/
|
||||
void ShortcutsConfigPage::filterRows(const QString &filter_text)
|
||||
{
|
||||
const QString needle = filter_text.trimmed();
|
||||
for (int row = 0; row < m_table->rowCount(); ++row) {
|
||||
const bool matches = needle.isEmpty()
|
||||
|| m_table->item(row, 0)->text().contains(needle, Qt::CaseInsensitive)
|
||||
|| m_table->item(row, 1)->text().contains(needle, Qt::CaseInsensitive);
|
||||
m_table->setRowHidden(row, !matches);
|
||||
Q_UNUSED(filter_text)
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ShortcutsConfigPage::quickFilterChanged
|
||||
Re-run the combined text + quick filter whenever the quick filter changes.
|
||||
*/
|
||||
void ShortcutsConfigPage::quickFilterChanged(int index)
|
||||
{
|
||||
Q_UNUSED(index)
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ShortcutsConfigPage::applyFilter
|
||||
Hide every row that doesn't match both the search box and the quick filter.
|
||||
The text query is split on whitespace and every term must match the category,
|
||||
action name or current sequence (accent- and case-insensitively). Matching
|
||||
category nodes are expanded so hits are not hidden inside collapsed groups,
|
||||
and the "N actions" label tracks how many actions remain visible.
|
||||
*/
|
||||
void ShortcutsConfigPage::applyFilter()
|
||||
{
|
||||
const QString needle = m_filter_edit->text().trimmed();
|
||||
const QStringList terms = needle.isEmpty()
|
||||
? QStringList()
|
||||
: needle.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
|
||||
|
||||
const int quick_filter = m_quick_filter->currentIndex();
|
||||
|
||||
int visible_actions = 0;
|
||||
for (const Row &row : std::as_const(m_rows)) {
|
||||
// Text is matched by substring, but the key sequence is matched exactly:
|
||||
// a substring match would let "Ctrl+S" also hit "Ctrl+Shift+F" (the "S"
|
||||
// of "Shift"), which is precisely the kind of false positive that hides
|
||||
// the one binding the user is looking for.
|
||||
const QString text_haystack = normalizedForSearch(
|
||||
row.category + QLatin1Char(' ') + row.description);
|
||||
const QString sequence_text = normalizedForSearch(row.edit->keySequence().toString());
|
||||
|
||||
bool matches = true;
|
||||
for (const QString &term : terms) {
|
||||
const QString t = normalizedForSearch(term);
|
||||
if (!text_haystack.contains(t) && sequence_text != t) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
switch (quick_filter) {
|
||||
case BoundOnly:
|
||||
matches = !row.edit->keySequence().isEmpty();
|
||||
break;
|
||||
case UnboundOnly:
|
||||
matches = row.edit->keySequence().isEmpty();
|
||||
break;
|
||||
case ConflictsOnly:
|
||||
matches = row.conflicted;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
row.item->setHidden(!matches);
|
||||
if (matches) {
|
||||
++visible_actions;
|
||||
}
|
||||
}
|
||||
|
||||
const bool filtering = !needle.isEmpty() || quick_filter != ShowAll;
|
||||
for (int i = 0; i < m_tree->topLevelItemCount(); ++i) {
|
||||
QTreeWidgetItem *top = m_tree->topLevelItem(i);
|
||||
bool any_visible = false;
|
||||
for (int j = 0; j < top->childCount(); ++j) {
|
||||
if (!top->child(j)->isHidden()) {
|
||||
any_visible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
top->setHidden(!any_visible);
|
||||
top->setExpanded(filtering && any_visible);
|
||||
}
|
||||
|
||||
m_count_label->setText(tr("%n action(s)", nullptr, visible_actions));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,27 +285,37 @@ void ShortcutsConfigPage::checkConflicts()
|
||||
}
|
||||
|
||||
for (int row = 0; row < m_rows.size(); ++row) {
|
||||
const Row ¤t_row = m_rows.at(row);
|
||||
Row ¤t_row = m_rows[row];
|
||||
const QString sequence_text = current_row.edit->keySequence().toString();
|
||||
const QList<int> &conflicting_rows = sequence_to_rows.value(sequence_text);
|
||||
const bool conflicted = !sequence_text.isEmpty() && conflicting_rows.size() > 1;
|
||||
|
||||
QTableWidgetItem *description_item = m_table->item(row, 1);
|
||||
current_row.conflicted = conflicted;
|
||||
|
||||
if (conflicted) {
|
||||
QStringList other_descriptions;
|
||||
for (int other_row : conflicting_rows) {
|
||||
if (other_row != row) {
|
||||
other_descriptions << m_table->item(other_row, 1)->text();
|
||||
other_descriptions << m_rows.at(other_row).description;
|
||||
}
|
||||
}
|
||||
description_item->setBackground(QColor(255, 205, 205));
|
||||
current_row.item->setBackground(0, QColor(255, 205, 205));
|
||||
current_row.edit->setToolTip(
|
||||
tr("Ce raccourci est aussi utilisé par : %1").arg(other_descriptions.join(QStringLiteral(", "))));
|
||||
} else {
|
||||
description_item->setBackground(Qt::NoBrush);
|
||||
current_row.item->setBackground(0, QBrush());
|
||||
current_row.edit->setToolTip(QString());
|
||||
}
|
||||
}
|
||||
|
||||
// A sequence edit can change while a filter is active (search-by-key or the
|
||||
// conflicts-only quick filter); refresh the visible set so the list doesn't
|
||||
// show stale results.
|
||||
const bool filtering = !m_filter_edit->text().trimmed().isEmpty()
|
||||
|| m_quick_filter->currentIndex() != ShowAll;
|
||||
if (filtering) {
|
||||
applyFilter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,7 +333,7 @@ void ShortcutsConfigPage::resetRow(int row_index)
|
||||
|
||||
void ShortcutsConfigPage::resetAllRows()
|
||||
{
|
||||
for (const Row &row : qAsConst(m_rows)) {
|
||||
for (const Row &row : std::as_const(m_rows)) {
|
||||
row.edit->setKeySequence(row.default_sequence);
|
||||
}
|
||||
checkConflicts();
|
||||
@@ -211,7 +346,7 @@ void ShortcutsConfigPage::resetAllRows()
|
||||
*/
|
||||
void ShortcutsConfigPage::applyConf()
|
||||
{
|
||||
for (const Row &row : qAsConst(m_rows)) {
|
||||
for (const Row &row : std::as_const(m_rows)) {
|
||||
ShortcutManager::instance().setSequence(row.id, row.edit->keySequence());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,12 @@
|
||||
|
||||
#include <QKeySequence>
|
||||
|
||||
class QComboBox;
|
||||
class QKeySequenceEdit;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QTableWidget;
|
||||
class QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
|
||||
/**
|
||||
@brief The ShortcutsConfigPage class
|
||||
@@ -47,21 +50,36 @@ class ShortcutsConfigPage : public ConfigPage
|
||||
|
||||
private slots:
|
||||
void filterRows(const QString &filter_text);
|
||||
void quickFilterChanged(int index);
|
||||
void checkConflicts();
|
||||
void resetAllRows();
|
||||
|
||||
private:
|
||||
enum QuickFilter {
|
||||
ShowAll = 0,
|
||||
BoundOnly,
|
||||
UnboundOnly,
|
||||
ConflictsOnly
|
||||
};
|
||||
|
||||
struct Row {
|
||||
QString id;
|
||||
QString category;
|
||||
QString description;
|
||||
QKeySequence default_sequence;
|
||||
QKeySequenceEdit *edit;
|
||||
QTreeWidgetItem *item;
|
||||
bool conflicted;
|
||||
};
|
||||
|
||||
void populateTable();
|
||||
void applyFilter();
|
||||
void resetRow(int row_index);
|
||||
|
||||
QLineEdit *m_filter_edit;
|
||||
QTableWidget *m_table;
|
||||
QComboBox *m_quick_filter;
|
||||
QLabel *m_count_label;
|
||||
QTreeWidget *m_tree;
|
||||
QList<Row> m_rows;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
/*
|
||||
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 "diagramselection.h"
|
||||
#include "ui_diagramselection.h"
|
||||
|
||||
diagramselection::diagramselection(QETProject *prj, QWidget *parent) :
|
||||
QWidget(parent),
|
||||
ui(new Ui::diagramselection),
|
||||
prj_ (prj),
|
||||
list_diagram_ (prj -> diagrams())
|
||||
{
|
||||
ui -> setupUi(this);
|
||||
|
||||
QString project_title = prj_ -> title();
|
||||
if (project_title.isEmpty()) project_title = tr("Projet sans titre");
|
||||
|
||||
ui -> label_prj -> setText( tr("Projet : ") + project_title );
|
||||
load_TableDiagram();
|
||||
}
|
||||
|
||||
diagramselection::~diagramselection()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief load all Diagrams of project in table
|
||||
*/
|
||||
void diagramselection::load_TableDiagram()
|
||||
{
|
||||
// Clear all items
|
||||
ui -> tableDiagram -> clear();
|
||||
for (int i=ui -> tableDiagram -> rowCount()-1; i >= 0; --i) {
|
||||
ui -> tableDiagram->removeRow(i);
|
||||
}
|
||||
for (int i=ui -> tableDiagram -> columnCount()-1; i>=0; --i) {
|
||||
ui -> tableDiagram->removeColumn(i);
|
||||
}
|
||||
|
||||
// Set the setting of table
|
||||
ui -> tableDiagram -> setColumnCount(2);
|
||||
ui -> tableDiagram -> setSelectionBehavior (QAbstractItemView::SelectRows);
|
||||
ui -> tableDiagram -> setSelectionMode (QAbstractItemView::SingleSelection);
|
||||
ui -> tableDiagram -> setEditTriggers (QAbstractItemView::NoEditTriggers);
|
||||
QStringList titles;
|
||||
titles.clear();
|
||||
titles << tr("Sélection") << tr("Nom");
|
||||
ui-> tableDiagram -> setHorizontalHeaderLabels( titles );
|
||||
|
||||
// List Diagrams
|
||||
for(int i=0,j=0; i<list_diagram_.count(); i++,j++){
|
||||
QTableWidgetItem *item_Name = new QTableWidgetItem();
|
||||
QTableWidgetItem *item_State = new QTableWidgetItem();
|
||||
|
||||
QString diagram_title = list_diagram_.at(i) -> title();
|
||||
if (diagram_title.isEmpty())
|
||||
diagram_title = tr("Folio sans titre");
|
||||
|
||||
item_Name -> setData(Qt::DisplayRole, diagram_title);
|
||||
item_State -> setData(Qt::CheckStateRole, Qt::Checked);
|
||||
|
||||
ui -> tableDiagram -> setRowCount(j+1);
|
||||
ui -> tableDiagram -> setItem(j, 0, item_State);
|
||||
ui -> tableDiagram -> setItem(j, 1, item_Name);
|
||||
|
||||
}
|
||||
ui -> tableDiagram -> horizontalHeader() -> setStretchLastSection(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief get list of Diagrams is selected
|
||||
@return this list of Diagrams
|
||||
*/
|
||||
QList<Diagram *> diagramselection::list_of_DiagramSelected()
|
||||
{
|
||||
QList<Diagram *> listDiag;
|
||||
for(int i=0; i<ui -> tableDiagram -> rowCount();i++){
|
||||
if(ui -> tableDiagram -> item(i, 0)->checkState()){
|
||||
listDiag.push_back( list_diagram_[i] );
|
||||
}
|
||||
}
|
||||
return listDiag;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief contextMenuRequested
|
||||
@param pos
|
||||
*/
|
||||
void diagramselection::on_tableDiagram_customContextMenuRequested(const QPoint &pos){
|
||||
QMenu menu(this);
|
||||
QAction *desl = menu.addAction( tr("Désélectionner tout") );
|
||||
menu.addAction(QIcon(":/ico/16x16/dialog-ok.png"), tr("Sélectionner tout") );
|
||||
|
||||
// Exec Menu
|
||||
QAction *ret = menu.exec(ui -> tableDiagram -> viewport() -> mapToGlobal(pos));
|
||||
if (ret == desl) {
|
||||
for(int i=0; i<ui -> tableDiagram -> rowCount();i++)
|
||||
ui -> tableDiagram -> item(i, 0)->setCheckState(Qt::Unchecked);
|
||||
}
|
||||
else{
|
||||
for(int i=0; i<ui -> tableDiagram -> rowCount();i++)
|
||||
ui -> tableDiagram -> item(i, 0)->setCheckState(Qt::Checked);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
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 DIAGRAMSELECTION_H
|
||||
#define DIAGRAMSELECTION_H
|
||||
|
||||
#include "../diagram.h"
|
||||
#include "../qetproject.h"
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
namespace Ui {
|
||||
class diagramselection;
|
||||
}
|
||||
|
||||
class diagramselection : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit diagramselection(QETProject *prj, QWidget *parent = nullptr);
|
||||
~diagramselection() override;
|
||||
|
||||
QList<Diagram *> list_of_DiagramSelected();
|
||||
|
||||
private slots:
|
||||
void on_tableDiagram_customContextMenuRequested(const QPoint &pos);
|
||||
|
||||
private:
|
||||
Ui::diagramselection *ui;
|
||||
QETProject *prj_;
|
||||
QList<Diagram *> list_diagram_;
|
||||
|
||||
void load_TableDiagram();
|
||||
};
|
||||
|
||||
#endif // DIAGRAMSELECTION_H
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user