mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-01 23:54:13 +02:00
Merge branch 'master' into feature-3dmouse-spnav
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 &);
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 *>();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
+2780
-11
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 > 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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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_);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user