Merge branch 'master' into feature-3dmouse-spnav

This commit is contained in:
Laurent Trinques
2026-09-24 15:45:33 +02:00
committed by GitHub
123 changed files with 135169 additions and 688 deletions
@@ -49,6 +49,14 @@ ElementsCollectionModel::ElementsCollectionModel(QObject *parent) :
*/
ElementsCollectionModel::~ElementsCollectionModel()
{
// Without cancel(), the wait below runs the whole queued
// QtConcurrent::map() to completion, so closing this dialog on a
// large collection blocks until every remaining item has been
// processed -- a visible hang on the button pressed precisely to
// stop the work. cancel() drops the not-yet-started items so the
// wait that follows (still needed, so an in-flight item can't
// dereference this object after it's gone) is short.
m_future.cancel();
m_future.waitForFinished();
}
@@ -431,7 +439,7 @@ void ElementsCollectionModel::addLocation(const ElementsLocation& location)
collection_name);
}
}
// ANPASSUNG: Makros und Custom Collection werden hier behandelt!
// Macros and Custom Collection are handled here
else if (location.isCustomCollection() || location.isMacrosCollection()) {
QList <ElementCollectionItem *> child_list;
@@ -647,7 +655,7 @@ QModelIndex ElementsCollectionModel::indexFromLocation(
if (eci->type() == FileElementCollectionItem::Type) {
if (FileElementCollectionItem *feci = static_cast<FileElementCollectionItem *>(eci)) {
// ANPASSUNG: Makro-Prüfung hinzugefügt, damit das Modell den Pfad im Baum findet!
// Macro check added so the model finds the path in the tree
if ( (location.isCommonCollection() && feci->isCommonCollection()) ||
(location.isCompanyCollection() && feci->isCompanyCollection()) ||
(location.isMacrosCollection() && feci->isMacrosCollection()) ||
@@ -653,9 +653,16 @@ QDomElement ElementsLocation::xml() const
if (!m_project)
{
QFile file (m_file_system_path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return QDomElement();
QDomDocument docu;
if (docu.setContent(&file))
{
file.close();
return docu.documentElement();
}
file.close();
}
else
{
@@ -162,7 +162,7 @@ QString FileElementCollectionItem::localName()
else if (m_path == QETApp::customElementsDirN())
setText(QObject::tr("Collection utilisateur"));
else if (m_path == macrosPath)
setText(QObject::tr("Makros"));
setText(QObject::tr("Macros"));
else
setText(QObject::tr("Collection inconnue"));
}
@@ -46,10 +46,11 @@ FreeTerminalEditor::FreeTerminalEditor(QETProject *project, QWidget *parent) :
connect(m_project, &QObject::destroyed, this, &FreeTerminalEditor::reload);
}
//Disabled the move if the table is currently edited (yellow cell)
connect(m_model, &FreeTerminalModel::dataChanged, this, [=] {
this->setDisabledMove();
});
//Disable the move button while cells are edited (yellow), re-evaluate on every change
connect(m_model, &FreeTerminalModel::dataChanged, this, &FreeTerminalEditor::selectionChanged);
connect(ui->m_table_view->selectionModel(), &QItemSelectionModel::selectionChanged,
this, &FreeTerminalEditor::selectionChanged);
connect(ui->m_table_view, &QAbstractItemView::doubleClicked, this, [=](const QModelIndex &index)
{
@@ -102,7 +103,7 @@ void FreeTerminalEditor::reload()
QString str(strip->installation() + " " + strip->location() + " " + strip->name());
ui->m_move_in_cb->addItem(str, strip->uuid());
}
setDisabledMove(false);
selectionChanged();
}
}
@@ -273,10 +274,19 @@ void FreeTerminalEditor::on_m_move_pb_clicked()
reload();
}
void FreeTerminalEditor::selectionChanged()
{
const bool has_selection = !ui->m_table_view->selectionModel()->selectedIndexes().isEmpty();
const bool has_pending = !m_model->modifiedModelRealTerminalData().isEmpty();
setDisabledMove(!has_selection || has_pending);
}
void FreeTerminalEditor::setDisabledMove(bool b)
{
ui->m_move_label->setDisabled(b);
ui->m_move_in_cb->setDisabled(b);
ui->m_move_pb->setDisabled(b);
ui->m_move_pb->setToolTip(b ? tr("Appliquez ou annulez les modifications en cours avant de déplacer")
: tr("Déplacer les bornes sélectionnées vers le bornier choisi"));
}
@@ -182,7 +182,7 @@ bool FreeTerminalModel::setData(const QModelIndex &index, const QVariant &value,
modified_ = true;
modified_cell = FUNCTION_CELL;
}
else if (column_ == LED_CELL)
else if (column_ == LED_CELL && mrtd.led_ != value.toBool())
{
mrtd.led_ = value.toBool();
modified_ = true;
@@ -0,0 +1,78 @@
/*
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 "renumberelementscommand.h"
#include "../qetproject.h"
#include "../qetgraphicsitem/element.h"
RenumberElementsCommand::RenumberElementsCommand(
QETProject *project,
QVector<ElementChange> changes,
QHash<QString, NumerotationContext> old_ctx,
QHash<QString, NumerotationContext> new_ctx,
const QString &text)
: QUndoCommand(text)
, project_(project)
, changes_(std::move(changes))
, old_ctx_(std::move(old_ctx))
, new_ctx_(std::move(new_ctx))
{
}
void RenumberElementsCommand::undo()
{
apply(false);
}
void RenumberElementsCommand::redo()
{
// QUndoStack calls redo() once immediately after push(); keep standard behaviour.
if (first_redo_) {
first_redo_ = false;
}
apply(true);
}
void RenumberElementsCommand::apply(bool use_new)
{
if (!project_) return;
// Restore project contexts
const auto &ctx = use_new ? new_ctx_ : old_ctx_;
for (auto it = ctx.constBegin(); it != ctx.constEnd(); ++it) {
project_->addElementAutoNum(it.key(), it.value());
}
// Apply per-element changes
for (const ElementChange &c : changes_) {
if (!c.element) continue;
const bool frozen = use_new ? c.new_frozen : c.old_frozen;
const auto &infos = use_new ? c.new_infos : c.old_infos;
const auto &seq = use_new ? c.new_seq : c.old_seq;
// Temporarily unfreeze so that label/infos update correctly.
const bool was_frozen = c.element->isFreezeLabel();
if (was_frozen) c.element->freezeLabel(false);
c.element->rSequenceStruct() = seq;
c.element->setElementInformations(infos);
c.element->freezeLabel(frozen);
}
}
+70
View File
@@ -0,0 +1,70 @@
/*
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 RENUMBERELEMENTSCOMMAND_H
#define RENUMBERELEMENTSCOMMAND_H
#include <QUndoCommand>
#include <QHash>
#include <QVector>
#include "../diagramcontext.h"
#include "assignvariables.h" // defines autonum::sequentialNumbers
class Element;
class QETProject;
/**
* @brief Undoable renumbering of element labels and sequence structs.
*/
class RenumberElementsCommand final : public QUndoCommand
{
public:
struct ElementChange
{
Element *element = nullptr;
DiagramContext old_infos;
DiagramContext new_infos;
autonum::sequentialNumbers old_seq;
autonum::sequentialNumbers new_seq;
bool old_frozen = false;
bool new_frozen = false;
};
RenumberElementsCommand(
QETProject *project,
QVector<ElementChange> changes,
QHash<QString, NumerotationContext> old_ctx,
QHash<QString, NumerotationContext> new_ctx,
const QString &text);
void undo() override;
void redo() override;
private:
void apply(bool use_new);
private:
QETProject *project_ = nullptr;
QVector<ElementChange> changes_;
QHash<QString, NumerotationContext> old_ctx_;
QHash<QString, NumerotationContext> new_ctx_;
bool first_redo_ = true;
};
#endif // RENUMBERELEMENTSCOMMAND_H
@@ -23,6 +23,7 @@
#include "formulaautonumberingw.h"
#include "numparteditorw.h"
#include "qdebug.h"
#include "renumberelementsdialog.h"
#include "ui_autonumberingmanagementw.h"
#include "ui_formulaautonumberingw.h"
@@ -48,6 +49,8 @@ AutoNumberingManagementW::AutoNumberingManagementW(QETProject *project,
ui->m_selected_folios_le->setDisabled(true);
ui->m_selected_folios_le->setReadOnly(true);
ui->m_apply_project_rb->setChecked(true);
// Enabled only when project is in "Under Development" status and not read-only.
ui->m_renumber_elements_pb->setEnabled(ui->m_status_cb->currentIndex() == 0 && project_ && !project_->isReadOnly());
setProjectContext();
}
@@ -87,6 +90,7 @@ void AutoNumberingManagementW::on_m_status_cb_currentIndexChanged(int index)
ui->m_both_conductor_rb->setChecked(true);
ui->m_both_element_rb->setChecked(true);
ui->m_both_folio_rb->setChecked(true);
ui->m_renumber_elements_pb->setEnabled(true);
}
//Installing
else if (index == 1) {
@@ -96,15 +100,31 @@ void AutoNumberingManagementW::on_m_status_cb_currentIndexChanged(int index)
ui->m_new_conductor_rb->setChecked(true);
ui->m_new_element_rb->setChecked(true);
ui->m_new_folio_rb->setChecked(true);
ui->m_renumber_elements_pb->setEnabled(true);
}
//Built
else if (index == 2) {
ui->m_disable_conductor_rb->setChecked(true);
ui->m_disable_element_rb->setChecked(true);
ui->m_disable_folio_rb->setChecked(true);
ui->m_renumber_elements_pb->setEnabled(false);
}
}
void AutoNumberingManagementW::on_m_renumber_elements_pb_clicked()
{
if (!project_ || project_->isReadOnly()) return;
// Only allowed during "Under Development"
// if (ui->m_status_cb->currentIndex() != 1) return;
QStringList titles = project_->elementAutoNum().keys();
titles.sort(Qt::CaseInsensitive);
RenumberElementsDialog dlg(titles, this);
if (dlg.exec() != QDialog::Accepted) return;
project_->renumberElementsBySchemeTitle(dlg.selectedSchemeTitle());
}
/**
@brief AutoNumberingManagementW::on_m_apply_folios_rb_clicked
Set From Folios Combobox
@@ -50,6 +50,7 @@ class AutoNumberingManagementW : public QWidget
void on_m_from_folios_cb_currentIndexChanged(int);
void on_m_to_folios_cb_currentIndexChanged(int);
void on_m_status_cb_currentIndexChanged(int);
void on_m_renumber_elements_pb_clicked();
void on_m_apply_folios_rb_clicked();
void on_m_apply_project_rb_clicked();
void on_buttonBox_clicked(QAbstractButton *);
@@ -305,6 +305,13 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="m_renumber_elements_pb">
<property name="text">
<string>Renumber element(s)…</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
@@ -0,0 +1,75 @@
/*
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 "renumberelementsdialog.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QRadioButton>
#include <QVBoxLayout>
RenumberElementsDialog::RenumberElementsDialog(const QStringList &scheme_titles, QWidget *parent)
: QDialog(parent)
, m_scheme_titles(scheme_titles)
{
setWindowTitle(tr("Renumber element(s)"));
setModal(true);
auto *vl = new QVBoxLayout(this);
auto *scope = new QGroupBox(tr("Scope"), this);
auto *scope_l = new QVBoxLayout(scope);
m_all_rb = new QRadioButton(tr("All schemes"), scope);
m_one_rb = new QRadioButton(tr("One scheme"), scope);
scope_l->addWidget(m_all_rb);
scope_l->addWidget(m_one_rb);
vl->addWidget(scope);
auto *form = new QFormLayout();
m_combo = new QComboBox(this);
m_combo->addItems(m_scheme_titles);
form->addRow(new QLabel(tr("Scheme:"), this), m_combo);
vl->addLayout(form);
m_buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(m_buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(m_buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
vl->addWidget(m_buttons);
m_all_rb->setChecked(true);
updateUi();
connect(m_all_rb, &QRadioButton::toggled, this, &RenumberElementsDialog::updateUi);
connect(m_one_rb, &QRadioButton::toggled, this, &RenumberElementsDialog::updateUi);
}
QString RenumberElementsDialog::selectedSchemeTitle() const
{
if (m_all_rb && m_all_rb->isChecked()) return QString();
return m_combo ? m_combo->currentText() : QString();
}
void RenumberElementsDialog::updateUi()
{
const bool one = m_one_rb && m_one_rb->isChecked();
if (m_combo) m_combo->setEnabled(one);
}
@@ -0,0 +1,59 @@
/*
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 RENUMBERELEMENTSDIALOG_H
#define RENUMBERELEMENTSDIALOG_H
#include <QDialog>
#include <QStringList>
class QComboBox;
class QRadioButton;
class QDialogButtonBox;
/**
* @brief Simple dialog to pick renumbering scope for elements.
*
* The user can choose between:
* - all element autonumbering schemes
* - one selected scheme title
*/
class RenumberElementsDialog final : public QDialog
{
Q_OBJECT
public:
explicit RenumberElementsDialog(const QStringList &scheme_titles, QWidget *parent = nullptr);
/**
* @return Empty string if "all" is selected, otherwise the chosen scheme title.
*/
QString selectedSchemeTitle() const;
private slots:
void updateUi();
private:
QStringList m_scheme_titles;
QRadioButton *m_all_rb = nullptr;
QRadioButton *m_one_rb = nullptr;
QComboBox *m_combo = nullptr;
QDialogButtonBox *m_buttons = nullptr;
};
#endif // RENUMBERELEMENTSDIALOG_H
+12 -2
View File
@@ -896,10 +896,20 @@ void BorderTitleBlock::updateDiagramContextForTitleBlock(
// An empty page-level value means the variable was auto-added to the
// folio's Custom tab (#495) but never actually set by the user, so it
// must not shadow a real project-level value of the same name (#531).
//
// That guard has to stop short of removing the key outright, though
// (#973). TitleBlockTemplate::interpreteVariables() only replaces a
// "%name"/"%{name}" placeholder when "name" is a key in this context at
// all -- an unset variable that never makes it in is left as its own
// literal placeholder text in the rendered title block, not blank.
// So an empty page-level value is skipped only when a real project-level
// one is already there to show through; otherwise it still goes in
// empty, which is what makes the placeholder resolve to nothing.
DiagramContext context = initial_context;
foreach (QString key, additional_fields_.keys()) {
if (!additional_fields_[key].toString().isEmpty())
context.addValue(key, additional_fields_[key]);
const QVariant value = additional_fields_[key];
if (!value.toString().isEmpty() || !context.contains(key))
context.addValue(key, value);
}
// ... overridden by the historical and/or dynamically generated fields
+74 -26
View File
@@ -17,6 +17,8 @@
*/
#include "projectdatabase.h"
#include "sqlreadonly.h"
#include "../diagram.h"
#include "../diagramposition.h"
#include "../elementprovider.h"
@@ -28,13 +30,15 @@
#include "../qetproject.h"
#include <QLocale>
#include <QFile>
#include <QRegularExpression>
#include <QSqlError>
#include <QSqlDriver>
#include <QSqlError>
#include <sqlite3.h>
/**
@brief projectDataBase::projectDataBase
Default constructor
@@ -208,6 +212,12 @@ bool projectDataBase::isReadOnlySelect(const QString &query, QString *error)
*/
QSqlQuery projectDataBase::newQuery(const QString &query, QString *error) {
QString reason;
// First gate: which kind of statement is acceptable here at all. A
// textual check is the right tool for that and the wrong tool for
// anything else -- see isReadOnlySelect()'s own comment. It is what
// keeps ATTACH, BEGIN and PRAGMA out, none of which SQLite itself
// considers writes.
if (!isReadOnlySelect(query, &reason)) {
qWarning().noquote() << "projectDataBase::newQuery: rejected query:" << reason << "--" << query;
if (error) {
@@ -215,6 +225,24 @@ QSqlQuery projectDataBase::newQuery(const QString &query, QString *error) {
}
return QSqlQuery(m_data_base);
}
// Second gate, and the one that actually enforces read-only: SQLite is
// asked about the statement it compiled, instead of the text being read
// for clues. The first gate cannot see through a CTE prefix --
// "WITH x AS (SELECT 1) DELETE FROM element" starts with WITH, contains
// no semicolon, and deletes every row. That matters beyond the
// custom-query box, because this path is reachable from a file: a
// <graphics_table>'s saved <query> is read straight out of the .qet by
// ProjectDBModel::fromXml() and executed by fillValue(), so opening or
// exporting a project someone else produced would have been enough.
if (!QETSql::isSingleReadOnlyStatement(sqliteHandle(&m_data_base), query, &reason)) {
qWarning().noquote() << "projectDataBase::newQuery: rejected query:" << reason << "--" << query;
if (error) {
*error = reason;
}
return QSqlQuery(m_data_base);
}
return QSqlQuery(query, m_data_base);
}
@@ -291,10 +319,23 @@ void projectDataBase::addElement(Element *element)
void projectDataBase::removeElement(Element *element)
{
m_content_changed = true;
bool changed = false;
m_remove_element_query.bindValue(":uuid", element->uuid().toString());
if(!m_remove_element_query.exec()) {
qDebug() << "projectDataBase::removeElement remove error : " << m_remove_element_query.lastError();
if (m_remove_element_query.exec()) {
changed = true;
} else {
qDebug() << "projectDataBase::removeElement remove error : " << m_remove_element_query.lastError();
}
m_remove_element_info_query.bindValue(":uuid", element->uuid().toString());
if (m_remove_element_info_query.exec()) {
changed = true;
} else {
qDebug() << "projectDataBase::removeElement remove element_info error : " << m_remove_element_info_query.lastError();
}
if (changed) {
emit dataBaseUpdated();
}
}
@@ -1131,6 +1172,21 @@ void projectDataBase::prepareQuery()
m_remove_element_query = QSqlQuery(m_data_base);
m_remove_element_query.prepare(remove_element);
//REMOVE ELEMENT INFO
//element_info has no ON DELETE CASCADE (foreign keys aren't
//enforced by this connection), so removeElement() must clear it
//explicitly. Without this, the row is orphaned under the removed
//element's uuid, and re-adding an element with that same uuid
//later -- undo of this same removal, or a redo replaying it --
//hits element_info's PRIMARY KEY constraint on element_uuid: the
//element re-add succeeds, but its element_info insert silently
//fails and is lost. removeDiagram()'s cascade already clears this
//table when a whole folio goes, but that does not run for a
//single element removed on its own.
QString remove_element_info("DELETE FROM element_info WHERE element_uuid=:uuid");
m_remove_element_info_query = QSqlQuery(m_data_base);
m_remove_element_info_query.prepare(remove_element_info);
//UPDATE ELEMENT INFO
QString update_str("UPDATE element_info SET ");
for (auto string : QETInformation::elementInfoKeys()) {
@@ -1245,7 +1301,6 @@ void projectDataBase::bindDiagramInfoValues(QSqlQuery &query, Diagram *diagram)
}
}
#ifdef QET_EXPORT_PROJECT_DB
/**
@brief projectDataBase::sqliteHandle
@param db
@@ -1263,6 +1318,7 @@ sqlite3 *projectDataBase::sqliteHandle(QSqlDatabase *db)
return handle;
}
#ifdef QET_EXPORT_PROJECT_DB
/**
* @brief projectDataBase::exportDb
@@ -1298,27 +1354,19 @@ void projectDataBase::exportDb(projectDataBase *db,
return;
}
QString connection_name("export_project_db_" % db->project()->uuid().toString());
if (true) //Enter in a scope only to nicely use QSqlDatabase::removeDatabase just after the end of the scope
{
auto file_db = QSqlDatabase::addDatabase("QSQLITE", connection_name);
file_db.setDatabaseName(path_);
if (!file_db.open()) {
return;
}
auto memory_db_handle = sqliteHandle(&db->m_data_base);
auto file_db_handle = sqliteHandle(&file_db);
auto sqlite_backup = sqlite3_backup_init(file_db_handle, "main", memory_db_handle, "main");
if (sqlite_backup)
{
sqlite3_backup_step(sqlite_backup, -1);
sqlite3_backup_finish(sqlite_backup);
}
file_db.close();
// VACUUM INTO requires the destination not to exist. QFileDialog may ask
// about overwriting, but it does not remove the existing file for us.
if (QFile::exists(path_) && !QFile::remove(path_)) {
qWarning() << "Unable to replace project database export:" << path_;
return;
}
// VACUUM INTO creates a standalone copy of the current database without
// requiring access to the SQLite driver's native connection handle.
const auto escaped_path = path_.replace("'", "''");
QSqlQuery query(db->m_data_base);
if (!query.exec("VACUUM INTO '" % escaped_path % "'")) {
qWarning() << "Unable to export project database:" << query.lastError().text();
}
QSqlDatabase::removeDatabase(connection_name);
}
#endif
+28 -2
View File
@@ -29,7 +29,7 @@ class QETProject;
class Diagram;
class Conductor;
class Terminal;
class sqlite3;
struct sqlite3;
/**
@brief The projectDataBase class
@@ -61,6 +61,26 @@ class projectDataBase : public QObject
QETProject *project() const;
QSqlQuery newQuery(const QString &query = QString(), QString *error = nullptr);
static bool isReadOnlySelect(const QString &query, QString *error = nullptr);
/**
The most rows any caller reads out of one query result.
A SELECT is not bounded by how much data the project holds:
SQLite produces rows lazily, so a query that never stops
producing them makes the loop that reads them never stop
either. A recursive CTE does exactly that in one line, and
a <graphics_table>'s <query> is stored in the .qet and run
on load -- so the text can arrive from a file rather than
from the person at the keyboard, and opening that file is
the whole attack.
100000 is far above any real result: the largest table in
the shipped examples is 396 rows. It is a backstop, not a
page size -- a caller that hits it has almost certainly
been handed something it should not run to completion, and
says so rather than truncating quietly.
*/
static constexpr int MaxResultRows = 100000;
QSqlDatabase database() const {return m_data_base;}
int excludedConductorCount() const;
@@ -119,6 +139,7 @@ class projectDataBase : public QObject
QSqlQuery m_insert_elements_query,
m_insert_element_info_query,
m_remove_element_query,
m_remove_element_info_query,
m_update_element_query,
m_insert_diagram_query,
m_remove_diagram_query,
@@ -135,9 +156,14 @@ class projectDataBase : public QObject
m_cascade_remove_conductor_query,
m_cascade_remove_element_query;
public:
// Deliberately outside the QET_EXPORT_PROJECT_DB guard below:
// newQuery() needs the raw connection to ask SQLite whether a
// query only reads, and that check runs in every build.
static sqlite3 *sqliteHandle(QSqlDatabase *db);
#ifdef QET_EXPORT_PROJECT_DB
public:
static sqlite3 *sqliteHandle(QSqlDatabase *db);
static void exportDb(projectDataBase *db,
QWidget *parent = nullptr,
const QString &caption = QString(),
+134
View File
@@ -0,0 +1,134 @@
/*
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 "sqlreadonly.h"
#include <QCoreApplication>
#include <sqlite3.h>
namespace QETSql {
/**
@brief QETSql::isSingleReadOnlyStatement
Ask SQLite itself whether @p query is exactly one statement, and whether
that statement only reads.
Why SQLite is asked rather than the text inspected: a check on the
query's first keyword cannot see what the statement actually does.
SQLite has allowed a CTE prefix in front of a data-modifying statement
since 3.8.3, so
@code
WITH x AS (SELECT 1) DELETE FROM element
@endcode
begins with WITH, contains no semicolon, and deletes every row.
sqlite3_stmt_readonly() reports on the statement SQLite compiled, not
on how it was spelled, so the same query is correctly refused here
while an ordinary WITH ... SELECT still passes.
The statement is compiled and immediately finalised; sqlite3_prepare_v2()
does not run it, so nothing is executed to reach this verdict.
This is a read-only test, NOT a statement-type allowlist: SQLite
considers ATTACH, BEGIN and several PRAGMAs read-only too, because none
of them change the contents of the database. Callers that need to
restrict which *kind* of statement is acceptable must say so separately
-- projectDataBase::newQuery() keeps isReadOnlySelect() in front of this
for exactly that reason.
@param handle the connection the query would run on. A null handle is
refused rather than waved through: without it there is nothing to ask,
and guessing from the text is the weakness this exists to replace.
@param query the raw SQL text
@param error set to a human-readable reason when this returns false
@return true if @p query is a single, read-only statement
*/
bool isSingleReadOnlyStatement(sqlite3 *handle, const QString &query, QString *error)
{
if (error) {
error->clear();
}
if (!handle) {
if (error) {
*error = QCoreApplication::translate("QETSql",
"Impossible de vérifier la requête : "
"aucune connexion SQLite disponible.");
}
return false;
}
const QByteArray utf8 = query.toUtf8();
sqlite3_stmt *statement = nullptr;
const char *tail = nullptr;
if (sqlite3_prepare_v2(handle, utf8.constData(), utf8.size(),
&statement, &tail) != SQLITE_OK)
{
if (error) {
*error = QCoreApplication::translate("QETSql",
"Requête SQL invalide : %1")
.arg(QString::fromUtf8(sqlite3_errmsg(handle)));
}
sqlite3_finalize(statement);
return false;
}
// Whitespace or a bare comment compiles successfully to no statement
// at all, and sqlite3_stmt_readonly() must not be handed that.
if (!statement) {
if (error) {
*error = QCoreApplication::translate("QETSql",
"La requête ne contient aucune instruction.");
}
return false;
}
const bool read_only = sqlite3_stmt_readonly(statement) != 0;
sqlite3_finalize(statement);
if (!read_only) {
if (error) {
*error = QCoreApplication::translate("QETSql",
"Seules les requêtes en lecture seule sont autorisées : "
"cette requête modifierait la base de données.");
}
return false;
}
// tail points just past the first statement, semicolon included.
// Anything left once semicolons and spacing are stripped is a second
// statement -- caught structurally here, where "SELECT ';'" is a
// perfectly ordinary query rather than a suspicious string.
if (tail) {
QString rest = QString::fromUtf8(tail);
rest.remove(QLatin1Char(';'));
if (!rest.trimmed().isEmpty()) {
if (error) {
*error = QCoreApplication::translate("QETSql",
"Une seule requête est autorisée.");
}
return false;
}
}
return true;
}
} // namespace QETSql
+43
View File
@@ -0,0 +1,43 @@
/*
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 SQLREADONLY_H
#define SQLREADONLY_H
#include <QString>
struct sqlite3;
/**
Deciding whether a piece of SQL only reads.
Deliberately its own translation unit, depending on nothing but QString
and SQLite: it is the enforcement point for every query QElectroTech
runs against a project database, including queries that arrive from
outside the application (a .qet file's saved report/table query), so it
is worth being able to test it in isolation -- see
tests/qttest/tst_sqlreadonly.cpp, which links this file and nothing
else of QElectroTech.
*/
namespace QETSql {
bool isSingleReadOnlyStatement(sqlite3 *handle,
const QString &query,
QString *error = nullptr);
}
#endif // SQLREADONLY_H
+17 -1
View File
@@ -77,6 +77,23 @@ void PasteDiagramCommand::redo()
{
first_redo = false;
//Resolve a linked master/slave pair pasted together (bugtracker
//#607) before anything below renews their uuids: at this exact
//moment a pasted element's tmp_uuids_link still holds its
//source's original partner uuid, which still equals the
//not-yet-renewed uuid of that partner's own pasted copy if it
//was carried along in the same batch. Scoped to this batch only
//(not a project-wide search), so a pair pasted together links to
//each other and not to an original element left elsewhere that
//happens to still carry that same soon-to-be-replaced uuid. If
//only one half of a linked group was pasted, its link entry
//simply finds no match here and is dropped -- same "leave it
//unlinked" outcome as always.
const QList <Element *> elmts_list = content.m_elements;
for (Element *e : elmts_list) {
e->initLink(elmts_list);
}
//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();
@@ -85,7 +102,6 @@ void PasteDiagramCommand::redo()
}
//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)
{
//make new uuid, because old uuid are the uuid of the copied element
+11 -3
View File
@@ -151,8 +151,8 @@ bool DiagramContext::operator!=(const DiagramContext &dc) const
void DiagramContext::toXml(QDomElement &e, const QString &tag_name) const
{
foreach (QString key, keys()) {
if ((tag_name == "elementInformation") &&
(m_content[key].toString().trimmed().isEmpty())) {
const QString raw = m_content[key].toString();
if ((tag_name == "elementInformation") && raw.trimmed().isEmpty()) {
continue;
}
QDomElement property = e.ownerDocument().createElement(tag_name);
@@ -161,7 +161,15 @@ void DiagramContext::toXml(QDomElement &e, const QString &tag_name) const
property.removeAttribute("name");
property.setAttribute("show", m_content_show[key]);
property.setAttribute("name", key);
QDomText value = e.ownerDocument().createTextNode(m_content[key].toString().trimmed());
// Trim stray leading/trailing whitespace around real content, but
// not a value that IS whitespace: unconditionally trimming an
// all-whitespace string collapses it to "", which is silently
// indistinguishable from a value that was never set. A title-block
// custom variable set to a single space -- a workaround for #973,
// where an unset variable renders as its own literal placeholder --
// would otherwise vanish on the very next save.
const QString stored = raw.trimmed().isEmpty() ? raw : raw.trimmed();
QDomText value = e.ownerDocument().createTextNode(stored);
property.appendChild(value);
e.appendChild(property);
}
+32 -15
View File
@@ -22,6 +22,7 @@
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
#include "../qetgraphicsitem/conductor.h"
#include "../qetproject.h"
#include <QSettings>
@@ -40,6 +41,7 @@
DiagramEventAddPaste::DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos) :
DiagramEventInterface(diagram)
{
Q_UNUSED(start_pos); // items stay at their original XML position
//DiagramEventInterface::init() is called by Diagram::setEventInterface
//only when it is replacing an earlier interface, so call it here as
//DiagramEventAddMacro does.
@@ -51,8 +53,25 @@
QDomDocument document_xml;
if (!document_xml.setContent(clipboard_text)) return;
//Batch the database work the same way project loading does
//(QETProject::readProjectXml): without this, every addItem()
//below emits dataBaseUpdated(), which makes each connected
//table model re-run its full SQL query -- ~77 queries for a
//typical paste, i.e. the multi-second stall on Ctrl+V.
auto *db = m_diagram->project() ? m_diagram->project()->dataBase() : nullptr;
if (db) {
db->blockSignals(true);
db->setUpdateBlocked(true);
}
//Load items at their original XML coordinates.
m_diagram->fromXml(document_xml, QPointF(), false, &m_content);
if (db) {
db->blockSignals(false);
db->setUpdateBlocked(false);
db->updateDB();
}
if (!m_content.count()) return;
const QList<QGraphicsItem *> movable = m_content.items(MovableItems);
@@ -85,33 +104,23 @@
};
const QPointF grid_origin = snapGrid(top_left);
//Move the group to the cursor, rather than the cursor to the
//group. Both put the copy under the pointer, but warping the
//pointer also drags it back to the original's position, so the
//copy appears exactly on top of what was copied until the mouse
//is moved -- which is the thing pasting under the cursor was
//meant to avoid (issue #913). Taking the pointer away from
//where the user put it is also its own surprise.
m_group_origin = snapGrid(start_pos);
const QPointF offset = m_group_origin - grid_origin;
//Store each item's position after the move. moveTo() applies a
//Store each item's original position. moveTo() applies a
//grid-snapped delta from the baseline to these, so items
//preserve their layout and move in whole grid steps.
for (auto *item : movable) {
item->setPos(item->pos() + offset);
m_relative_pos.insert(item, item->pos());
}
m_group_origin = grid_origin;
//The conductors were laid out against the old terminal
//The conductors were laid out against the original terminal
//positions, so re-route them before anything is drawn.
const QList<Conductor *> conductors = m_content.conductors(DiagramContent::AnyConductor);
for (auto *conductor : conductors) {
conductor->updatePath();
}
//The baseline is known now, so moveTo() does not have to
//capture one from the first mouse movement.
//The baseline is the group's grid-snapped origin, so moveTo()
//does not have to capture one from the first mouse movement.
m_initial_cursor = m_group_origin;
m_baseline_captured = true;
@@ -125,6 +134,14 @@
if (const auto qde = QETApp::diagramEditorAncestorOf(view)) {
m_status_bar = qde->statusBar();
}
//Warp the cursor to the group's grid-snapped origin so
//the actual cursor position matches m_initial_cursor.
//Without this the first mouseMoveEvent computes a large
//delta (cursor is still at the Ctrl+V press location)
//and the items jump on first touch.
const QPoint view_pos = view->mapFromScene(m_initial_cursor);
const QPoint global_pos = view->viewport()->mapToGlobal(view_pos);
QCursor::setPos(global_pos);
}
}
showHint();
+77 -1
View File
@@ -445,6 +445,82 @@ void DiagramView::pasteHere()
paste(mapToScene(m_paste_here_pos));
}
/**
@brief DiagramView::duplicate
Copy the current selection and place the copy at @p stepOffset grid
steps from it, landing immediately rather than following the cursor
like Ctrl+V does (bugtracker #991). @p stepOffset comes from
DuplicateOffsetDialog: (1, 0) is one grid step right, (0, -1) is one
grid step up, and so on -- QET's own scene axes, X right and Y down.
No interactive placement step on purpose: the point of a duplicate
shortcut is unattended, repeatable stamping (configure the offset
once, then tap Ctrl+D to lay out a row), which following the cursor
would interrupt on every press. QET already reselects whatever a
paste just added (see PasteDiagramCommand::redo()), so the next
Ctrl+D naturally continues from the copy just placed, not the
original -- a press-and-hold row falls out of that for free, with no
special-casing needed here for "keep going from the last one".
The offset is applied by hand rather than by asking paste()/
Diagram::fromXml() to place the copy at a target position. Both of
those feed the position through Diagram::snapToGrid(), which reads
QApplication::keyboardModifiers() and rounds to the nearest PIXEL
instead of the grid whenever Ctrl is held -- and Ctrl is always held
here, this action's own shortcut being Ctrl+D. Measured the hard way
before settling on this: routing the offset through paste() first
produced copies off-grid on both axes, by an amount that tracked the
selection's own bounding-box geometry rather than being a fixed
error. fromXml() is instead called with no position at all, which
leaves every item at its source coordinates (landing the copy
exactly on top of the originals -- (0, 0) is not a position, this is
"keep the source coordinates"), and the offset is added directly
with setPos(). A plain addition cannot be off by a rounding rule
that never runs.
Conductors are not in the translated set: fromXml() itself does not
reposition them either -- they are loaded from XML after elements
are already in their final place and take their geometry from their
terminals, which have already moved with the elements that own
them. Likewise dynamic element texts are not translated separately:
they are children of their element and move with it under Qt's
normal parent-child transform.
*/
void DiagramView::duplicate(const QPoint &stepOffset)
{
if (!isInteractive() || m_diagram->isReadOnly()) return;
const QList<QGraphicsItem *> selection = m_diagram->selectedItems();
if (selection.isEmpty()) return;
QSettings settings;
const int x_grid = settings.value(QStringLiteral("diagrameditor/Xgrid"),
Diagram::xGrid).toInt();
const int y_grid = settings.value(QStringLiteral("diagrameditor/Ygrid"),
Diagram::yGrid).toInt();
const QPointF offset(stepOffset.x() * x_grid, stepOffset.y() * y_grid);
// Mirrors copy(), but does not touch the system clipboard: Ctrl+D
// should not clobber whatever the user last copied with Ctrl+C.
QDomDocument document = m_diagram->toXml(false, true);
DiagramContent pasted;
// No position argument -- see the function comment above for why
// the offset is not passed here.
m_diagram->fromXml(document, QPointF(), false, &pasted);
if (!pasted.count()) return;
const int movable = DiagramContent::Elements | DiagramContent::TextFields
| DiagramContent::Images | DiagramContent::Shapes
| DiagramContent::Tables | DiagramContent::TerminalStrip;
for (QGraphicsItem *item : pasted.items(movable))
item->setPos(item->pos() + offset);
m_diagram->clearSelection();
m_diagram->undoStack().push(new PasteDiagramCommand(m_diagram, pasted));
adjustSceneRect();
}
/**
Manage the events press click :
* click to add an independent text field
@@ -1401,7 +1477,7 @@ void DiagramView::createTemplateFromSelection()
collection_node.appendChild(collection_elmt);
} else {
qDebug() << "Warnung: Konnte XML-Definition für" << old_type << "nicht laden.";
qDebug() << "Warning: could not load XML definition for" << old_type;
}
}
}
+1
View File
@@ -137,6 +137,7 @@ class DiagramView : public PaletteGraphicsView
void copy();
void paste(const QPointF & = QPointF(), QClipboard::Mode = QClipboard::Clipboard);
void pasteHere();
void duplicate(const QPoint &stepOffset);
void adjustSceneRect();
void updateWindowTitle();
void resetConductors();
@@ -17,6 +17,7 @@
*/
#include "elementpropertieseditorwidget.h"
#include "../../qet.h"
#include "../../qetapp.h"
#include "../../qetinformation.h"
#include "ui_elementpropertieseditorwidget.h"
@@ -185,13 +186,13 @@ void ElementPropertiesEditorWidget::upDateInterface()
const DiagramContext &info = m_data.m_informations;
ui->m_auto_num_locked_cb->setChecked(
info.value(QStringLiteral("auto_num_locked")).toString() == QLatin1String("true"));
QET::infoFlagIsTrue(info.value(QStringLiteral("auto_num_locked")).toString()));
ui->m_potential_isolating_cb->setChecked(
info.value(QStringLiteral("potential_isolating")).toString() == QLatin1String("true"));
QET::infoFlagIsTrue(info.value(QStringLiteral("potential_isolating")).toString()));
}
ui->m_exclude_from_bom_cb->setChecked(
m_data.m_informations.value(QStringLiteral("exclude_from_bom")).toString() == QLatin1String("true"));
QET::infoFlagIsTrue(m_data.m_informations.value(QStringLiteral("exclude_from_bom")).toString()));
on_m_base_type_cb_currentIndexChanged(ui->m_base_type_cb->currentIndex());
}
+20
View File
@@ -25,6 +25,8 @@
#include "../../qetapp.h"
#include "../../qetmainwindow.h"
#include "../../recentfiles.h"
#include "../../elementscollectioncache.h"
#include "../../factory/elementpicturefactory.h"
#include "../graphicspart/customelementpart.h"
#include "../elementitemeditor.h"
#include "../styleeditor.h"
@@ -348,6 +350,24 @@ bool QETElementEditor::toLocation(const ElementsLocation &location)
tr("Impossible d'enregistrer l'élément", "message box content"));
return(false);
}
//setXml() just wrote the new drawing to disk, but the preview shown
//in the elements panel comes from two caches keyed by path+uuid that
//nothing here has told about the change: ElementPictureFactory's
//in-memory picture cache and ElementsCollectionCache's on-disk
//SQLite cache (see ElementsLocation::icon()). locationWasSaved()
//(elementscollectionwidget.cpp) re-reads the icon right after this
//call returns, but both caches still hand back the pre-edit pixmap,
//so the panel keeps showing the stale preview until the whole
//collection is reloaded. Drop and rebuild them here.
ElementPictureFactory::instance()->dropCache(location);
if (ElementsCollectionCache *cache = QETApp::collectionCache()) {
if (cache->fetchData(location)) {
cache->cacheName(location.toString(), location.uuid());
cache->cachePixmap(location.toString(), location.uuid());
}
}
return(true);
}
+24 -3
View File
@@ -674,6 +674,27 @@ void ElementsPanelWidget::duplicateDiagram()
bool erase_labels = settings.value(
"diagramcommands/erase-label-on-copy", true).toBool();
// Resolve a linked pair duplicated together against each other
// (bugtracker #607) before the loop below renews their uuids or
// clears their pending links: at this exact moment a copy's
// tmp_uuids_link still holds its source's original partner
// uuid, which still equals the not-yet-renewed uuid of that
// partner's own copy if both were duplicated together. Scoped
// to this diagram's own copies, not a project-wide search, so
// this never links back to the source elements the copies were
// made from -- if only one half of a linked pair is here, its
// link entry simply finds no match and is dropped, same as
// clearPendingLinks() used to do unconditionally for every copy.
QList<Element *> new_elements;
for (QGraphicsItem *item : new_diagram->items()) {
if (Element *elmt = dynamic_cast<Element *>(item)) {
new_elements << elmt;
}
}
for (Element *elmt : new_elements) {
elmt->initLink(new_elements);
}
for (QGraphicsItem *item : new_diagram->items()) {
if (Element *elmt = dynamic_cast<Element *>(item)) {
// The XML round-trip kept the source elements' uuids. Give the
@@ -695,9 +716,9 @@ void ElementsPanelWidget::duplicateDiagram()
new_diagram->restoreText(elmt);
}
// Clear pending links so copies don't link back to
// the source elements via stale UUIDs.
elmt->clearPendingLinks();
// initLink() above already cleared tmp_uuids_link for
// every copy, matched or not -- nothing left here that
// could link back to a stale source uuid.
// Clean up copied element data:
// 1. Slaves always lose label/formula/comment/location
+7 -1
View File
@@ -32,8 +32,14 @@ class QetGraphicsTableFactory
static void createAndAddNomenclature(Diagram *diagram);
static void createAndAddSummary(Diagram *diagram);
private:
// Public so a caller that has already built and configured an
// AddTableDialog itself (never shown or exec'd -- the two
// methods above always exec() one, which the scripting API
// cannot use headlessly) can create a table from it directly.
// create() only reads settings already on the dialog; nothing
// about it depends on the dialog having been shown.
static void create(Diagram *diagram, AddTableDialog *dialog);
private:
static QetGraphicsTableItem *newTable(
Diagram *diagram,
AddTableDialog *dialog,
+30
View File
@@ -72,6 +72,15 @@ bool AddTableDialog::adjustTableToFolio() const
return ui->m_adjust_table_size_cb->isChecked();
}
/**
@brief AddTableDialog::setAdjustTableToFolio
@param set
*/
void AddTableDialog::setAdjustTableToFolio(bool set)
{
ui->m_adjust_table_size_cb->setChecked(set);
}
/**
@brief AddTableDialog::addNewTableToNewDiagram
@return
@@ -81,6 +90,15 @@ bool AddTableDialog::addNewTableToNewDiagram() const
return ui->m_add_table_and_folio->isChecked();
}
/**
@brief AddTableDialog::setAddNewTableToNewDiagram
@param set
*/
void AddTableDialog::setAddNewTableToNewDiagram(bool set)
{
ui->m_add_table_and_folio->setChecked(set);
}
/**
@brief AddTableDialog::tableName
@return
@@ -90,6 +108,18 @@ QString AddTableDialog::tableName() const
return ui->m_table_name_le->text();
}
/**
@brief AddTableDialog::setTableName
Set the name field directly, so a caller that builds this dialog to
read from (never shows or execs it -- the scripting API's addTable())
does not need a name typed by a user who was never there to type one.
@param name
*/
void AddTableDialog::setTableName(const QString &name)
{
ui->m_table_name_le->setText(name);
}
/**
@brief AddTableDialog::headerMargins
@return
+3
View File
@@ -44,9 +44,12 @@ class AddTableDialog : public QDialog
void setQueryWidget(QWidget *widget);
bool adjustTableToFolio() const;
void setAdjustTableToFolio(bool set);
bool addNewTableToNewDiagram() const;
void setAddNewTableToNewDiagram(bool set);
QString tableName() const;
void setTableName(const QString &name);
QMargins headerMargins() const;
Qt::Alignment headerAlignment() const;
+32
View File
@@ -22,6 +22,8 @@
#include "logging/eventloopwatchdog.h"
#include "logging/qetlogger.h"
#include "machine_info.h"
#include "diagram.h"
#include "palettegraphicsview.h"
#include "qet.h"
#include "qetapp.h"
#include "qetmessagebox.h"
@@ -31,6 +33,7 @@
#include <QApplication>
#include <QDomImplementation>
#include <QFont>
#include <QStyleFactory>
#include <QtConcurrentRun>
@@ -105,6 +108,19 @@ int main(int argc, char **argv)
// from Qt 6.12 on; opt in explicitly for older Qt 5/6.
QDomImplementation::setInvalidDataPolicy(
QDomImplementation::ReturnNullNode);
#ifdef Q_OS_WIN
// "MS Shell Dlg 2" is not a font but a Windows alias, and many projects
// and settings saved on Windows carry it. Qt 5's GDI font backend let
// Windows resolve it to Tahoma; Qt 6's DirectWrite backend does not know
// the alias and falls back to Arial, so those texts come out heavier on
// screen and in exported PDFs (bugtracker #340). Resolve both aliases
// the way Windows does. Done before any application object exists so
// that the headless export and scripting runs below get it too.
QFont::insertSubstitution("MS Shell Dlg 2", "Tahoma");
QFont::insertSubstitution("MS Shell Dlg", "Microsoft Sans Serif");
#endif
//Creation and execution of the application
//HighDPI
qputenv("QT_ENABLE_HIGHDPI_SCALING", "1");
@@ -144,6 +160,22 @@ int main(int argc, char **argv)
#endif
}
// Re-apply the sheet background last picked in the diagram editor, so
// every project opened from here on -- existing or new, whichever one
// it is -- draws that background instead of the built-in default that
// would otherwise force the user to pick it again after each start.
//
// Done here rather than in main()'s first lines on purpose: the
// headless export and scripting runs above return before reaching
// this point and must keep rendering on plain white. It also has to
// happen before QETApp is constructed below, since that constructor
// already loads the projects given on the command line.
{
const QetSettings::SheetBackground sheet_background = QetSettings::sheetBackground();
PaletteGraphicsView::setCustomBackgroundColor(sheet_background.custom);
Diagram::background_color = sheet_background.color;
}
// Resolve the logger's state (log directory, session filename, open
// file handle) explicitly here, immediately before installing the
// handler -- not implicitly on whichever thread happens to log
+24
View File
@@ -20,6 +20,7 @@
#include "qeticons.h"
#include "shortcutmanager.h"
#include <cmath>
#include <limits>
#include <QBuffer>
#include <QColorDialog>
@@ -244,10 +245,33 @@ bool QET::attributeIsAReal(
bool ok;
qreal tmp = e.attribute(nom_attribut).toDouble(&ok);
if (!ok) return(false);
// QString::toDouble() sets ok=true for "nan"/"inf"/"-inf" -- these
// parse successfully but are not usable coordinates. A non-finite
// element/terminal position reaches Conductor::shape() during load
// and hangs there at 100% CPU inside QPainterPathStroker::createStroke(),
// confirmed with gdb: not a blocked wait, genuine unbounded computation.
if (!std::isfinite(tmp)) return(false);
if (reel != nullptr) *reel = tmp;
return(true);
}
/**
@brief QET::infoFlagIsTrue
@see the header comment for why this exists rather than a bare
== "true" comparison.
@param value the raw elementInformations string to test
@return true if @p value, trimmed and case-folded, is one of the
truthy spellings this codebase already accepts elsewhere
*/
bool QET::infoFlagIsTrue(const QString &value)
{
const QString v = value.trimmed().toLower();
return v == QLatin1String("true")
|| v == QLatin1String("1")
|| v == QLatin1String("yes")
|| v == QLatin1String("on");
}
/**
@brief QET::ElementsAndConductorsSentence
Permet de composer rapidement la proposition "x elements et y conducteurs"
+16
View File
@@ -161,6 +161,22 @@ namespace QET {
bool orthogonalProjection(const QPointF &, const QLineF &, QPointF * = nullptr);
bool attributeIsAnInteger(const QDomElement &, const QString& , int * = nullptr);
bool attributeIsAReal(const QDomElement &, const QString& , qreal * = nullptr);
/**
Whether an elementInformations flag (auto_num_locked,
potential_isolating, exclude_from_bom, ...) counts as "on".
Case-insensitive and tolerant of surrounding whitespace, and
accepts the same set of truthy spellings ("true", "1", "yes",
"on") that element_nomenclature_view's SQL predicate for
exclude_from_bom already does -- see
projectDataBase::createElementNomenclatureView(). These flags
are only ever written by this app's own checkboxes as literal
"true"/"false" today, but a bare == "true" comparison silently
treats anything else -- "True", "TRUE", a trailing space from
a hand-edited file, a value some other tool wrote -- as off,
with no error and no visible difference from the checkbox
being genuinely unticked (discussion #785).
*/
bool infoFlagIsTrue(const QString &value);
QString ElementsAndConductorsSentence(int elements=0,
int conductors=0,
int indi_texts=0,
+94 -4
View File
@@ -167,9 +167,7 @@ QETApp::QETApp() :
if (m_splash_screen) {
m_splash_screen -> hide();
}
checkBackupFiles();
#ifdef QET_SPACEMOUSE_SUPPORT
//Always safe to construct: it silently does nothing when spacenavd
//isn't running or no device is attached, which is the common case
@@ -177,6 +175,17 @@ QETApp::QETApp() :
//SpaceMouseListener's class comment.
m_space_mouse_listener = new SpaceMouseListener(this);
#endif
//Deferred so this constructor returns before the prompts appear.
//checkBackupFiles() opens modal dialogs, and main() still has work to
//do once we return -- in particular connecting
//SingleApplication::receivedMessage to receiveMessage(). While those
//prompts were up that connection did not exist yet, so a file handed
//to the already-running instance during start-up was accepted by the
//socket and then dropped on the floor.
QMetaObject::invokeMethod(this, [this]() {
checkBackupFiles();
}, Qt::QueuedConnection);
}
/**
@@ -1828,6 +1837,81 @@ void QETApp::useSystemPalette(bool use) {
QET::Palette::refreshStyleSheets();
}
/**
@brief QETApp::useCustomPalette
Apply a user-chosen color as the application-wide palette.
Builds a full QPalette from \a color, keeping the system palette
as a fallback for roles we don't touch.
@param color the user-chosen base color
*/
void QETApp::useCustomPalette(const QColor &color) {
if (!color.isValid())
return;
// Derive readable text colors from the chosen color.
const bool dark = color.lightness() < 128;
const QColor text = dark ? QColor(220, 220, 220) : QColor(30, 30, 30);
const QColor disabled_text = dark ? QColor(175, 175, 175) : QColor(128, 128, 128);
// Slightly lighter/darker for button and window shading.
QColor button = color;
button = QColor::fromHslF(color.hslHueF(),
color.hslSaturationF(),
dark ? qMin(color.lightnessF() + 0.08, 1.0)
: qMax(color.lightnessF() - 0.08, 0.0));
QColor light = QColor::fromHslF(color.hslHueF(),
color.hslSaturationF(),
dark ? qMin(color.lightnessF() + 0.15, 1.0)
: qMax(color.lightnessF() - 0.15, 0.0));
QColor mid = QColor::fromHslF(color.hslHueF(),
color.hslSaturationF(),
dark ? qMin(color.lightnessF() + 0.04, 1.0)
: qMax(color.lightnessF() - 0.04, 0.0));
QColor dark_c = QColor::fromHslF(color.hslHueF(),
color.hslSaturationF(),
dark ? qMin(color.lightnessF() - 0.04, 1.0)
: qMax(color.lightnessF() - 0.12, 0.0));
QColor shadow = QColor::fromHslF(color.hslHueF(),
color.hslSaturationF(),
dark ? qMin(color.lightnessF() - 0.10, 1.0)
: qMax(color.lightnessF() - 0.20, 0.0));
QPalette p;
// Active and Inactive get the same colors; only Disabled differs.
for (auto group : {QPalette::Active, QPalette::Inactive}) {
p.setColor(group, QPalette::Window, color);
p.setColor(group, QPalette::WindowText, text);
p.setColor(group, QPalette::Base, color);
p.setColor(group, QPalette::AlternateBase, button);
p.setColor(group, QPalette::Text, text);
p.setColor(group, QPalette::Button, button);
p.setColor(group, QPalette::ButtonText, text);
p.setColor(group, QPalette::BrightText, dark ? QColor(255,90,90) : Qt::white);
p.setColor(group, QPalette::Highlight, QColor(30, 96, 176));
p.setColor(group, QPalette::HighlightedText, Qt::white);
p.setColor(group, QPalette::ToolTipBase, button);
p.setColor(group, QPalette::ToolTipText, text);
p.setColor(group, QPalette::Light, light);
p.setColor(group, QPalette::Midlight, mid);
p.setColor(group, QPalette::Mid, mid);
p.setColor(group, QPalette::Dark, dark_c);
p.setColor(group, QPalette::Shadow, shadow);
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
p.setColor(group, QPalette::Accent, QColor(30, 96, 176));
#endif
}
p.setColor(QPalette::Disabled, QPalette::WindowText, disabled_text);
p.setColor(QPalette::Disabled, QPalette::Text, disabled_text);
p.setColor(QPalette::Disabled, QPalette::ButtonText, disabled_text);
qApp->setPalette(p);
qApp->setStyleSheet(QString());
// Switch icon theme to match light/dark.
applyIconTheme(p);
QET::Palette::refreshStyleSheets();
}
/**
@brief QETApp::quitQET
Request the closing of all windows;
@@ -2419,7 +2503,13 @@ void QETApp::initStyle()
//Apply or not the system style
QSettings settings;
useSystemPalette(settings.value("usesystemcolors", true).toBool());
if (settings.value("usesystemcolors", true).toBool()) {
useSystemPalette(true);
} else if (settings.contains("customapplicationcolor")) {
useCustomPalette(QColor(settings.value("customapplicationcolor").toString()));
} else {
useSystemPalette(false);
}
#if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
// Setting an application palette stops Qt from following the OS
+1
View File
@@ -276,6 +276,7 @@ class QETApp : public QObject
void setMainWindowVisible(QMainWindow *, bool);
void invertMainWindowVisibility(QWidget *);
void useSystemPalette(bool);
void useCustomPalette(const QColor &color);
void quitQET();
void checkRemainingWindows();
void openFiles(const QETArguments &);
+82 -1
View File
@@ -52,11 +52,13 @@
#include "ui/bomexportdialog.h"
#include "ui/conductorcolortoolbutton.h"
#include "ui/diagrambgcolorbutton.h"
#include "ui/duplicateoffsetdialog.h"
#include "ui/jumptoelementdialog.h"
#include "ui/diagrampropertieseditordockwidget.h"
#include "ui/backupdialog.h"
#include "ui/dialogwaiting.h"
#include "undocommand/addelementtextcommand.h"
#include "utils/qetsettings.h"
#include "utils/qetutils.h"
#include "undocommand/rotateselectioncommand.h"
#include "undocommand/rotatetextscommand.h"
@@ -374,6 +376,46 @@ void QETDiagramEditor::setUpActions()
new DiagramEventAddPaste(dv->diagram(), start_pos));
});
//Duplicate: copy the selection and place it at a configured,
//grid-step offset immediately -- no interactive follow-the-
//cursor step, unlike Ctrl+V above. That is deliberate (#991):
//the point of a duplicate shortcut is repeatable, unattended
//stamping (configure the offset once, then tap Ctrl+D to lay
//out a row), which an interactive placement would interrupt on
//every press.
m_duplicate = new QAction(QET::Icons::EditCopy, tr("Dupli&quer"), this);
ShortcutManager::instance().registerAction(m_duplicate, "diagrameditor.duplicate", tr("Éditeur de schémas"), Qt::CTRL | Qt::Key_D);
m_duplicate->setStatusTip(tr("Copie la sélection, décalée de l'espacement configuré", "status bar tip"));
connect(m_duplicate, &QAction::triggered, [this]() {
auto *dv = currentDiagramView();
if (!dv || !dv->diagram()) return;
//Ask the first time only -- every later press reuses whatever
//was confirmed then, so the shortcut can be tapped repeatedly
//without an interruption each time. m_configure_duplicate
//below is the deliberate way back into this dialog.
if (!DuplicateOffsetDialog::hasSavedStepOffset()) {
DuplicateOffsetDialog dialog(this);
if (dialog.exec() != QDialog::Accepted) return;
DuplicateOffsetDialog::saveStepOffset(dialog.stepOffset());
}
dv->duplicate(DuplicateOffsetDialog::savedStepOffset());
});
//Reopens the dialog above on demand, to change the spacing or
//direction a later Ctrl+D should use. Enabled unconditionally
//(see slot_updateComplexActions()): it only ever writes a
//setting, so it does not need a diagram open or anything
//selected the way m_duplicate itself does.
m_configure_duplicate = new QAction(tr("Configurer la duplication..."), this);
m_configure_duplicate->setStatusTip(tr("Choisir l'espacement et la direction utilisés par Dupliquer", "status bar tip"));
connect(m_configure_duplicate, &QAction::triggered, [this]() {
DuplicateOffsetDialog dialog(this);
if (dialog.exec() == QDialog::Accepted) {
DuplicateOffsetDialog::saveStepOffset(dialog.stepOffset());
}
});
//Reset conductor path
m_conductor_reset = new QAction(QET::Icons::ConductorSettings, tr("Réinitialiser les conducteurs"), this);
ShortcutManager::instance().registerAction(m_conductor_reset, "diagrameditor.conductor_reset", tr("Éditeur de schémas"), Qt::CTRL | Qt::Key_K);
@@ -894,6 +936,7 @@ void QETDiagramEditor::setUpToolBar()
main_tool_bar -> addAction(m_cut);
main_tool_bar -> addAction(m_copy);
main_tool_bar -> addAction(m_paste);
main_tool_bar -> addAction(m_duplicate);
main_tool_bar -> addSeparator();
main_tool_bar -> addAction(m_delete_selection);
main_tool_bar -> addAction(m_rotate_selection);
@@ -985,6 +1028,8 @@ void QETDiagramEditor::setUpMenu()
menu_edition -> addAction(m_cut);
menu_edition -> addAction(m_copy);
menu_edition -> addAction(m_paste);
menu_edition -> addAction(m_duplicate);
menu_edition -> addAction(m_configure_duplicate);
menu_edition -> addSeparator();
//The same actions the "Ajouter" toolbar holds. They were toolbar-only,
//which left them unreachable for anyone working without a mouse: a
@@ -1811,7 +1856,18 @@ void QETDiagramEditor::selectionGroupTriggered(QAction *action)
diagram->undoStack().push(c);
}
else if (value == "rotate_selected_text")
diagram->undoStack().push(new RotateTextsCommand(diagram));
{
//Ask for the angle first, then build the command: the command
//itself no longer opens a dialog. Guarding on the selection keeps
//the previous behaviour of showing no dialog when there is
//nothing to rotate.
if (RotateTextsCommand::hasSelectedTexts(diagram))
{
qreal rotation = 0;
if (RotateTextsCommand::askRotation(rotation))
diagram->undoStack().push(new RotateTextsCommand(diagram, rotation));
}
}
else if (value == "find_selected_element" && currentElement())
findElementInPanel(currentElement()->location());
else if (value == "edit_selected_element")
@@ -1947,6 +2003,7 @@ void QETDiagramEditor::slot_updateComplexActions()
<< m_find_element
<< m_cut
<< m_copy
<< m_duplicate
<< m_delete_selection
<< m_rotate_selection
<< m_rotate_group_selection
@@ -1976,6 +2033,7 @@ void QETDiagramEditor::slot_updateComplexActions()
bool deletable_items = dc.hasDeletableItems();
m_cut -> setEnabled(!ro && copiable_items);
m_copy -> setEnabled(copiable_items);
m_duplicate -> setEnabled(!ro && copiable_items);
m_delete_selection -> setEnabled(!ro && deletable_items);
m_rotate_selection -> setEnabled(!ro && diagram_->canRotateSelection());
m_rotate_group_selection -> setEnabled(!ro && diagram_->canRotateSelection());
@@ -3122,6 +3180,29 @@ void QETDiagramEditor::slot_runScript() {
QETProject *project = currentProject();
if (!project) return;
// Scripting is off until somebody says otherwise, so the first use has
// to ask. Asking here rather than greying the action out keeps the
// feature discoverable: a disabled menu entry tells a user that
// something exists and nothing about how to have it.
if (!QetSettings::scriptingEnabled()) {
const QMessageBox::StandardButton answer = QET::QetMessageBox::question(
this,
tr("Exécuter un script"),
tr("Les scripts sont désactivés.\n\n"
"Un script s'exécute avec vos droits : il peut lire et "
"modifier le projet ouvert et écrire des fichiers. "
"N'exécutez que des scripts dont vous connaissez "
"l'origine.\n\n"
"Activer les scripts ? Ce réglage est modifiable dans "
"Configurer QElectroTech > Général > Projets."),
QMessageBox::Yes | QMessageBox::Cancel,
QMessageBox::Cancel);
if (answer != QMessageBox::Yes) {
return;
}
QetSettings::setScriptingEnabled(true);
}
const QString script_path = QFileDialog::getOpenFileName(
this,
tr("Exécuter un script"),
+2
View File
@@ -199,6 +199,8 @@ class QETDiagramEditor : public QETMainWindow
*undo, ///< Cancel the latest action
*redo, ///< Redo the latest cancelled operation
*m_paste, ///< Paste clipboard content on the current diagram
*m_duplicate, ///< Copy selection, offset by the configured step (#991)
*m_configure_duplicate, ///< Reopen the duplicate offset/direction dialog (#991)
*m_auto_conductor, ///< Enable/Disable the use of auto conductor
*m_auto_break_conductor, ///< Enable/Disable the use of auto break conductor
*m_draw_grid, ///< Switch the background grid display or not
@@ -379,6 +379,21 @@ void ProjectDBModel::fillValue()
while (query_.next())
{
//This query text comes out of the project file, so its result is
//not bounded by anything the project actually contains: a
//recursive CTE produces rows for as long as anyone reads them.
//Without this, opening such a file hangs QElectroTech at 100% CPU
//while m_record grows until memory runs out. @see
//projectDataBase::MaxResultRows.
if (m_record.size() >= projectDataBase::MaxResultRows) {
qWarning().noquote()
<< "ProjectDBModel: query stopped after"
<< projectDataBase::MaxResultRows
<< "rows, which is far more than a folio table can show."
<< "The table is incomplete. Query:" << m_query;
break;
}
QStringList record_;
auto i=0;
while (query_.value(i).isValid())
+28
View File
@@ -1310,6 +1310,34 @@ const QList<ConductorSegment *> Conductor::segmentsList() const
return(segments_vector);
}
/**
@brief Conductor::moveSegment
Move one segment of this conductor by (dx, dy), the same primitive
handlerMouseMoveEvent()/handlerMouseReleaseEvent() apply on a manual
drag -- moveX()/moveY() each silently no-op on the wrong axis or a
static (terminal-anchored) segment, so both are always called and
whichever applies takes effect. Unlike a drag this commits the whole
move as a single undo step.
@param index a segmentsList() index
@param dx @param dy the movement, in the diagram's own coordinates
@return false if index is out of range
*/
bool Conductor::moveSegment(int index, qreal dx, qreal dy)
{
const QList<ConductorSegment *> segs = segmentsList();
if (index < 0 || index >= segs.count()) return false;
before_mov_text_pos_ = m_text_item->pos();
ConductorSegment *seg = segs.at(index);
seg->moveX(dx);
seg->moveY(dy);
modified_path = true;
segmentsToPath();
calculateTextItemPosition();
saveProfile();
return true;
}
/**
@brief Conductor::length
@return the length of this conductor
+1
View File
@@ -114,6 +114,7 @@ class Conductor : public QGraphicsObject
public:
QVector <QPointF> handlerPoints() const;
const QList<ConductorSegment *> segmentsList() const;
bool moveSegment(int index, qreal dx, qreal dy);
void setPropertyToPotential(
const ConductorProperties &property,
@@ -33,6 +33,7 @@
#include <QDomElement>
#include <QtCore/qnumeric.h>
#include <QGraphicsSceneMouseEvent>
#include <QAbstractTextDocumentLayout>
/**
@brief DynamicElementTextItem::DynamicElementTextItem
@@ -732,19 +733,6 @@ void DynamicElementTextItem::paint(QPainter *painter, const QStyleOptionGraphics
{
DiagramTextItem::paint(painter, option, widget);
//Only ever repositions already-existing sibling items here --
//never adds or removes one. paint() runs while QGraphicsScene is
//iterating its item list to draw it, and mutating that list mid
//-iteration (which addResizeHandles()/removeResizeHandles() do,
//through QGraphicsScene::addItem()/removeItem()) crashes. An
//earlier version of this fix called them from here and crashed
//qelectrotech reproducibly on deselecting a text (SIGABRT); see
//refreshResizeHandlesVisibility() for where that now happens
//instead -- itemChange(), Qt's own safe hook for exactly this,
//already used below for this item's own selection.
if (m_left_resize_handle || m_right_resize_handle)
updateResizeHandlesPos();
if (m_frame)
{
painter->save();
@@ -927,11 +915,17 @@ void DynamicElementTextItem::addResizeHandles()
for (QetGraphicsHandlerItem *handle : {m_left_resize_handle, m_right_resize_handle})
{
scene()->addItem(handle);
//Children of this text, not free scene items: Qt then carries
//them along when the parent element moves, rotates or is
//zoomed, and repaints their old and new area in the same
//update as this text. Moving free items from paint() instead
//left green fragments behind (qelectrotech#1002).
handle->setParentItem(this);
handle->setColor(Qt::darkGreen);
handle->setZValue(zValue() + 1);
handle->installSceneEventFilter(this);
}
m_resize_handles_con = connect(document()->documentLayout(), &QAbstractTextDocumentLayout::documentSizeChanged,
this, &DynamicElementTextItem::updateResizeHandlesPos);
updateResizeHandlesPos();
}
@@ -941,6 +935,7 @@ void DynamicElementTextItem::addResizeHandles()
*/
void DynamicElementTextItem::removeResizeHandles()
{
disconnect(m_resize_handles_con);
delete m_left_resize_handle;
delete m_right_resize_handle;
m_left_resize_handle = nullptr;
@@ -973,8 +968,8 @@ void DynamicElementTextItem::updateResizeHandlesPos()
return;
QRectF br = boundingRect();
m_left_resize_handle->setPos(mapToScene(QPointF(br.left(), br.center().y())));
m_right_resize_handle->setPos(mapToScene(QPointF(br.right(), br.center().y())));
m_left_resize_handle->setPos(br.left(), br.center().y());
m_right_resize_handle->setPos(br.right(), br.center().y());
}
/**
@@ -200,6 +200,7 @@ class DynamicElementTextItem : public DiagramTextItem
qreal m_resize_original_width = -1;
qreal m_resize_baseline_width = -1;
qreal m_resize_start_local_x = 0;
QMetaObject::Connection m_resize_handles_con;
};
#endif // DYNAMICELEMENTTEXTITEM_H
+44
View File
@@ -39,7 +39,9 @@
#include "elementtextitemgroup.h"
#include "iostream"
#include <QApplication>
#include <QCollator>
#include <QScreen>
static const QString plcTerminalKeys[] = {
QETInformation::ELMT_PLC_T1,
@@ -209,6 +211,21 @@ void Element::editProperty()
//with the "text" tab of ElementPropertiesWidget,
//the ui freeze, until user press escape key
dialog.setWindowModality(Qt::WindowModal);
// A PLC master carries a 6-column IO table: without an explicit
// size the dialog falls back to its (cramped) sizeHint, so open it
// at three times its natural width instead. The height stays at
// the natural one, and the width never exceeds the screen.
const ElementData data = elementData();
if (data.m_type == ElementData::Master
&& data.m_master_type == ElementData::PLC) {
const QSize natural = dialog.sizeHint();
int width = natural.width() * 3;
if (QScreen *screen = QApplication::primaryScreen())
width = qMin(width, screen->availableGeometry().width());
dialog.resize(width, natural.height());
}
dialog.exec();
}
}
@@ -1369,6 +1386,33 @@ void Element::initLink(QETProject *prj)
tmp_uuids_link.clear();
}
/**
@brief Element::initLink
Overload resolving tmp_uuids_link against @p candidates instead of a
project-wide ElementProvider search -- see the header comment for
why the search has to be scoped this way right after a paste or
folio-duplication XML round-trip, before uuids are renewed.
@param candidates the elements to search for a link partner in
*/
void Element::initLink(const QList<Element *> &candidates)
{
// if nothing to link return now
if (tmp_uuids_link.isEmpty()) return;
for (int i = 0; i < tmp_uuids_link.size(); ++i) {
for (Element *elmt : candidates) {
if (elmt->uuid() == tmp_uuids_link[i].uuid) {
elmt->linkToElement(this);
if (tmp_uuids_link[i].group_index >= 0) {
m_group_index_map[elmt] = tmp_uuids_link[i].group_index;
}
break;
}
}
}
tmp_uuids_link.clear();
}
/**
* @brief Element::linkTypeToString
* \deprecated use instead ElementData::typeToString
+19
View File
@@ -207,6 +207,25 @@ class Element : public QetGraphicsItem
virtual void unlinkAllElements() {}
virtual void unlinkElement(Element *) {}
virtual void initLink(QETProject *);
/**
Resolve tmp_uuids_link against a caller-supplied candidate
list instead of a project-wide search (bugtracker #607).
Used right after an XML round-trip (paste, folio
duplication) and before the pasted/duplicated elements'
uuids are renewed: at that moment a copy's tmp_uuids_link
still holds its source's original partner uuid, which
still matches the not-yet-renewed uuid of that partner's
own copy if it was carried along in the same batch.
Resolving only within @p candidates -- not the whole
project -- is what stops a linked pair pasted together
from matching an original element left elsewhere that
happens to still carry that same soon-to-be-replaced
uuid. If only one half of a linked group is in
@p candidates, its entry finds no match and is dropped,
same as initLink(QETProject *) leaving an unresolvable
link unlinked.
*/
void initLink(const QList<Element *> &candidates);
QList<Element *> linkedElements ();
int groupIndexForElement(Element *elmt) const;
+2 -1
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "../qetgraphicsitem/terminal.h"
#include "../qet.h"
#include "../qetproject.h"
#include "../conductorautonumerotation.h"
#include "../diagram.h"
@@ -986,7 +987,7 @@ QList<Terminal *> relatedPotentialTerminal (
else if (terminal -> parentElement() -> linkType() & Element::Terminale)
{
// English: Check if the user activated the potential isolation checkbox for this terminal
if (terminal->parentElement()->elementInformations().value(QStringLiteral("potential_isolating")).toString() == QLatin1String("true")) {
if (QET::infoFlagIsTrue(terminal->parentElement()->elementInformations().value(QStringLiteral("potential_isolating")).toString())) {
// English: Potential is isolated. Return an empty list so it does not propagate to the other side.
return QList<Terminal *>();
}
+8
View File
@@ -20,7 +20,9 @@
#include <QApplication>
#include <QColor>
#include <QImage>
#include <QMdiArea>
#include <QStyle>
#include <QTabBar>
#include <QWidget>
#include <cmath>
@@ -307,4 +309,10 @@ void QET::Palette::refreshStyleSheets()
for (QWidget *widget : widgets)
if (!widget->styleSheet().isEmpty())
widget->setStyleSheet(widget->styleSheet());
// Force an immediate repaint on tab bars and MDI areas so their text
// updates together with the rest of the UI, not one event loop later.
for (QWidget *widget : widgets)
if (qobject_cast<QTabBar *>(widget) || qobject_cast<QMdiArea *>(widget))
widget->update();
}
+165 -1
View File
@@ -22,7 +22,9 @@
#include "autoNum/assignvariables.h"
#include "autoNum/numerotationcontext.h"
#include "autoNum/numerotationcontextcommands.h"
#include "autoNum/renumberelementscommand.h"
#include "diagram.h"
#include "qetgraphicsitem/element.h"
#include "qetapp.h"
#include "qetmessagebox.h"
#include "qetresult.h"
@@ -40,7 +42,30 @@
#include <QHash>
#include <QTimer>
#include <QtConcurrentRun>
namespace {
/**
* @brief Reset numeric fields of a NumerotationContext so renumbering starts at 1.
* Keeps non-numeric parts (string/idfolio/folio/plant/locmach/elementline/elementcolumn/elementprefix) unchanged.
*/
NumerotationContext resetContextForRenumber(const NumerotationContext &tmpl)
{
NumerotationContext out = tmpl;
for (int i = 0; i < out.size(); ++i) {
const QStringList parts = out.itemAt(i);
if (parts.isEmpty()) continue;
const QString type = parts.at(0);
if (out.keyIsNumber(type)) {
out.replaceValue(i, QStringLiteral("1"));
}
}
return out;
}
} // namespace
#include <QtDebug>
#include <algorithm>
#include <utility>
static int BACKUP_INTERVAL = 1200000; //interval in ms of backup = 20min
@@ -132,6 +157,9 @@ QETProject::QETProject(const QString &path, QObject *parent) :
return;
}
//The file just read already holds everything a crash could lose, so
//there is nothing to back up until the project is changed.
m_backup_needed = false;
init();
}
@@ -258,6 +286,19 @@ void QETProject::init()
m_undo_stack = new QUndoStack(this);
connect(m_undo_stack, &QUndoStack::cleanChanged, this, &QETProject::undoStackChanged);
//What counts as a change for writeBackup(): the undo stack moving,
//setModified(true), and the embedded collections, which can change
//without going through either.
const auto backup_needed = [this]() { m_backup_needed = true; };
connect(m_undo_stack, &QUndoStack::indexChanged, this, backup_needed);
connect(&m_titleblocks_collection, &TitleBlockTemplatesCollection::changed, this, backup_needed);
connect(&m_titleblocks_collection, &TitleBlockTemplatesCollection::aboutToRemove, this, backup_needed);
connect(m_elements_collection, &XmlElementCollection::elementAdded, this, backup_needed);
connect(m_elements_collection, &XmlElementCollection::elementChanged, this, backup_needed);
connect(m_elements_collection, &XmlElementCollection::elementRemoved, this, backup_needed);
connect(m_elements_collection, &XmlElementCollection::directorieAdded, this, backup_needed);
connect(m_elements_collection, &XmlElementCollection::directoryRemoved, this, backup_needed);
m_save_backup_timer.setInterval(BACKUP_INTERVAL);
connect(&m_save_backup_timer, &QTimer::timeout, this, &QETProject::writeBackup);
m_save_backup_timer.start();
@@ -334,7 +375,18 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
//file without a persisted uuid derives its uuid from them.
const QByteArray content = file->readAll();
QDomDocument xml_project;
if (!xml_project.setContent(content))
// PreserveSpacingOnlyNodes: without it, a text node that is entirely
// whitespace -- e.g. a title-block custom variable deliberately set to
// a single space, the only way to give it a value other than blank
// (bugtracker #973) -- is silently dropped by Qt's default parsing,
// and QDomElement::text() then returns "" for it exactly as if it had
// never been set. Confirmed in isolation: <a> </a> parses to text()=="",
// this option makes it text()==" ". Every place in this codebase that
// walks a QDomNode's children already filters on isElement() (see
// QET::findInDomElement()), so the extra whitespace-only text nodes
// this keeps around are inert everywhere but the two elements that
// call .text() on themselves -- which is exactly where the bug was.
if (!xml_project.setContent(content, QDomDocument::ParseOption::PreserveSpacingOnlyNodes))
{
if(opened_here) {
file->close();
@@ -782,6 +834,109 @@ void QETProject::setCurrrentElementAutonum(QString autoNum) {
m_current_element_autonum = std::move(autoNum);
}
/**
@brief QETProject::renumberElementsBySchemeTitle
Renumber existing elements by element autonumbering scheme title.
Elements do not store the scheme title; they store a "formula" (elementInformations["formula"]).
This method matches elements to schemes by comparing the stored formula with the formula derived
from each scheme's NumerotationContext.
If scheme_title is empty, all schemes are renumbered. Otherwise only that scheme is renumbered.
The operation is undoable.
*/
void QETProject::renumberElementsBySchemeTitle(const QString &scheme_title)
{
if (!m_undo_stack) return;
if (isReadOnly()) return;
// Build map: scheme title -> canonical formula
QHash<QString, QString> scheme_formula;
for (const QString &k : m_element_autonum.keys()) {
if (!scheme_title.isEmpty() && k != scheme_title) continue;
scheme_formula.insert(k, autonum::numerotationContextToFormula(m_element_autonum.value(k)));
}
if (scheme_formula.isEmpty()) return;
// Collect elements per scheme by formula match
QHash<QString, QVector<Element*>> by_key;
for (Diagram *d : diagrams()) {
if (!d) continue;
const auto items = d->items();
for (QGraphicsItem *it : items) {
auto *el = qgraphicsitem_cast<Element*>(it);
if (!el) continue;
if (el->linkType() == Element::Slave || (el->linkType() & Element::AllReport))
continue;
const QString el_formula = el->elementInformations().value(QStringLiteral("formula")).toString();
if (el_formula.isEmpty()) continue;
QString matched_key;
for (auto itf = scheme_formula.constBegin(); itf != scheme_formula.constEnd(); ++itf) {
if (itf.value() == el_formula) { matched_key = itf.key(); break; }
}
if (matched_key.isEmpty()) continue;
by_key[matched_key].append(el);
}
}
if (by_key.isEmpty()) return;
QVector<RenumberElementsCommand::ElementChange> changes;
QHash<QString, NumerotationContext> old_ctx;
QHash<QString, NumerotationContext> new_ctx;
for (auto it = by_key.constBegin(); it != by_key.constEnd(); ++it) {
old_ctx.insert(it.key(), m_element_autonum.value(it.key()));
}
for (auto it = by_key.begin(); it != by_key.end(); ++it) {
const QString key = it.key();
auto &elements = it.value();
std::sort(elements.begin(), elements.end(), [](Element *a, Element *b){ return comparPos(a, b); });
NumerotationContext base_tmpl = m_element_autonum.value(key);
NumerotationContext nc = resetContextForRenumber(base_tmpl);
NumerotationContextCommands ncc(nc);
for (Element *el : elements) {
RenumberElementsCommand::ElementChange ch;
ch.element = el;
ch.old_infos = el->elementInformations();
ch.old_seq = el->sequenceStruct();
ch.old_frozen = el->isFreezeLabel();
ch.new_frozen = ch.old_frozen; // preserve frozen state
const QString formula = ch.old_infos.value(QStringLiteral("formula")).toString();
autonum::sequentialNumbers new_seq;
new_seq.clear();
autonum::setSequential(formula, new_seq, nc, el->diagram(), key);
DiagramContext new_infos = ch.old_infos;
new_infos.addValue(QStringLiteral("label"), autonum::AssignVariables::formulaToLabel(formula, new_seq, el->diagram(), el, nullptr));
ch.new_infos = new_infos;
ch.new_seq = new_seq;
changes.append(ch);
// advance
nc = ncc.next();
ncc = NumerotationContextCommands(nc);
}
new_ctx.insert(key, nc);
}
if (changes.isEmpty()) return;
auto *cmd = new RenumberElementsCommand(
this,
changes,
old_ctx,
new_ctx,
scheme_title.isEmpty() ? tr("Renumber elements") : tr("Renumber elements (%1)").arg(scheme_title));
m_undo_stack->push(cmd);
}
/**
@brief QETProject::conductorAutoNumFormula
@param key : autonum title
@@ -1553,6 +1708,9 @@ void QETProject::diagramOrderChanged(int old_index, int new_index) {
Mark this project as modified and emit the projectModified() signal.
*/
void QETProject::setModified(bool modified) {
if (modified) {
m_backup_needed = true;
}
if (m_modified != modified) {
m_modified = modified;
emit projectModified(this, m_modified);
@@ -2135,6 +2293,12 @@ void QETProject::writeBackup()
//both would write through &m_backup_file on different threads.
if (m_backup_future.isRunning())
return;
//toXml() walks the whole project on the GUI thread, which freezes
//big projects for seconds (bugtracker #273, #329). A backup of an
//unchanged project would be identical to the last one, so skip it.
if (!m_backup_needed)
return;
m_backup_needed = false;
//Capture the document by value (implicitly shared, so cheap): the
//Qt5-style QtConcurrent::run(function, reference-args) call did not
//survive the Qt6 API change, a lambda behaves identically on both.
+16
View File
@@ -169,6 +169,20 @@ class QETProject : public QObject
QString elementCurrentAutoNum() const;
void setCurrrentElementAutonum(QString autoNum);
/**
* @brief Renumber existing elements by element autonumbering scheme.
*
* Elements do not store the scheme title but they store the corresponding formula.
* This operation matches elements to schemes by comparing the stored formula
* with the formula derived from the scheme's NumerotationContext.
*
* If @p scheme_title is empty, all schemes are renumbered.
* If @p scheme_title is non-empty, only that scheme is renumbered.
*
* The operation is undoable.
*/
void renumberElementsBySchemeTitle(const QString &scheme_title = QString());
//Element
void freezeExistentElementLabel(bool freeze, int from, int to);
void freezeNewElementLabel(bool freeze, int from, int to);
@@ -297,6 +311,8 @@ class QETProject : public QObject
private:
/// When false, writeBackup() is a no-op (set by the headless CLI)
static bool m_backup_enabled;
/// Something changed since the last backup, see writeBackup()
bool m_backup_needed = true;
/// File path this project is saved to
QString m_file_path;
/// Current state of the project
File diff suppressed because it is too large Load Diff
+289 -4
View File
@@ -22,6 +22,7 @@
#include <QString>
#include <QStringList>
#include <QVariantList>
#include <QVariantMap>
class QETProject;
class DiagramView;
@@ -30,6 +31,9 @@ class Terminal;
class Conductor;
class IndependentTextItem;
class QetShapeItem;
class DiagramImageItem;
class DynamicElementTextItem;
class QetGraphicsTableItem;
/**
@brief The QetScriptApi class
@@ -93,13 +97,18 @@ class QetShapeItem;
Terminals are addressed by their @b index in Element::terminals(),
not by uuid, and elementTerminals() prints that indexing so a script
can see what it is about to wire. Terminal uuids look like the
can see what it is about to wire. The index is the terminal's place
in the element's own top-to-bottom, left-to-right ordering, not the
order its definition file lists them. Terminal uuids look like the
obvious key and are not one: Terminal::uuid() is a property of the
catalog .elmt definition, empty for most of the installed base and,
where present, identical across every instance of that element -- so
it does not distinguish one placed coil's A1 from another's.
- @b Conductor properties and @b cross-references: set a conductor's
number, formula, colour or section, and link a master to a slave or
number, formula, colour or section (and its look: style normal/
dashed/dashdotted, two-colour mode and second colour, dash size,
line width, text size, whether its number is shown -- all under the
names the .qet file uses for them), and link a master to a slave or
one report to another. Both follow the application's own rules rather
than writing the field: a conductor property is applied to every
conductor of the same electrical potential, which is what the GUI and
@@ -123,7 +132,11 @@ class QetShapeItem;
its circuit -- a free-standing note, a line, a rectangle, an ellipse
-- added with the same AddGraphicsObjectCommand the corresponding GUI
tools use, and changed through the plainText/color/rotation
properties those items already publish.
properties those items already publish. A shape's look is set with
setShapeProperty(): color and fill (a colour name, or "none" for no
fill), width, line-style (solid, dashed, dotted, dashdot) and
rotation, through the pen/brush/rotation properties the shape's own
style editor changes.
These are addressed by @b index into a listing sorted by position
(top to bottom, then left to right), because unlike an element they
@@ -150,6 +163,157 @@ class QetShapeItem;
depend on. The underlying tables are how the cache happens to be
arranged today, and a column may move. tables() lists both so a
script can see what it is querying rather than guess.
- @b Terminal @b strips: create a strip, put terminal-type elements on
it, remove it. Strips are addressed by index into terminalStrips(),
which is the project's own order (unlike texts and shapes it is not
re-sorted) and does shift when one is removed. Only elements whose
link type is "terminal" can be added, the same restriction the
editor enforces by construction.
stripRealTerminals() lists the strip's real terminals -- the actual
wire-ends added by addTerminalToStrip(), one per index -- which
physical position (clamp) each currently sits on and how many
neighbours share it, since that is what groupTerminals() and
bridgeTerminals() address by index into.
groupTerminals() merges several real terminals onto one physical
position, choosing the receiving position the same way the terminal
strip editor's own "group" button does: the position among the ones
named that already carries the most real terminals, not necessarily
the first one given -- a script asking to group indices [0, 1] is not
guaranteed index 0's position is where they end up. bridgeTerminals()
wires several real terminals together electrically without merging
their positions, refused (TerminalStrip::isBridgeable()) when they
are not all at the same level -- the same check the editor's bridge
button applies, not a rule reimplemented here. sortTerminalStrip()
reorders the strip's physical positions into the canonical order the
editor's own sort button computes.
- @b Tables: a BOM/nomenclature or a summary (table of contents) placed
on a folio, through QetGraphicsTableFactory::create() -- the same
factory call the "add table" menu action makes, minus the modal
AddTableDialog it collects its settings from first. That dialog is
still built here, off-screen and never shown or exec'd: addTable()
calls setTableName() and the query widget's setQuery() on it, the
same as a user filling in the form, and forces its two checkboxes
("adjust table to folio" and "add a new folio if the table overflows"
it) off regardless of their .ui-file default of checked -- a script
calling addTable() once should create exactly the one table it asked
for, not possibly several spread across folios it never asked to add.
A script that wants either behaviour can resize the result itself or
add its own folio.
Neither creating nor deleting a table is undoable:
QetGraphicsTableFactory::newTable(), which create() calls, calls
Diagram::addItem() directly, with no undo command of its own, in the
stock "add table" action as much as here -- a pre-existing gap in the
application, not something introduced by this API. Tables are
addressed by index in a position-sorted listing, like texts, shapes
and images.
- @b Auto-numbering: define a named numbering context of kind
"conductor", "element" or "folio", built from parts written
"type[:value[:increase]]" -- types are the ones the auto-numbering
dialog offers (string, unit, ten, hundred, alpha, idfolio, folio,
plant, locmach, elementline, elementcolumn, elementprefix, wrap,
unitfolio, tenfolio, hundredfolio) -- and select which one a folio's
new conductors use. Defining or removing a context is not undoable,
because the application itself does it through direct project calls
and only the counter advance is on the undo stack; the numbering
actually applied to a conductor is.
For elements, useElementAutoNum() selects the current context and
numberElement() applies it to one element, as the "add element" tool
does right after placing one. addElement() deliberately does not
number what it places: doing it silently would change what an existing
script produces the moment its project happens to have a context
selected, so it is a separate, explicit call. Folio auto-numbering is
not offered: in the application it spawns whole new folios from a
context, which is a different operation from labelling.
- @b Duplicating: copy elements, together with the conductors that run
between them, to a position on the same or another folio, through
Diagram::toXml() and fromXml() and PasteDiagramCommand -- what Ctrl+C
and Ctrl+V do, so a paste behaves as a paste does there: the copies
come without their labels and without their conductors' wire numbers,
which the application clears on paste (measured: '' on both).
The position is the top left of the pasted group's bounding rectangle,
so an element's own origin ends up offset from it by its hotspot
(measured: +20, +30 for a coil); (0, 0) is not a position but means
"keep the source coordinates", as Diagram::fromXml() treats it. The
result lists the copies in the order the elements were named --
the application's own list is in scene order, and a caller pairing by
index would otherwise be wired to the wrong copies -- paired by
position, which a paste preserves, so two elements at the same point
cannot be told apart. A conductor is copied
only if both its ends are among the copied elements. The previous
selection is put back afterwards, since copying works by selecting.
- @b Project title and folio frame: setProjectTitle(), and the grid that
frames each folio -- columns and rows, their size, and whether the
headers show (columns, column-width, display-columns, rows, row-height,
display-rows) -- through ChangeBorderCommand. These are the six fields
the folio properties panel offers; the title block's header sizes,
which it does not, are left alone. Changing the project title is not
undoable: the application sets it directly too.
A folio's title block @b template is a seventh, separate case:
Diagram::setTitleBlockTemplate() resolves a name only against
QETProject::embeddedTitleBlockTemplatesCollection() -- the same
copy-into-the-project step addElement() already does for elements,
and for the same reason (a project opened on another machine must not
depend on files only this one has). titleBlockTemplates() lists what
is embedded and what is available to embed from the common/company
/custom collections, each name suffixed with its source;
embedTitleBlockTemplate() does the copy (QDomElement in, unmodified,
via *TemplatesCollection::get/setTemplateXmlDescription() -- neither
side is scripting-specific code, both already exist for the template
editor to call). setFolioProperty(folio, "template", name) then
embeds it first if it is not already, refusing only if no collection
has that name at all. Embedding is not undoable, the same as defining
an auto-numbering context is not: the application does both through
direct collection/project calls with no undo command of their own.
A template literally named "default" reads back as folioProperty()
"" afterwards, not "default": BorderTitleBlock::titleBlockTemplateName()
treats the two as the same thing, since "no override" already renders
with the template named "default".
- @b Geometry and folio order: elementGeometry() reads where an element
is -- x, y (its origin), rotation, and the box it occupies on the folio
(left, top, right, bottom) -- so a script can lay one thing out relative
to another instead of only setting absolute coordinates, and can check
that a move landed. insertFolio() puts a new folio at a position
instead of at the end, which is what reordering is mostly for while
moving an existing folio still needs the application's project view.
- @b Images: place a picture from a file. The pixels are copied into
the project, which stores them inline in the .qet -- the saved file
does not refer to the original path, so it opens on another machine,
and it grows by roughly the size of the image, which is why files
over 10 MB are refused. Images are addressed by index in a
position-sorted listing, like texts and shapes -- by the on-screen
bounding box, so scaling or rotating an image, which turns about its
centre, can change where it sorts. Re-list after either.
- @b Element @b texts: the text fields drawn on a symbol -- its label,
the names beside its terminals, any value the definition placed there.
A symbol arrives with the fields its definition gives it; setElementLabel()
fills the value one of them shows, and these methods control the fields
themselves: where each sits, its size, whether it draws a frame, what it
shows, and adding or deleting one. Addressed by index in the element's
own list, which follows the definition's order and shifts when one is
deleted -- and undoing a deletion puts the field back at the end, so
list again after either.
Two things called text, which differ for a field bound to an
information key: the @b "text" property is the field's stored string,
which for an information-bound field is an unused placeholder (empty,
or "Texte" once one has been added), and @b "shows" is what is drawn,
which follows the element's information straight away -- compared
against elementInfo() at seven points across relabel, rebinding,
setting and undo, with no difference. Read "shows".
Consecutive setElementInfo()/setElementLabel() calls on one element
merge into a single undo step, as ChangeElementInformationCommand
does, so one undo can revert several.
A field's @b source is "text" (a fixed string), "info" (the value of one
of the element's information keys, so it follows setElementInfo() and
setElementLabel()) or "composite" (a formula over several). Position is in
the element's own coordinates, not the folio's.
- @b Navigating and @b messaging: select an element, zoom the active
view, and show the user a message. Deliberately narrow: selection and
messaging work with no view at all (headless `--run`); zoom is a no-op
@@ -238,16 +402,38 @@ class QetScriptApi : public QObject
int terminalIndex, const QString &property,
const QString &value);
// -- a conductor's own drawn path, not the whole potential's
// properties above -- one conductor only, addressed the same way --
Q_INVOKABLE QStringList conductorSegments(int folioIndex, const QString &elementUuid,
int terminalIndex) const;
Q_INVOKABLE bool moveConductorSegment(int folioIndex, const QString &elementUuid,
int terminalIndex, int segmentIndex,
double dx, double dy);
// -- cross-references: master/slave and report links --
Q_INVOKABLE QString elementLinkType(int folioIndex, const QString &elementUuid) const;
Q_INVOKABLE QStringList linkedElements(int folioIndex, const QString &elementUuid) const;
Q_INVOKABLE bool linkElements(int folioIndexA, const QString &elementUuidA,
int folioIndexB, const QString &elementUuidB);
int folioIndexB, const QString &elementUuidB,
int groupIndex = -1);
Q_INVOKABLE bool unlinkElement(int folioIndex, const QString &elementUuid);
Q_INVOKABLE int elementLinkGroupIndex(int folioIndex, const QString &elementUuid,
int otherFolioIndex, const QString &otherElementUuid) const;
// -- a PLC master's IO table: address/function/comment rows a PLC
// slave links onto via linkElements()'s groupIndex --
Q_INVOKABLE QStringList plcIOs(int folioIndex, const QString &elementUuid) const;
Q_INVOKABLE int addPlcIO(int folioIndex, const QString &elementUuid, const QString &type,
const QString &address, const QString &functionText,
const QString &comment);
Q_INVOKABLE bool setPlcIO(int folioIndex, const QString &elementUuid, int ioIndex,
const QString &property, const QString &value);
Q_INVOKABLE bool removePlcIO(int folioIndex, const QString &elementUuid, int ioIndex);
// -- independent text and drawing shapes --
Q_INVOKABLE QStringList texts(int folioIndex) const;
Q_INVOKABLE int addText(int folioIndex, const QString &text, double x, double y);
Q_INVOKABLE QString textContent(int folioIndex, int textIndex) const;
Q_INVOKABLE bool setTextContent(int folioIndex, int textIndex, const QString &text);
Q_INVOKABLE bool setTextColor(int folioIndex, int textIndex, const QString &color);
Q_INVOKABLE bool setTextRotation(int folioIndex, int textIndex, double angle);
@@ -257,14 +443,97 @@ class QetScriptApi : public QObject
Q_INVOKABLE int addShape(int folioIndex, const QString &type,
double x1, double y1, double x2, double y2);
Q_INVOKABLE bool deleteShape(int folioIndex, int shapeIndex);
Q_INVOKABLE QString shapeProperty(int folioIndex, int shapeIndex, const QString &property) const;
Q_INVOKABLE bool setShapeProperty(int folioIndex, int shapeIndex,
const QString &property, const QString &value);
// -- polygon and path shapes: more than addShape()'s two-point box --
Q_INVOKABLE int addPolygon(int folioIndex, const QVariantList &points, bool closed);
Q_INVOKABLE QVariantList shapePolygon(int folioIndex, int shapeIndex) const;
Q_INVOKABLE bool setShapePolygon(int folioIndex, int shapeIndex, const QVariantList &points);
Q_INVOKABLE int addPath(int folioIndex, const QVariantList &nodes, bool closed);
Q_INVOKABLE QVariantList shapePathNodes(int folioIndex, int shapeIndex) const;
Q_INVOKABLE bool setShapePathNodes(int folioIndex, int shapeIndex, const QVariantList &nodes);
Q_INVOKABLE bool setShapeClosed(int folioIndex, int shapeIndex, bool closed);
// -- query the project database --
Q_INVOKABLE QStringList tables() const;
Q_INVOKABLE QVariantList query(const QString &sql);
Q_INVOKABLE QString queryError() const;
// -- removing a conductor or a folio; folio properties beyond the title --
Q_INVOKABLE bool deleteConductor(int folioIndex, const QString &elementUuid, int terminalIndex);
Q_INVOKABLE bool removeFolio(int folioIndex);
Q_INVOKABLE bool setFolioProperty(int folioIndex, const QString &property, const QString &value);
Q_INVOKABLE QString folioProperty(int folioIndex, const QString &property) const;
// -- terminal strips (borniers) --
Q_INVOKABLE QStringList terminalStrips() const;
Q_INVOKABLE int addTerminalStrip(const QString &installation, const QString &location,
const QString &name);
Q_INVOKABLE bool removeTerminalStrip(int stripIndex);
Q_INVOKABLE bool addTerminalToStrip(int stripIndex, int folioIndex,
const QString &elementUuid);
Q_INVOKABLE QStringList stripRealTerminals(int stripIndex) const;
Q_INVOKABLE bool groupTerminals(int stripIndex, const QVariantList &realTerminalIndices);
Q_INVOKABLE bool bridgeTerminals(int stripIndex, const QVariantList &realTerminalIndices);
Q_INVOKABLE bool sortTerminalStrip(int stripIndex);
// -- a BOM/nomenclature or summary table placed on a folio --
Q_INVOKABLE QStringList tables(int folioIndex) const;
Q_INVOKABLE int addTable(int folioIndex, const QString &kind, const QString &name,
const QString &query);
Q_INVOKABLE bool deleteTable(int folioIndex, int tableIndex);
Q_INVOKABLE bool setTablePosition(int folioIndex, int tableIndex, double x, double y);
// -- auto-numbering contexts (conductor, element, folio) --
Q_INVOKABLE QStringList autoNums(const QString &kind) const;
Q_INVOKABLE bool addAutoNum(const QString &kind, const QString &name, const QStringList &parts);
Q_INVOKABLE bool removeAutoNum(const QString &kind, const QString &name);
Q_INVOKABLE bool useConductorAutoNum(int folioIndex, const QString &name);
Q_INVOKABLE bool useElementAutoNum(const QString &name);
Q_INVOKABLE bool numberElement(int folioIndex, const QString &elementUuid);
// -- images, embedded in the project --
Q_INVOKABLE QStringList images(int folioIndex) const;
Q_INVOKABLE int addImage(int folioIndex, const QString &filePath, double x, double y);
Q_INVOKABLE bool setImageScale(int folioIndex, int imageIndex, double factor);
Q_INVOKABLE bool setImageRotation(int folioIndex, int imageIndex, double angle);
Q_INVOKABLE bool deleteImage(int folioIndex, int imageIndex);
Q_INVOKABLE int addPdfPage(int folioIndex, const QString &pdfPath, int pageNumber,
int dpi, double x, double y);
// -- the text fields shown on a symbol (label, terminal names, ...) --
Q_INVOKABLE QStringList elementTexts(int folioIndex, const QString &elementUuid) const;
Q_INVOKABLE int addElementText(int folioIndex, const QString &elementUuid,
const QString &source, const QString &value,
double x, double y);
Q_INVOKABLE bool setElementTextProperty(int folioIndex, const QString &elementUuid,
int textIndex, const QString &property,
const QString &value);
Q_INVOKABLE QString elementTextProperty(int folioIndex, const QString &elementUuid,
int textIndex, const QString &property) const;
Q_INVOKABLE bool deleteElementText(int folioIndex, const QString &elementUuid, int textIndex);
// -- copy elements (with the conductors between them) to a position --
Q_INVOKABLE QStringList duplicateElements(int fromFolioIndex, const QStringList &elementUuids,
int toFolioIndex, double x, double y);
// -- the project title, and each folio's frame (grid of columns and rows) --
Q_INVOKABLE bool setProjectTitle(const QString &title);
Q_INVOKABLE QString folioBorder(int folioIndex, const QString &property) const;
Q_INVOKABLE bool setFolioBorder(int folioIndex, const QString &property, const QString &value);
// -- title block templates: which exist, embedding one into the project --
Q_INVOKABLE QStringList titleBlockTemplates() const;
Q_INVOKABLE bool embedTitleBlockTemplate(const QString &name);
// -- read an element's geometry --
Q_INVOKABLE QVariantMap elementGeometry(int folioIndex, const QString &elementUuid) const;
// -- folios --
Q_INVOKABLE int addFolio();
Q_INVOKABLE int insertFolio(int position);
Q_INVOKABLE bool setFolioTitle(int folioIndex, const QString &title);
Q_INVOKABLE bool undo();
@@ -272,9 +541,21 @@ class QetScriptApi : public QObject
Q_INVOKABLE bool canUndo() const;
Q_INVOKABLE bool canRedo() const;
// -- project-wide text search & replace, one undo step for the
// whole run, in the same spirit as the "Search and replace" panel --
Q_INVOKABLE int searchAndReplace(const QString &kind, const QString &field,
const QString &pattern, const QString &replacement,
bool useRegex, bool caseSensitive);
// -- electrical continuity / ERC: read-only, structural checks
// against the live object graph rather than the XML -- see the
// .cpp doc comment for exactly what is and is not covered --
Q_INVOKABLE QVariantList checkContinuity(int folioIndex);
// -- navigate and message --
Q_INVOKABLE bool selectElement(const QString &elementUuid);
Q_INVOKABLE void deselectAll(int folioIndex);
Q_INVOKABLE QStringList selectedElements(int folioIndex) const;
Q_INVOKABLE bool zoomFit();
Q_INVOKABLE bool zoomToContent();
Q_INVOKABLE bool zoomReset();
@@ -292,6 +573,10 @@ class QetScriptApi : public QObject
const QString &caller);
QList<IndependentTextItem *> sortedTexts(int folioIndex) const;
QList<QetShapeItem *> sortedShapes(int folioIndex) const;
QList<QetGraphicsTableItem *> sortedTables(int folioIndex) const;
QList<DiagramImageItem *> sortedImages(int folioIndex) const;
DynamicElementTextItem *findElementText(int folioIndex, const QString &elementUuid,
int textIndex, const QString &caller) const;
IndependentTextItem *findText(int folioIndex, int textIndex, const QString &caller);
bool setInfoKey(int folioIndex, const QString &elementUuid,
const QString &key, const QString &value, const QString &caller);
+40
View File
@@ -20,6 +20,7 @@
#include "qetscriptapi.h"
#include "../qetmessagebox.h"
#include "../qetproject.h"
#include "../utils/qetsettings.h"
#include <QFile>
#include <QFileInfo>
@@ -48,8 +49,33 @@ bool isRunRequest(const QStringList &args)
#ifdef QET_HAS_SCRIPTING
namespace {
/**
@brief refusalMessage
What to tell somebody whose script was not run, and how to change
that. Written once because the command line and the graphical
editor both need to say it, and an explanation that names only one
of the two ways out sends half the people down the wrong path.
*/
QString refusalMessage()
{
return QObject::tr(
"Les scripts sont désactivés.\n\n"
"Un script a accès à l'ensemble du projet et peut écrire des "
"fichiers, aussi cette fonction est-elle désactivée par défaut.\n\n"
"Pour l'activer : Configurer QElectroTech > Général > Projets, "
"ou définir la variable d'environnement QET_ENABLE_SCRIPTING=1 "
"pour une exécution sans interface (CI, traitement par lot).");
}
}
int run(const QStringList &args)
{
if (!QetSettings::scriptingEnabled()) {
err << refusalMessage() << "\n";
return 3;
}
const int idx = args.indexOf(QStringLiteral("--run"));
const QString script_path = args.value(idx + 1);
const QString project_path = args.value(idx + 2);
@@ -88,6 +114,20 @@ namespace {
bool runOnProject(const QString &scriptPath, QETProject *project, DiagramView *view)
{
// Checked here as well as at each caller, deliberately: this is the
// one function that actually evaluates JavaScript, so it is the one
// place a future caller cannot forget to ask. The callers check first
// only to give a better answer than this one can -- a usable exit code
// on the command line, an offer to switch the setting on in the editor.
if (!QetSettings::scriptingEnabled()) {
err << refusalMessage() << "\n";
if (view) {
QET::QetMessageBox::warning(nullptr, QObject::tr("Script"),
refusalMessage());
}
return false;
}
QFile file(scriptPath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
err << "Cannot open script: " << scriptPath << "\n";
+14 -2
View File
@@ -31,6 +31,7 @@
#include <QMenu>
#include <QPainter>
#include <QPixmap>
#include <QSet>
namespace {
/**
@@ -167,11 +168,22 @@ void ConductorColorToolButton::applyColor(const QColor &color)
}
DiagramContent dc(diagram);
const auto conductors = dc.conductors(DiagramContent::AnyConductor);
if (conductors.isEmpty()) {
const auto selected_conductors = dc.conductors(DiagramContent::AnyConductor);
if (selected_conductors.isEmpty()) {
return;
}
//A wire drawn across a junction is several Conductor objects
//sharing one electrical potential; selecting one segment must
//still recolour the whole potential, the way the F2 dialog's
//"apply to all" already does.
QSet<Conductor *> conductors;
for (Conductor *conductor : selected_conductors)
{
conductors << conductor;
conductors += conductor->relatedPotentialConductors();
}
QUndoCommand *undo = new QUndoCommand(tr("Modifier la couleur de %n conducteur(s)",
"undo caption", conductors.count()));
int changed = 0;
@@ -23,6 +23,7 @@
#include "../../utils/qetsettings.h"
#include "../../utils/qetutils.h"
#include "../../qetmessagebox.h"
#include "../nokde/kcolorbutton.h"
#include <QFileDialog>
#include <QFontDialog>
#include <QSettings>
@@ -74,6 +75,12 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
ui->DiagramEditor_Grid_PointSize_min_sb->setValue(settings.value("diagrameditor/grid_pointsize_min", 1).toInt());
ui->DiagramEditor_Grid_PointSize_max_sb->setValue(settings.value("diagrameditor/grid_pointsize_max", 1).toInt());
ui->m_use_system_color_cb->setChecked(settings.value("usesystemcolors", "true").toBool());
bool sysColors = ui->m_use_system_color_cb->isChecked();
ui->m_custom_app_color_kpb->setEnabled(!sysColors);
if (settings.contains("customapplicationcolor"))
ui->m_custom_app_color_kpb->setColor(QColor(settings.value("customapplicationcolor").toString()));
else
ui->m_custom_app_color_kpb->setColor(QApplication::palette().color(QPalette::Window));
bool tabbed = settings.value("diagrameditor/viewmode", "tabbed") == "tabbed";
if(tabbed)
ui->m_use_tab_mode_rb->setChecked(true);
@@ -82,6 +89,25 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
ui->m_zoom_out_beyond_folio->setChecked(settings.value("diagrameditor/zoom-out-beyond-of-folio", false).toBool());
ui->m_use_gesture_trackpad->setChecked(settings.value("diagramview/gestures", false).toBool());
ui->m_save_label_paste->setChecked(settings.value("diagramcommands/erase-label-on-copy", true).toBool());
ui->m_enable_scripting->setChecked(QetSettings::scriptingEnabled());
#ifdef QET_HAS_SCRIPTING
if (QetSettings::scriptingForcedByEnvironment()) {
//QET_ENABLE_SCRIPTING wins over the stored value, so let the box
//say so rather than offer a tick that changes nothing.
ui->m_enable_scripting->setEnabled(false);
ui->m_enable_scripting->setToolTip(
tr("Activé par la variable d'environnement "
"QET_ENABLE_SCRIPTING ; ce réglage est sans effet "
"tant qu'elle est définie."));
}
#else
//Built without Qt Qml: there is no scripting to allow. Disabled as
//well as hidden, so applyConf() leaves the stored value alone --
//a hidden box still reports its state, and writing it here would
//quietly clear a preference set on a build that does have Qml.
ui->m_enable_scripting->setVisible(false);
ui->m_enable_scripting->setEnabled(false);
#endif
ui->m_use_folio_label->setChecked(settings.value("genericpanel/folio", true).toBool());
ui->m_border_0->setChecked(settings.value("border-columns_0", false).toBool());
ui->m_autosave_sb->setValue(settings.value("diagrameditor/autosave-interval", 0).toInt());
@@ -206,7 +232,17 @@ void GeneralConfigurationPage::applyConf()
bool must_use_system_colors = ui->m_use_system_color_cb->isChecked();
settings.setValue("usesystemcolors", must_use_system_colors);
if (was_using_system_colors != must_use_system_colors) {
QETApp::instance()->useSystemPalette(must_use_system_colors);
if (must_use_system_colors) {
QETApp::instance()->useSystemPalette(true);
} else {
QColor custom_color = ui->m_custom_app_color_kpb->color();
settings.setValue("customapplicationcolor", custom_color.name());
QETApp::instance()->useCustomPalette(custom_color);
}
} else if (!must_use_system_colors) {
QColor custom_color = ui->m_custom_app_color_kpb->color();
settings.setValue("customapplicationcolor", custom_color.name());
QETApp::instance()->useCustomPalette(custom_color);
}
settings.setValue("border-columns_0",ui->m_border_0->isChecked());
settings.setValue("lang", ui->m_lang_cb->itemData(ui->m_lang_cb->currentIndex()).toString());
@@ -227,6 +263,14 @@ void GeneralConfigurationPage::applyConf()
//DIAGRAM COMMAND
settings.setValue("diagramcommands/erase-label-on-copy", ui->m_save_label_paste->isChecked());
//SCRIPTING
//Left alone while the environment forces it on: the box is disabled
//in that case and writing its state would silently clear the user's
//real preference the first time this dialog is accepted.
if (ui->m_enable_scripting->isEnabled()) {
QetSettings::setScriptingEnabled(ui->m_enable_scripting->isChecked());
}
//GENERIC PANEL
settings.setValue("genericpanel/folio",ui->m_use_folio_label->isChecked());
@@ -625,3 +669,14 @@ void GeneralConfigurationPage::on_m_hdpi_round_cb_clicked(bool checked)
ui->m_hdpi_round_policy_cb->setEnabled(checked);
}
/**
@brief GeneralConfigurationPage::on_m_use_system_color_cb_toggled
Enable/disable the custom color picker when the system color
checkbox is toggled.
@param checked
*/
void GeneralConfigurationPage::on_m_use_system_color_cb_toggled(bool checked)
{
ui->m_custom_app_color_kpb->setEnabled(!checked);
}
@@ -53,6 +53,7 @@ class GeneralConfigurationPage : public ConfigPage
void on_ElementEditor_Grid_PointSize_min_sb_valueChanged(int value);
void on_m_hdpi_round_cb_clicked(bool checked);
void on_m_use_system_color_cb_toggled(bool checked);
private:
void fillLang();
@@ -23,15 +23,26 @@
<attribute name="title">
<string>Apparence</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="m_use_system_color_cb">
<property name="text">
<string>Utiliser les couleurs du système</string>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="m_system_color_layout">
<item>
<widget class="QCheckBox" name="m_use_system_color_cb">
<property name="text">
<string>Utiliser les couleurs du système</string>
</property>
</widget>
</item>
<item>
<widget class="KColorButton" name="m_custom_app_color_kpb">
<property name="toolTip">
<string>Couleur de l'application</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@@ -207,6 +218,16 @@
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="m_enable_scripting">
<property name="text">
<string>Autoriser l'exécution de scripts JavaScript (Projet &gt; Exécuter un script, et --run)</string>
</property>
<property name="toolTip">
<string>Un script s'exécute avec vos droits : il peut lire et modifier le projet ouvert et écrire des fichiers. Désactivé par défaut ; n'exécutez que des scripts dont vous connaissez l'origine.</string>
</property>
</widget>
</item>
<item row="5" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -1161,4 +1182,11 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
</tabstops>
<resources/>
<connections/>
<customwidgets>
<customwidget>
<class>KColorButton</class>
<extends>QPushButton</extends>
<header>nokde/kcolorbutton.h</header>
</customwidget>
</customwidgets>
</ui>
+41 -2
View File
@@ -23,6 +23,7 @@
#include "../qetdiagrameditor.h"
#include "../projectview.h"
#include "../qetproject.h"
#include "../utils/qetsettings.h"
#include <QApplication>
#include <QColorDialog>
@@ -62,8 +63,28 @@ DiagramBgColorToolButton::DiagramBgColorToolButton(QETDiagramEditor *editor, QWi
setStatusTip(tr("Choisir la couleur de fond du folio",
"status bar tip"));
m_is_system_color = true;
m_current = QApplication::palette().color(QPalette::Base);
//main() already restored the stored choice onto the global
//Diagram::background_color and PaletteGraphicsView flag; mirror it
//here so the button opens on what the editor actually draws, rather
//than always claiming "system colour" as it used to.
if (PaletteGraphicsView::customBackgroundColor()) {
m_is_system_color = false;
m_current = Diagram::background_color;
} else {
m_is_system_color = true;
m_current = QApplication::palette().color(QPalette::Base);
}
for (const QString &name : QetSettings::sheetBackgroundRecentColors())
{
const QColor c(name);
if (c.isValid()) {
m_recent.append(c);
}
}
while (m_recent.size() > MAX_RECENT) {
m_recent.removeLast();
}
setMenu(new QMenu(this));
rebuildMenu();
setSwatch(m_current);
@@ -123,6 +144,7 @@ void DiagramBgColorToolButton::applyColor(const QColor &color)
PaletteGraphicsView::setCustomBackgroundColor(true);
Diagram::background_color = color;
QetSettings::setSheetBackground(QetSettings::SheetBackground{color, true});
QETDiagramEditor *editor = m_editor;
if (!editor) {
@@ -146,6 +168,8 @@ void DiagramBgColorToolButton::applySystemColor()
PaletteGraphicsView::setCustomBackgroundColor(false);
Diagram::background_color = Qt::white;
QetSettings::setSheetBackground(
QetSettings::SheetBackground{QColor(Qt::white), false});
QETDiagramEditor *editor = m_editor;
if (!editor) {
@@ -187,6 +211,21 @@ void DiagramBgColorToolButton::rememberRecent(const QColor &color)
m_recent.removeLast();
}
rebuildMenu();
persistRecent();
}
/**
@brief DiagramBgColorToolButton::persistRecent
Write the "recently used" colours back to the settings, so they are
still there on the next start instead of starting out empty.
*/
void DiagramBgColorToolButton::persistRecent()
{
QStringList names;
for (const QColor &c : std::as_const(m_recent)) {
names << c.name();
}
QetSettings::setSheetBackgroundRecentColors(names);
}
/**
+1
View File
@@ -52,6 +52,7 @@ class DiagramBgColorToolButton : public QToolButton
void applySystemColor();
void chooseOtherColor();
void rememberRecent(const QColor &color);
void persistRecent();
void setSwatch(const QColor &color);
static QIcon swatchIcon(const QColor &color);
+114
View File
@@ -0,0 +1,114 @@
/*
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 "duplicateoffsetdialog.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QSettings>
#include <QSpinBox>
#include <QVBoxLayout>
namespace {
// Stored as one integer per axis, in grid steps -- not pixels, so the
// remembered offset still makes sense if the grid size ever changes.
const QString kOffsetXKey = QStringLiteral("diagrameditor/duplicate_offset_x");
const QString kOffsetYKey = QStringLiteral("diagrameditor/duplicate_offset_y");
}
DuplicateOffsetDialog::DuplicateOffsetDialog(QWidget *parent) :
QDialog(parent)
{
setWindowTitle(tr("Dupliquer"));
auto *form = new QFormLayout;
m_spacing = new QSpinBox(this);
m_spacing->setRange(1, 1000);
m_spacing->setSuffix(tr(" pas de grille"));
form->addRow(tr("Espacement :"), m_spacing);
m_direction = new QComboBox(this);
// Order matches the Direction enum, so currentIndex() can be used
// directly wherever Direction is needed.
m_direction->addItem(tr("Haut"));
m_direction->addItem(tr("Bas"));
m_direction->addItem(tr("Gauche"));
m_direction->addItem(tr("Droite"));
form->addRow(tr("Direction :"), m_direction);
const QPoint saved = savedStepOffset();
// The saved value is a signed (dx, dy) pair, not itself a
// spacing+direction pair, so it has to be decomposed back into the
// two the dialog shows. Exactly one axis is ever non-zero (see
// stepOffset()), so whichever one is picks the direction; a value
// that somehow has neither (only possible if QSettings was hand-
// edited) falls back to the same default stepOffset() would.
int spacing = 1;
Direction direction = Right;
if (saved.x() > 0) { direction = Right; spacing = saved.x(); }
else if (saved.x() < 0) { direction = Left; spacing = -saved.x(); }
else if (saved.y() > 0) { direction = Down; spacing = saved.y(); }
else if (saved.y() < 0) { direction = Up; spacing = -saved.y(); }
m_spacing->setValue(spacing);
m_direction->setCurrentIndex(static_cast<int>(direction));
auto *buttons = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
auto *layout = new QVBoxLayout(this);
layout->addLayout(form);
layout->addWidget(buttons);
}
QPoint DuplicateOffsetDialog::stepOffset() const
{
const int spacing = m_spacing->value();
switch (static_cast<Direction>(m_direction->currentIndex())) {
case Up: return QPoint(0, -spacing);
case Down: return QPoint(0, spacing);
case Left: return QPoint(-spacing, 0);
case Right: return QPoint(spacing, 0);
}
return QPoint(spacing, 0); // unreachable; keeps -Wreturn-type quiet
}
QPoint DuplicateOffsetDialog::savedStepOffset()
{
QSettings settings;
if (!hasSavedStepOffset()) {
return QPoint(1, 0); // default: one grid step to the right
}
return QPoint(settings.value(kOffsetXKey).toInt(),
settings.value(kOffsetYKey).toInt());
}
void DuplicateOffsetDialog::saveStepOffset(const QPoint &steps)
{
QSettings settings;
settings.setValue(kOffsetXKey, steps.x());
settings.setValue(kOffsetYKey, steps.y());
}
bool DuplicateOffsetDialog::hasSavedStepOffset()
{
QSettings settings;
return settings.contains(kOffsetXKey) && settings.contains(kOffsetYKey);
}
+65
View File
@@ -0,0 +1,65 @@
/*
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 DUPLICATEOFFSETDIALOG_H
#define DUPLICATEOFFSETDIALOG_H
#include <QDialog>
#include <QPoint>
class QSpinBox;
class QComboBox;
/**
@brief The DuplicateOffsetDialog class
Asks how far, and in which of the four cardinal directions, Ctrl+D
(DiagramView::duplicate(), bugtracker #991) should offset a copy from
its source. Shown once, then remembered: the answer is stored in
QSettings and reused by every later Ctrl+D press without asking
again, until this dialog is reopened deliberately.
*/
class DuplicateOffsetDialog : public QDialog
{
Q_OBJECT
public:
enum Direction { Up, Down, Left, Right };
explicit DuplicateOffsetDialog(QWidget *parent = nullptr);
/// The offset in grid steps, positive along X to the right
/// and positive along Y downward -- QET's own scene axes,
/// matching the sign convention setPos() already uses
/// everywhere else in this codebase.
QPoint stepOffset() const;
/// Reads the last-confirmed spacing/direction from QSettings,
/// or the default (1 step, right) if none was ever set.
static QPoint savedStepOffset();
/// Writes @p steps to QSettings, in the same X/Y convention
/// as stepOffset().
static void saveStepOffset(const QPoint &steps);
/// Whether a direction/spacing has already been confirmed
/// once, i.e. whether Ctrl+D can skip the dialog.
static bool hasSavedStepOffset();
private:
QSpinBox *m_spacing = nullptr;
QComboBox *m_direction = nullptr;
};
#endif // DUPLICATEOFFSETDIALOG_H
+4 -3
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "elementinfowidget.h"
#include "../qet.h"
#include <QCheckBox>
#include <QPushButton>
#include "../diagram.h"
@@ -366,18 +367,18 @@ void ElementInfoWidget::updateUi()
// Load the lock status for auto numbering
if (m_element->elementData().m_type == ElementData::Terminal) {
QString lock_value = element_info.value(QStringLiteral("auto_num_locked")).toString();
ui->m_auto_num_locked_cb->setChecked(lock_value == QLatin1String("true"));
ui->m_auto_num_locked_cb->setChecked(QET::infoFlagIsTrue(lock_value));
// English: Load the potential isolating status from the element information mapping
if (m_potential_isolating_cb) {
QString isolating_value = element_info.value(QStringLiteral("potential_isolating")).toString();
m_potential_isolating_cb->setChecked(isolating_value == QLatin1String("true"));
m_potential_isolating_cb->setChecked(QET::infoFlagIsTrue(isolating_value));
}
}
// English: Load the BOM exclusion status from the element information mapping
if (m_exclude_from_bom_cb) {
QString exclude_bom_value = element_info.value(QStringLiteral("exclude_from_bom")).toString();
m_exclude_from_bom_cb->setChecked(exclude_bom_value == QLatin1String("true"));
m_exclude_from_bom_cb->setChecked(QET::infoFlagIsTrue(exclude_bom_value));
}
if (m_live_edit) {
+29 -1
View File
@@ -539,7 +539,35 @@ void MasterPropertiesWidget::updateUi()
tr("Type"), tr("Adresse"), tr("Fonction"),
tr("Commentaire"), tr("Réf. croisée"), tr("Bornes")
});
m_plc_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
m_plc_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
m_plc_table->horizontalHeader()->setSectionsMovable(true);
m_plc_table->horizontalHeader()->resizeSection(0, 120);
m_plc_table->horizontalHeader()->resizeSection(1, 100);
m_plc_table->horizontalHeader()->resizeSection(2, 150);
m_plc_table->horizontalHeader()->resizeSection(3, 150);
m_plc_table->horizontalHeader()->resizeSection(4, 100);
m_plc_table->horizontalHeader()->resizeSection(5, 120);
// Restore a previously saved column layout, if any
auto *plc_hdr = m_plc_table->horizontalHeader();
QSettings plc_settings;
const QVariant plc_hdr_state = plc_settings.value(
QStringLiteral("masterpropertieswidget/plc-table-header-state"));
if (!plc_hdr_state.isNull())
plc_hdr->restoreState(plc_hdr_state.toByteArray());
// Persist the column layout whenever the user resizes or moves a column
auto save_plc_header_state = [plc_hdr]()
{
QSettings settings;
settings.setValue(QStringLiteral("masterpropertieswidget/plc-table-header-state"),
plc_hdr->saveState());
};
connect(plc_hdr, &QHeaderView::sectionResized,
this, save_plc_header_state);
connect(plc_hdr, &QHeaderView::sectionMoved,
this, save_plc_header_state);
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems);
m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_plc_table->setMinimumHeight(200);
+3 -2
View File
@@ -1,5 +1,6 @@
#include "terminalnumberingdialog.h"
#include "ui_terminalnumberingdialog.h"
#include "../qet.h"
#include "../qetproject.h"
#include "../diagram.h"
#include "../qetgraphicsitem/element.h"
@@ -34,7 +35,7 @@ TerminalNumberingDialog::TerminalNumberingDialog(QWidget *parent, QETProject *pr
if (elmt->elementData().m_type == ElementData::Terminal) {
// Ignore locked terminals
DiagramContext info = elmt->elementInformations();
if (info.value(QStringLiteral("auto_num_locked")).toString() == QLatin1String("true")) {
if (QET::infoFlagIsTrue(info.value(QStringLiteral("auto_num_locked")).toString())) {
continue;
}
@@ -158,7 +159,7 @@ QUndoCommand* TerminalNumberingDialog::getUndoCommand(QETProject *project) const
DiagramContext info = elmt->elementInformations();
// Ignore locked terminals (if the user checked a 'lock' property)
if (info.value(QStringLiteral("auto_num_locked")).toString() == QLatin1String("true")) {
if (QET::infoFlagIsTrue(info.value(QStringLiteral("auto_num_locked")).toString())) {
continue;
}
+63 -21
View File
@@ -314,31 +314,15 @@ void LinkElementCommand::redo()
if(m_element->diagram()) m_element->diagram()->showMe();
makeLink(m_linked_after);
//If the action is to link two reports together, we check if the conductors
//of the new potential have the same text, function, and protocol.
//if not, a dialog ask what do to.
//If the action is to link two reports together, and the conductors
//of the new potential disagree on a property that matters, a
//dialog asks what to do. See reportLinkNeedsPotentialChoice() for
//what "disagree" checks and the bug fixed there (bugtracker #974).
if (m_first_redo && (m_element->linkType() & Element::AllReport) \
&& m_element->conductors().size() \
&& m_linked_after.size() && m_linked_after.first()->conductors().size())
{
//fill list of potential
QSet <Conductor *> c_list = m_element->conductors().first()->relatedPotentialConductors();
c_list << m_element->conductors().first();
//fill list of text
QStringList str_txt;
QStringList str_funct;
QStringList str_tens;
for (const Conductor *c : c_list)
{
str_txt << c->properties().text;
str_funct << c->properties().m_function;
str_tens << c->properties().m_tension_protocol;
str_tens << c->properties().m_wire_color;
str_tens << c->properties().m_wire_section;
}
//check text list, isn't same in potential, ask user what to do
if (!QET::eachStrIsEqual(str_txt) || !QET::eachStrIsEqual(str_funct) || !QET::eachStrIsEqual(str_tens))
if (reportLinkNeedsPotentialChoice(m_element, m_linked_after.first()))
{
PotentialSelectorDialog psd(m_element, this);
psd.exec();
@@ -348,6 +332,64 @@ void LinkElementCommand::redo()
QUndoCommand::redo();
}
/**
@brief LinkElementCommand::reportLinkNeedsPotentialChoice
Whether linking these two report elements (next_report/previous_report)
would pop PotentialSelectorDialog -- i.e. whether their conductors (if
any exist yet, on either side) disagree on a property redo() cares
about. Exposed as its own static method, rather than left inline in
redo(), for the same reason ConductorCreator::needsPotentialChoice()
is: a caller with nobody there to answer a modal dialog (the scripting
API) can check first and decline, and the condition cannot drift away
from the one redo() actually applies.
Bug fixed here (bugtracker #974): the original check built ONE
combined list from three unrelated fields (tension_protocol,
wire_color, wire_section) and tested that whole list for equality --
comparing a tension-protocol string against a wire-colour string is
never equal even when each field individually matches across every
conductor, and wire_color/wire_section are ConductorProperties::
m_wire_color/m_wire_section, a separate free-text documentation pair
that says nothing about how the wire is actually drawn (that is
"color"/"style"). Net effect: the dialog could not reliably detect a
real mismatch, including the exact case #974 reported -- two
report-linked conductors drawn in different colours -- and could just
as easily fire on conductors that matched in every way that mattered.
Comparing each relevant field (text/num, function, tension protocol,
colour, line style) on its own fixes both.
@param element_a @param element_b the two elements about to be (or
already) linked; order does not matter
@return true if the dialog would (or does) open
*/
bool LinkElementCommand::reportLinkNeedsPotentialChoice(Element *element_a, Element *element_b)
{
if (!element_a || !element_b) return false;
if (element_a->conductors().isEmpty() || element_b->conductors().isEmpty()) return false;
QSet<Conductor *> c_list;
for (Element *e : {element_a, element_b})
{
if (e->conductors().isEmpty()) continue;
c_list << e->conductors().first();
c_list += e->conductors().first()->relatedPotentialConductors();
}
if (c_list.size() < 2) return false;
QStringList str_txt, str_funct, str_tens, str_color, str_style;
for (const Conductor *c : std::as_const(c_list))
{
str_txt << c->properties().text;
str_funct << c->properties().m_function;
str_tens << c->properties().m_tension_protocol;
str_color << c->properties().color.name();
str_style << QString::number(int(c->properties().style));
}
return !QET::eachStrIsEqual(str_txt) || !QET::eachStrIsEqual(str_funct)
|| !QET::eachStrIsEqual(str_tens) || !QET::eachStrIsEqual(str_color)
|| !QET::eachStrIsEqual(str_style);
}
/**
@brief LinkElementCommand::setUpNewLink
Update the content of m_link_after with the content of element_list.
+1
View File
@@ -38,6 +38,7 @@ class LinkElementCommand : public QUndoCommand
bool mergeWith(const QUndoCommand *other) override;
static bool isLinkable (Element *element_a, Element *element_b, bool already_linked = false);
static bool reportLinkNeedsPotentialChoice(Element *element_a, Element *element_b);
void setLink (const QList<Element *>& element_list);
void setLink (Element *element_);
+50 -12
View File
@@ -25,15 +25,34 @@
#include "../qetgraphicsitem/elementtextitemgroup.h"
#include "../qtextorientationspinboxwidget.h"
/**
@brief RotateTextsCommand::hasSelectedTexts
@param diagram
@return true if @p diagram has at least one selected text or text group.
Lets a caller decide whether to ask the user for an angle at all, keeping
the previous behaviour where no dialog appeared for an empty selection.
*/
bool RotateTextsCommand::hasSelectedTexts(Diagram *diagram)
{
if(!diagram)
return false;
DiagramContent dc(diagram);
return (!dc.selectedTexts().isEmpty() || !dc.selectedTextsGroup().isEmpty());
}
/**
@brief RotateTextsCommand::RotateTextsCommand
@param diagram : Apply the rotation to the selected texts and group of texts
of diagram at construction time.
of diagram at construction time.
@param rotation : the angle to apply, in degrees. Obtain it from
askRotation() for interactive use; pass it directly from a test or script.
@param parent : undo parent
*/
RotateTextsCommand::RotateTextsCommand(Diagram *diagram, QUndoCommand *parent) :
RotateTextsCommand::RotateTextsCommand(Diagram *diagram, qreal rotation, QUndoCommand *parent) :
QUndoCommand(parent),
m_diagram(diagram)
m_diagram(diagram),
m_rotation(rotation)
{
DiagramContent dc(m_diagram);
QList <DiagramTextItem *> texts_list;
@@ -53,8 +72,6 @@ m_diagram(diagram)
if(texts_list.count() || groups_list.count())
{
openDialog();
QStringList parts;
if (texts_list.count())
parts << QObject::tr("%n texte(s)", "", texts_list.count());
@@ -76,9 +93,15 @@ void RotateTextsCommand::undo()
if(m_diagram)
m_diagram.data()->showMe();
//Nothing was selected at construction time: there is no animation to
//run. QUndoStack::push() calls redo() before it discards an obsolete
//command, so this has to be survivable rather than assumed away.
if(!m_anim_group)
return;
m_anim_group->setDirection(QAnimationGroup::Backward);
m_anim_group->start();
for(ConductorTextItem *cti : m_cond_texts.keys())
cti->forceRotateByUser(m_cond_texts.value(cti));
}
@@ -88,14 +111,28 @@ void RotateTextsCommand::redo()
if(m_diagram)
m_diagram.data()->showMe();
if(!m_anim_group)
return;
m_anim_group->setDirection(QAnimationGroup::Forward);
m_anim_group->start();
for(ConductorTextItem *cti : m_cond_texts.keys())
cti->forceRotateByUser(true);
}
void RotateTextsCommand::openDialog()
/**
@brief RotateTextsCommand::askRotation
Ask the user for an orientation.
@param rotation : set to the chosen angle when the dialog is accepted,
left untouched otherwise.
@return true if the user accepted, false if they cancelled.
Deliberately static and separate from the command: a QUndoCommand that
blocks on a modal in its constructor cannot be built by a test, a script,
or any headless caller.
*/
bool RotateTextsCommand::askRotation(qreal &rotation)
{
//Open the dialog
QDialog ori_text_dialog;
@@ -120,10 +157,11 @@ void RotateTextsCommand::openDialog()
layout_v.addStretch();
layout_v.addWidget(&buttons);
if (ori_text_dialog.exec() == QDialog::Accepted)
m_rotation = ori_widget->orientation();
else
setObsolete(true);
if (ori_text_dialog.exec() != QDialog::Accepted)
return false;
rotation = ori_widget->orientation();
return true;
}
void RotateTextsCommand::setupAnimation(QObject *target, const QByteArray &propertyName, const QVariant& start, const QVariant& end)
+23 -4
View File
@@ -28,19 +28,38 @@ class QParallelAnimationGroup;
/**
@brief The RotateTextsCommand class
Open a dialog for edit the rotation of the current selected texts and texts group in diagram.
Just instantiate this undo command and push it in a QUndoStack.
Apply @p rotation to the currently selected texts and texts group of a
diagram. Just instantiate this undo command and push it in a QUndoStack.
This command does not ask the user for anything: obtaining the angle is the
caller's job, via the askRotation() helper below. Keeping the dialog out of
the constructor is what makes the command usable outside an interactive
session — from a test harness, a regression sweep, or a script — and stops
a headless caller from blocking forever on a modal nobody can answer.
Typical interactive use:
@code
if (RotateTextsCommand::hasSelectedTexts(diagram)) {
qreal rotation = 0;
if (RotateTextsCommand::askRotation(rotation))
diagram->undoStack().push(new RotateTextsCommand(diagram, rotation));
}
@endcode
*/
class RotateTextsCommand : public QUndoCommand
{
public:
RotateTextsCommand(Diagram *diagram, QUndoCommand *parent=nullptr);
RotateTextsCommand(Diagram *diagram, qreal rotation, QUndoCommand *parent=nullptr);
/// @return true if @p diagram has at least one selected text or text group to rotate.
static bool hasSelectedTexts(Diagram *diagram);
/// Open the orientation dialog. @return true and set @p rotation if accepted, false if cancelled.
static bool askRotation(qreal &rotation);
void undo() override;
void redo() override;
private:
void openDialog();
void setupAnimation(QObject *target, const QByteArray &propertyName, const QVariant& start, const QVariant& end);
private:
+11
View File
@@ -18,7 +18,9 @@
#include "conductorcreator.h"
#include "../conductorautonumerotation.h"
#include "../dataBase/projectdatabase.h"
#include "../diagram.h"
#include "../qetproject.h"
#include "../undocommand/addgraphicsobjectcommand.h"
#include "../qetgraphicsitem/conductor.h"
#include "../qetgraphicsitem/element.h"
@@ -68,6 +70,15 @@ ConductorCreator::ConductorCreator(Diagram *d, QList<Terminal *> terminals_list)
for(Conductor *c : c_list) {
c->refreshText();
//refreshText() resolves an auto-numbering formula into
//properties.text without emitting propertiesChange, which is
//what the project database listens to. The row was inserted
//while text was still the raw formula ("W%sequ_1"), so without
//this the wiring list and BOM read the formula, not "W1",
//until something forces a full rebuild.
if (d->project() && d->project()->dataBase()) {
d->project()->dataBase()->updateConductor(c);
}
}
}
+110
View File
@@ -19,6 +19,7 @@
#include "qetsettings.h"
#include <QSettings>
#include <QVariant>
#include <QByteArray>
namespace QetSettings
{
@@ -105,4 +106,113 @@ namespace QetSettings
return default_policy;
}
}
/**
* @brief scriptingForcedByEnvironment
* @return true if QET_ENABLE_SCRIPTING is set to 1 in the environment.
*
* The way to turn scripting on where there is nobody to tick a box:
* a headless run, a CI job, a build server. Those have no settings
* file worth writing to -- and on a machine whose HOME is created
* fresh for the run, writing one would not survive anyway.
*/
bool scriptingForcedByEnvironment()
{
return qgetenv("QET_ENABLE_SCRIPTING") == QByteArray("1");
}
/**
* @brief scriptingEnabled
* @return whether QElectroTech may run a JavaScript script.
*
* Off unless the user turned it on. A script reaches the whole project
* and the filesystem through the export calls, so it is capability the
* great majority of users never asked for; leaving it on by default
* would hand it to them anyway. @sa setScriptingEnabled
*
* The environment override wins over the stored value, and is checked
* first so that a machine with no settings at all still answers.
*/
bool scriptingEnabled()
{
if (scriptingForcedByEnvironment()) {
return true;
}
QSettings settings;
return settings.value("scripting/enabled", false).toBool();
}
/**
* @brief setScriptingEnabled
* Store whether scripting is allowed. @sa scriptingEnabled
* @param enabled
*/
void setScriptingEnabled(bool enabled)
{
QSettings settings;
settings.setValue("scripting/enabled", enabled);
}
/**
* @brief setSheetBackground
* Store the sheet background last picked in the diagram editor, so the
* next start opens on it for every project, old or new. @sa sheetBackground
* @param background
*/
void setSheetBackground(const SheetBackground &background)
{
QSettings settings;
//HexRgb, not HexArgb, on purpose: Diagram::background_color is
//temporarily given an alpha of 0 by the SVG export to make the
//background transparent, and that transient value must never end
//up here as a stored transparent sheet.
settings.setValue(QStringLiteral("diagrameditor/sheet_background_color"),
background.color.name(QColor::HexRgb));
settings.setValue(QStringLiteral("diagrameditor/sheet_background_custom"),
background.custom);
}
/**
* @brief sheetBackground
* @return the sheet background stored by setSheetBackground(), or the
* built-in default (white, following the system) when there is none.
* @sa setSheetBackground
*/
SheetBackground sheetBackground()
{
QSettings settings;
SheetBackground background;
background.custom = settings.value(
QStringLiteral("diagrameditor/sheet_background_custom"),
false).toBool();
const QColor stored(settings.value(
QStringLiteral("diagrameditor/sheet_background_color"),
QStringLiteral("#ffffff")).toString());
background.color = stored.isValid() ? stored : QColor(Qt::white);
return background;
}
/**
* @brief sheetBackgroundRecentColors
* @return the "recently used" sheet background colours, most recent
* first. Empty until the user has picked a non-preset colour.
* @sa setSheetBackgroundRecentColors
*/
QStringList sheetBackgroundRecentColors()
{
QSettings settings;
return settings.value(QStringLiteral("diagrameditor/sheet_background_recent")).toStringList();
}
/**
* @brief setSheetBackgroundRecentColors
* Store the "recently used" sheet background colours, most recent first.
* @sa sheetBackgroundRecentColors
* @param colors
*/
void setSheetBackgroundRecentColors(const QStringList &colors)
{
QSettings settings;
settings.setValue(QStringLiteral("diagrameditor/sheet_background_recent"), colors);
}
}
+28
View File
@@ -19,6 +19,8 @@
#define QETSETTINGS_H
#include <Qt>
#include <QColor>
#include <QStringList>
/**
*Interface to use QSettings in QElectroTech
@@ -32,6 +34,32 @@ namespace QetSettings
void setHdpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy policy);
Qt::HighDpiScaleFactorRoundingPolicy hdpiScaleFactorRoundingPolicy(
Qt::HighDpiScaleFactorRoundingPolicy default_policy = Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
bool scriptingEnabled();
void setScriptingEnabled(bool enabled);
bool scriptingForcedByEnvironment();
/**
@brief The sheet (folio) background the diagram editors draw.
Both halves are needed, not just the colour: with custom == false
the sheet follows the system ("Couleur systeme") and the views are
free to invert the lightness on a dark palette, while the very same
#ffffff with custom == true is an explicit "always white" that
must not be inverted. Storing only the colour would collapse the
two on the next start.
*/
struct SheetBackground
{
QColor color;
bool custom = false;
};
SheetBackground sheetBackground();
void setSheetBackground(const SheetBackground &background);
QStringList sheetBackgroundRecentColors();
void setSheetBackgroundRecentColors(const QStringList &colors);
}
#endif // QETSETTINGS_H