From a556204ba1f1b77a3e1ad3926eec1120c497edc7 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Thu, 1 Oct 2026 23:15:32 +1300 Subject: [PATCH 001/225] Edit several wires at once in the Selection properties panel Selecting more than one wire left the Selection properties panel empty, and so did a box selection over a single wire, because it also picks up the wire's number label. Now the panel opens for any number of selected wires. A selected wire label counts as its wire, once, so a box drawn over wires works. The fields show the first wire's values, a line above them says how many wires are selected, and an edit is applied to each wire field by field: setting the function on ten wires leaves each with its own number, cable and section. With "apply to all conductors of this potential" ticked, the potentials of all the selected wires follow the same rule. Each edit is one undo step. Fields whose values differ between the selected wires are not marked as such; that can follow if wanted. Co-Authored-By: Claude Opus 5.5 --- sources/factory/propertieseditorfactory.cpp | 39 +++--- .../ui/conductorpropertieseditorwidget.cpp | 118 ++++++++++++------ sources/ui/conductorpropertieseditorwidget.h | 15 ++- 3 files changed, 116 insertions(+), 56 deletions(-) diff --git a/sources/factory/propertieseditorfactory.cpp b/sources/factory/propertieseditorfactory.cpp index 48a39738b..83b554f79 100644 --- a/sources/factory/propertieseditorfactory.cpp +++ b/sources/factory/propertieseditorfactory.cpp @@ -82,22 +82,28 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor( PropertiesEditorWidget *editor, QWidget *parent) { + //Selecting a conductor's text label edits its parent conductor (#500), + //mirroring how double-clicking the label opens the conductor dialog. + //A rubber band over wires selects their labels too: each label + //counts as its conductor, once. + QList mapped; + for (QGraphicsItem *qgi : std::as_const(items)) { + if (auto *cti = qgraphicsitem_cast(qgi)) { + if (Conductor *parent_cond = cti->parentConductor()) { + qgi = parent_cond; + } + } + if (!mapped.contains(qgi)) { + mapped << qgi; + } + } + items = mapped; + const int count_ = items.size(); if (count_ == 0) { return nullptr; } QGraphicsItem *item = items.first(); - - //Selecting a conductor's text label edits its parent conductor (#500), - //mirroring how double-clicking the label opens the conductor dialog. - if (count_ == 1) { - if (auto *cti = qgraphicsitem_cast(item)) { - if (Conductor *parent_cond = cti->parentConductor()) { - items = {parent_cond}; - item = parent_cond; - } - } - } const int type_ = item->type(); //The editor widget can only edit one item @@ -124,18 +130,17 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor( false).toBool()) { return nullptr; } - //Prototype (#500): single-conductor editing in the dock. - if (count_ > 1) { - return nullptr; + QList conductors; + for (QGraphicsItem *qgi : std::as_const(items)) { + conductors << static_cast(qgi); } - auto conductor = static_cast(item); if (class_name == ConductorPropertiesEditorWidget::staticMetaObject.className()) { - static_cast(editor)->setConductor(conductor); + static_cast(editor)->setConductors(conductors); return editor; } - return new ConductorPropertiesEditorWidget(conductor, parent); + return new ConductorPropertiesEditorWidget(conductors, parent); } case Element::Type: //1000 { diff --git a/sources/ui/conductorpropertieseditorwidget.cpp b/sources/ui/conductorpropertieseditorwidget.cpp index 8b50fa02d..af3e0be33 100644 --- a/sources/ui/conductorpropertieseditorwidget.cpp +++ b/sources/ui/conductorpropertieseditorwidget.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -44,11 +45,11 @@ /** @brief ConductorPropertiesEditorWidget::ConductorPropertiesEditorWidget - @param conductor : conductor to edit + @param conductors : conductors to edit @param parent : parent widget */ ConductorPropertiesEditorWidget::ConductorPropertiesEditorWidget( - Conductor *conductor, QWidget *parent) : + const QList &conductors, QWidget *parent) : PropertiesEditorWidget(parent), m_cpw(new ConductorPropertiesWidget(this)) { @@ -76,12 +77,19 @@ ConductorPropertiesEditorWidget::ConductorPropertiesEditorWidget( QStringLiteral("diagrameditor/conductor_apply_all"), on); }); + //Shown only when several conductors are selected: the fields hold + //the first one's values, and only the edited ones reach the others. + m_count_label = new QLabel(this); + m_count_label->setWordWrap(true); + m_count_label->hide(); + auto *scroll = new QScrollArea(this); scroll->setWidgetResizable(true); scroll->setFrameShape(QFrame::NoFrame); scroll->setWidget(m_cpw); auto *layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(m_count_label); layout->addWidget(m_apply_all_cb); layout->addWidget(scroll); setMinimumWidth(120); @@ -100,7 +108,7 @@ ConductorPropertiesEditorWidget::ConductorPropertiesEditorWidget( gb->installEventFilter(this); setDisabled(true); - setConductor(conductor); + setConductors(conductors); } /** @@ -123,26 +131,56 @@ ConductorPropertiesEditorWidget::~ConductorPropertiesEditorWidget() {} /** - @brief ConductorPropertiesEditorWidget::setConductor - Set (or change) the conductor whose properties are edited. - @param conductor + @brief ConductorPropertiesEditorWidget::setConductors + Set (or change) the conductors whose properties are edited. + @param conductors */ -void ConductorPropertiesEditorWidget::setConductor(Conductor *conductor) +void ConductorPropertiesEditorWidget::setConductors( + const QList &conductors) { - if (!conductor) return; - if (m_conductor && m_conductor != conductor) - disconnect(m_conductor, &Conductor::propertiesChange, - this, &ConductorPropertiesEditorWidget::updateUi); - m_conductor = conductor; - //Keep the dock in sync when the conductor is edited elsewhere (e.g. the - //modal "Edit conductor" dialog); otherwise a stale snapshot would be - //written back on the next apply() and overwrite that change (issue #500). - connect(m_conductor, &Conductor::propertiesChange, - this, &ConductorPropertiesEditorWidget::updateUi, Qt::UniqueConnection); + if (conductors.isEmpty()) return; + + for (const QPointer &c : std::as_const(m_conductors)) + if (c) + disconnect(c, &Conductor::propertiesChange, + this, &ConductorPropertiesEditorWidget::updateUi); + m_conductors.clear(); + + for (Conductor *c : conductors) + { + m_conductors << c; + //Keep the dock in sync when a conductor is edited elsewhere (e.g. + //the modal "Edit conductor" dialog); otherwise a stale snapshot + //would be written back on the next apply() and overwrite that + //change (issue #500). + connect(c, &Conductor::propertiesChange, + this, &ConductorPropertiesEditorWidget::updateUi, + Qt::UniqueConnection); + } + + const int count = conductors.size(); + m_count_label->setText( + tr("%n conducteurs sélectionnés : seuls les champs modifiés leur sont appliqués.", + "selection properties panel", count)); + m_count_label->setVisible(count > 1); + setEnabled(true); updateUi(); } +/** + @brief ConductorPropertiesEditorWidget::firstConductor + @return the first edited conductor still alive, its values are the ones + shown, or nullptr +*/ +Conductor *ConductorPropertiesEditorWidget::firstConductor() const +{ + for (const QPointer &c : m_conductors) + if (c) + return c; + return nullptr; +} + /** @brief ConductorPropertiesEditorWidget::apply Push the edit onto the diagram's undo stack. @@ -153,9 +191,10 @@ void ConductorPropertiesEditorWidget::apply() // programmatically (updateUi/reset): mid-load the widget holds a partial // state that must not be committed onto the conductor. if (m_updating) return; - if (!m_conductor || !m_conductor->diagram()) return; + Conductor *first = firstConductor(); + if (!first || !first->diagram()) return; if (QUndoCommand *undo = associatedUndo()) - m_conductor->diagram()->undoStack().push(undo); + first->diagram()->undoStack().push(undo); m_shown = m_cpw->properties(); } @@ -242,7 +281,7 @@ void ConductorPropertiesEditorWidget::disconnectChangeSignals() */ void ConductorPropertiesEditorWidget::reset() { - if (!m_conductor) return; + if (!firstConductor()) return; m_updating = true; m_cpw->setProperties(m_shown); m_updating = false; @@ -250,13 +289,15 @@ void ConductorPropertiesEditorWidget::reset() /** @brief ConductorPropertiesEditorWidget::updateUi - Reload the widget from the conductor (e.g. when the selection changes). + Reload the widget from the first conductor (e.g. when the selection + changes). */ void ConductorPropertiesEditorWidget::updateUi() { - if (!m_conductor) return; + Conductor *first = firstConductor(); + if (!first) return; m_updating = true; - m_cpw->setProperties(m_conductor->properties()); + m_cpw->setProperties(first->properties()); //Read back rather than keep the conductor's own values: a value the //widget cannot show exactly must not count as an edit. m_shown = m_cpw->properties(); @@ -267,25 +308,32 @@ void ConductorPropertiesEditorWidget::updateUi() @brief ConductorPropertiesEditorWidget::associatedUndo @return the edit as one undo step, or nullptr if nothing changes. - Only the fields the user changed are written: the conductor keeps every - other value, including one the widget cannot show exactly. + Only the fields the user changed are applied, to each edited conductor: + with several conductors selected, each keeps its own text, function, + cable... unless that is the field being edited. - When "apply to all" is ticked, every conductor on the same potential is - updated too, in the same undo step (one undo reverts them all), as the - modal dialog does (ConductorPropertiesDialog::PropertiesDialog). + When "apply to all" is ticked, every conductor on the same potential as + an edited one is updated too, in the same undo step (one undo reverts + them all), as the modal dialog does + (ConductorPropertiesDialog::PropertiesDialog). */ QUndoCommand *ConductorPropertiesEditorWidget::associatedUndo() const { - if (!m_conductor) return nullptr; - const ConductorProperties new_properties = m_cpw->properties(); if (new_properties == m_shown) return nullptr; - QList targets {m_conductor}; - if (m_apply_all_cb && m_apply_all_cb->isChecked()) - for (Conductor *potential_conductor : m_conductor->relatedPotentialConductors()) - if (!targets.contains(potential_conductor)) - targets << potential_conductor; + const bool apply_all = m_apply_all_cb && m_apply_all_cb->isChecked(); + QList targets; + for (const QPointer &c : m_conductors) + { + if (!c) continue; + if (!targets.contains(c.data())) + targets << c.data(); + if (apply_all) + for (Conductor *potential_conductor : c->relatedPotentialConductors()) + if (!targets.contains(potential_conductor)) + targets << potential_conductor; + } auto *undo = new QUndoCommand(); int changed = 0; diff --git a/sources/ui/conductorpropertieseditorwidget.h b/sources/ui/conductorpropertieseditorwidget.h index a5f517738..f0b0d0d0c 100644 --- a/sources/ui/conductorpropertieseditorwidget.h +++ b/sources/ui/conductorpropertieseditorwidget.h @@ -23,18 +23,22 @@ #include #include +#include class Conductor; class ConductorPropertiesWidget; class QCheckBox; +class QLabel; /** @brief The ConductorPropertiesEditorWidget class Hosts the existing ConductorPropertiesWidget in the dockable selection- - properties panel, so a selected conductor can be edited in place like the + properties panel, so selected conductors can be edited in place like the other item types, instead of only through the modal dialog (issue #500). A pinned "apply to all conductors of the potential" checkbox (persisted) mirrors the modal dialog's option to propagate edits to the whole potential. + When several conductors are selected, the panel shows the first one and + applies to each only the fields the user changed. */ class ConductorPropertiesEditorWidget : public PropertiesEditorWidget { @@ -42,10 +46,11 @@ class ConductorPropertiesEditorWidget : public PropertiesEditorWidget public: explicit ConductorPropertiesEditorWidget( - Conductor *conductor = nullptr, QWidget *parent = nullptr); + const QList &conductors = {}, + QWidget *parent = nullptr); ~ConductorPropertiesEditorWidget() override; - void setConductor(Conductor *conductor); + void setConductors(const QList &conductors); void apply() override; void reset() override; @@ -60,11 +65,13 @@ class ConductorPropertiesEditorWidget : public PropertiesEditorWidget private: void connectChangeSignals(); void disconnectChangeSignals(); + Conductor *firstConductor() const; private: ConductorPropertiesWidget *m_cpw = nullptr; QCheckBox *m_apply_all_cb = nullptr; - Conductor *m_conductor = nullptr; + QLabel *m_count_label = nullptr; + QList> m_conductors; //What the widget showed before the edit: the fields that //differ from it are the ones the user changed. ConductorProperties m_shown; From 87bd172c447571f2ec8455fda0706e6b48a32458 Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Thu, 1 Oct 2026 13:04:04 +0200 Subject: [PATCH 002/225] Ctrl+Shift+V pastes at the origin point, Ctrl+V keeps pasting under the cursor Ctrl+V moves the pasted group under the pointer as before. The new Ctrl+Shift+V ("Coller au point d'origine", diagrameditor.paste_origin) leaves the group exactly where it was copied from and warps the OS cursor to the group's grid-snapped origin, so the paste baseline and the physical cursor agree and the group starts following the mouse from its original place instead of jumping to the pointer. The cursor warp is only trusted as the baseline when it verifiably lands (QCursor::pos() agrees with the target): QCursor::setPos() needs pointer focus and is a silent no-op on compositors without wp_pointer_warp_v1. Otherwise m_baseline_captured stays false and moveTo() re-baselines on the first real mouse move, so a refused warp costs at most the first movement rather than flinging the group across the folio. An origin outside the viewport is scrolled into view first, since the compositor rejects warp targets outside the window. --- sources/diagramevent/diagrameventaddpaste.cpp | 90 ++++++++++++++----- sources/diagramevent/diagrameventaddpaste.h | 31 +++++-- sources/qetdiagrameditor.cpp | 18 +++- sources/qetdiagrameditor.h | 1 + 4 files changed, 111 insertions(+), 29 deletions(-) diff --git a/sources/diagramevent/diagrameventaddpaste.cpp b/sources/diagramevent/diagrameventaddpaste.cpp index ae408d4b2..e4b1b4ba5 100644 --- a/sources/diagramevent/diagrameventaddpaste.cpp +++ b/sources/diagramevent/diagrameventaddpaste.cpp @@ -28,7 +28,9 @@ #include #include +#include #include +#include #include #include @@ -36,11 +38,18 @@ @brief DiagramEventAddPaste::DiagramEventAddPaste @param diagram : diagram to paste into @param start_pos : where the pasted items first appear, in scene - coordinates -- normally the cursor + coordinates -- normally the cursor. Only used for UnderCursor; an + AtOrigin paste stays at the coordinates it was copied from. + @param placement : UnderCursor moves the group under the cursor + (default), AtOrigin leaves it at its original XML position and warps + the OS cursor to the group's origin so baseline and screen match. */ - DiagramEventAddPaste::DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos) : + DiagramEventAddPaste::DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos, + PastePlacement placement) : DiagramEventInterface(diagram) { + m_placement = placement; + //DiagramEventInterface::init() is called by Diagram::setEventInterface //only when it is replacing an earlier interface, so call it here as //DiagramEventAddMacro does. @@ -106,21 +115,31 @@ qRound(p.x() / xGrid) * xGrid, qRound(p.y() / yGrid) * yGrid); }; - const QPointF grid_origin = snapGrid(start_pos); + const QPointF grid_origin = (m_placement == AtOrigin) + ? snapGrid(top_left) + : snapGrid(start_pos); - //Land the pasted content under the cursor immediately, rather than - //leaving it at the copied source's own coordinates: fromXml() above - //loads items at their original position purely because it doesn't - //know the target yet, not because that is where a paste should end - //up. The previous approach instead left items there and warped the - //OS cursor to match -- QCursor::setPos() is silently ignored by - //many window managers and compositors (Wayland in particular), so - //on any of those the warp simply never happened and the paste was - //left wherever it had originally been copied from, which could be - //anywhere on the folio -- exactly the "far from the cursor" bug. - const QPointF initial_delta = grid_origin - snapGrid(top_left); - for (auto *item : movable) { - item->setPos(item->pos() + initial_delta); + if (m_placement == UnderCursor) { + //Land the pasted content under the cursor immediately, rather than + //leaving it at the copied source's own coordinates: fromXml() above + //loads items at their original position purely because it doesn't + //know the target yet, not because that is where a paste should end + //up. The previous approach instead left items there and warped the + //OS cursor to match -- QCursor::setPos() is silently ignored by + //many window managers and compositors (Wayland in particular), so + //on any of those the warp simply never happened and the paste was + //left wherever it had originally been copied from, which could be + //anywhere on the folio -- exactly the "far from the cursor" bug. + const QPointF initial_delta = grid_origin - snapGrid(top_left); + for (auto *item : movable) { + item->setPos(item->pos() + initial_delta); + } + } else { + //AtOrigin: the items keep the coordinates they were copied from, + //so the copy stands exactly where the original stands. No delta is + //applied; what has to move is the cursor -- it is warped to the + //group's grid-snapped origin below, once m_group_origin is set, + //so the baseline and the physical cursor position agree. } //Store each item's now-placed position. moveTo() applies a @@ -138,10 +157,41 @@ conductor->updatePath(); } - //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; + //The baseline is the group's grid-snapped origin. For UnderCursor + //the group was just put there, so the baseline is known outright. + //For AtOrigin the group stayed where it was copied from and the + //OS cursor is warped to match instead -- but only a warp that + //verifiably landed may be trusted as the baseline: QCursor::setPos() + //needs pointer focus inside the target window (on Wayland it goes + //through wp_pointer_warp_v1, and is a silent no-op without it), and + //a baseline that assumes a warp which did not happen would move the + //whole group by the warp-sized delta on the first mouse move. So + //when the warp cannot be confirmed, leave m_baseline_captured + //false: moveTo() re-baselines from the first real mouse position + //instead, which costs at most the first movement but never flings + //the group across the folio. + if (m_placement == AtOrigin && !m_diagram->views().isEmpty()) { + if (auto *view = m_diagram->views().at(0)) { + //An origin outside the visible area cannot be warped to + //(the compositor rejects targets outside the window) and + //would leave the paste invisible, so scroll it into view + //first. + if (!view->viewport()->rect().contains(view->mapFromScene(m_group_origin))) { + view->ensureVisible(items_rect, 50, 50); + } + const QPoint view_pos = view->mapFromScene(m_group_origin); + const QPoint global_pos = view->viewport()->mapToGlobal(view_pos); + QCursor::setPos(global_pos); + if ((QCursor::pos() - global_pos).manhattanLength() <= 4) { + m_initial_cursor = m_group_origin; + m_baseline_captured = true; + } + } + } + if (m_placement == UnderCursor) { + m_initial_cursor = m_group_origin; + m_baseline_captured = true; + } m_diagram->clearSelection(); for (auto *item : movable) { diff --git a/sources/diagramevent/diagrameventaddpaste.h b/sources/diagramevent/diagrameventaddpaste.h index 2cb7eaf9f..fb56113cc 100644 --- a/sources/diagramevent/diagrameventaddpaste.h +++ b/sources/diagramevent/diagrameventaddpaste.h @@ -56,7 +56,21 @@ class DiagramEventAddPaste : public DiagramEventInterface | DiagramContent::Tables | DiagramContent::TerminalStrip; - DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos); + ///Where the group starts, relative to what was copied. + enum PastePlacement { + ///Move the group so its top left lands at \a start_pos + ///(normally the cursor): Ctrl+V, the copy appears under + ///the pointer and follows it. + UnderCursor, + ///Leave the group at its original XML position and warp + ///the OS cursor to the group's origin so the baseline + ///matches what is on screen: Ctrl+Shift+V, "paste at the + ///origin point". + AtOrigin + }; + + DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos, + PastePlacement placement = UnderCursor); ~DiagramEventAddPaste() override; void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override; @@ -79,14 +93,17 @@ class DiagramEventAddPaste : public DiagramEventInterface ///Each movable item's position relative to the group's top left, ///taken once so repeated moves cannot accumulate rounding drift. QHash m_relative_pos; - ///Where the group's grid-snapped top left was put when the paste - ///started, in scene coordinates -- the cursor, so the copy - ///appears under the pointer rather than on top of what was - ///copied. + ///Where this paste starts: UnderCursor moves the group to + ///start_pos, AtOrigin leaves it at its original XML position. + PastePlacement m_placement{UnderCursor}; + ///Where the group's grid-snapped top left sits when the paste + ///started, in scene coordinates -- under the cursor for + ///UnderCursor, the copy's own origin for AtOrigin. QPointF m_group_origin; ///Cursor position (scene coords) the delta-based movement in - ///moveTo() measures from. Equal to m_group_origin, since the - ///group is placed at the cursor. + ///moveTo() measures from. Equal to m_group_origin: either the + ///group was put there (UnderCursor) or the cursor was warped + ///there (AtOrigin). QPointF m_initial_cursor; ///Whether m_initial_cursor holds a usable baseline. A flag rather ///than testing m_initial_cursor.isNull(), which cannot tell "not diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index f826bbc52..ef5be8835 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -359,14 +359,17 @@ void QETDiagramEditor::setUpActions() m_cut = new QAction(QET::Icons::EditCut, tr("Co&uper"), this); m_copy = new QAction(QET::Icons::EditCopy, tr("Cop&ier"), this); m_paste = new QAction(QET::Icons::EditPaste, tr("C&oller"), this); + m_paste_origin = new QAction(QET::Icons::EditPaste, tr("Coller au point d'origine"), this); ShortcutManager::instance().registerAction(m_cut, "diagrameditor.cut", tr("Éditeur de schémas"), QKeySequence::Cut); ShortcutManager::instance().registerAction(m_copy, "diagrameditor.copy", tr("Éditeur de schémas"), QKeySequence::Copy); ShortcutManager::instance().registerAction(m_paste, "diagrameditor.paste", tr("Éditeur de schémas"), QKeySequence::Paste); + ShortcutManager::instance().registerAction(m_paste_origin, "diagrameditor.paste_origin", tr("Éditeur de schémas"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_V)); m_cut -> setStatusTip(tr("Transfère les éléments sélectionnés dans le presse-papier", "status bar tip")); m_copy -> setStatusTip(tr("Copie les éléments sélectionnés dans le presse-papier", "status bar tip")); m_paste -> setStatusTip(tr("Place les éléments du presse-papier sur le folio", "status bar tip")); + m_paste_origin -> setStatusTip(tr("Place les éléments du presse-papier à leur position d'origine et déplace le curseur vers ce point", "status bar tip")); connect(m_cut, &QAction::triggered, [this]() { if (currentDiagramView()) @@ -376,7 +379,10 @@ void QETDiagramEditor::setUpActions() if (currentDiagramView()) currentDiagramView()->copy(); }); - connect(m_paste, &QAction::triggered, [this]() { + + //Both paste shortcuts share one starter; they differ only in the + //placement mode handed to DiagramEventAddPaste. + const auto start_paste = [this](DiagramEventAddPaste::PastePlacement placement) { auto *dv = currentDiagramView(); if (!dv || !dv->diagram()) return; @@ -399,7 +405,13 @@ void QETDiagramEditor::setUpActions() const QPointF start_pos = dv->mapToScene(view_pos); dv->diagram()->setEventInterface( - new DiagramEventAddPaste(dv->diagram(), start_pos)); + new DiagramEventAddPaste(dv->diagram(), start_pos, placement)); + }; + connect(m_paste, &QAction::triggered, [start_paste]() { + start_paste(DiagramEventAddPaste::UnderCursor); + }); + connect(m_paste_origin, &QAction::triggered, [start_paste]() { + start_paste(DiagramEventAddPaste::AtOrigin); }); //Duplicate: copy the selection and place it at a configured, @@ -1239,6 +1251,7 @@ void QETDiagramEditor::setUpMenu() menu_edition -> addAction(m_cut); menu_edition -> addAction(m_copy); menu_edition -> addAction(m_paste); + menu_edition -> addAction(m_paste_origin); menu_edition -> addAction(m_duplicate); menu_edition -> addAction(m_configure_duplicate); menu_edition -> addAction(m_insert_last_element); @@ -2566,6 +2579,7 @@ void QETDiagramEditor::slot_updatePasteAction() // pour coller, il faut un schema ouvert et un schema dans le presse-papier m_paste -> setEnabled(editable_diagram && Diagram::clipboardMayContainDiagram()); + m_paste_origin -> setEnabled(editable_diagram && Diagram::clipboardMayContainDiagram()); } /** diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h index d8c59e29d..0345255d4 100644 --- a/sources/qetdiagrameditor.h +++ b/sources/qetdiagrameditor.h @@ -220,6 +220,7 @@ 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_paste_origin, ///< Same, at the copied position, cursor warped to the origin (Ctrl+Shift+V) *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 From 4c0f51c47632df626a5523759f791b6f1d77758f Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 2 Oct 2026 06:12:09 +1300 Subject: [PATCH 003/225] Arc: show the middle handle only with the size handles, uncovering the one under it The middle handle of a half arc sits exactly on the middle of the top or bottom edge of the ellipse -- a resize handle in Size mode, a skew handle in RotateSkew mode -- and, drawn last, it covered that handle (arummler, discussion #1203). It is now shown in Size mode only, where the resize handle under it is hidden instead: the middle handle already changes that height, with the ends kept in place. RotateSkew mode shows its skew handle again. Co-Authored-By: Claude Opus 5.5 --- sources/qetgraphicsitem/qetshapeitem.cpp | 26 ++++++++++++++++++++++-- sources/qetgraphicsitem/qetshapeitem.h | 2 +- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/sources/qetgraphicsitem/qetshapeitem.cpp b/sources/qetgraphicsitem/qetshapeitem.cpp index b2f3a152d..8c1be55e4 100644 --- a/sources/qetgraphicsitem/qetshapeitem.cpp +++ b/sources/qetgraphicsitem/qetshapeitem.cpp @@ -1746,11 +1746,33 @@ void QetShapeItem::rebuildHandles() updateArcBulgeVisibility(); } +/** + @brief QetShapeItem::updateArcBulgeVisibility + The middle handle of a half arc is shown with the Size handles only. + It sits exactly on one of them -- the middle of the top or bottom + edge (left or right for an upright arc) -- and on the top skew handle + in RotateSkew mode, and being drawn last it covered whichever one it + sat on. In Size mode the Resize handle under it is hidden instead: + the middle handle already changes that same height, keeping the ends + in place. +*/ void QetShapeItem::updateArcBulgeVisibility() { const int index = m_handleRoles.indexOf(HandleRole::ArcBulge); - if (index >= 0 && index < m_handler_vector.size()) - m_handler_vector.at(index)->setVisible(isAxisHalfArc()); + if (index < 0 || index >= m_handler_vector.size()) + return; + + const bool shown = isAxisHalfArc() && m_handleMode == HandleMode::Size; + m_handler_vector.at(index)->setVisible(shown); + + const QPointF middle = handlePositionFor(HandleRole::ArcBulge, 0); + for (int i = 0; i < m_handler_vector.size() && i < m_handleRoles.size(); ++i) + { + if (m_handleRoles.at(i) != HandleRole::Resize) + continue; + const QPointF p = handlePositionFor(HandleRole::Resize, m_handleSlot.at(i)); + m_handler_vector.at(i)->setVisible(!shown || QLineF(p, middle).length() > 0.01); + } } /** diff --git a/sources/qetgraphicsitem/qetshapeitem.h b/sources/qetgraphicsitem/qetshapeitem.h index ba4c2ae87..7d5d90c81 100644 --- a/sources/qetgraphicsitem/qetshapeitem.h +++ b/sources/qetgraphicsitem/qetshapeitem.h @@ -131,7 +131,7 @@ class QetShapeItem : public QetGraphicsItem Rotate, SkewEdge, Pivot, // RotateSkew mode CornerRadius, // Rectangle, always shown alongside Size handles ArcEndpoint, // Ellipse, always shown - ArcBulge, // Ellipse, shown only on a half arc (see isAxisHalfArc()) + ArcBulge, // Ellipse, Size mode, shown only on a half arc (see isAxisHalfArc()) PathAnchor, PathControlIn, PathControlOut // Polygon/Path, node-edit mode (see setPathNodes()) }; From 1c5fd5a284dcb38cbc59fa4b9ed8a0ea1dbffcda Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 2 Oct 2026 06:20:17 +1300 Subject: [PATCH 004/225] Multi-wire panel: stable first wire, mixed fields, locked text, one reload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of #1205: - The scene gives no order for its selection, so the "first" wire whose values the panel shows could change between two selections of the same wires. The wires are now sorted top to bottom, then left to right. - A text field the selected wires do not agree on (function, section, cable...) is shown blank with the hint "Plusieurs valeurs". Typing any value there, including the first wire's own, now differs from what was shown, so it reaches every wire: 1.5 mm² on a 1.5 and a 2.5 wire works. - The conductor text is locked while several wires are selected: the same number on all of them would make duplicates. "Apply to all conductors of this potential" with one wire selected still sets the number on the whole potential. - One edit of N wires emitted N signals, each reloading the panel. They are now gathered into one reload, which matters since a reload reads every selected wire to find the mixed fields. Selections and targets are de-duplicated with sets, and a wire reached through an earlier wire's potential is not walked again. An edit that changes nothing returns before any potential is walked. The rules move to sources/conductormultiedit.h, templates over the wire type, so tst_conductormultiedit can check them against a fake wire: order, targets with and without potentials, one undo step and its redo for three wires, and the mixed fields. tst_conductorapplychanges now gives the other wire the opposite value in every boolean field, so copying one by mistake fails the test. Co-Authored-By: Claude Opus 5.5 --- cmake/qet_compilation_vars.cmake | 1 + sources/conductormultiedit.h | 213 ++++++++++++++++++ sources/factory/propertieseditorfactory.cpp | 5 +- .../ui/conductorpropertieseditorwidget.cpp | 102 +++++---- sources/ui/conductorpropertieseditorwidget.h | 2 + sources/ui/conductorpropertieswidget.cpp | 44 +++- sources/ui/conductorpropertieswidget.h | 4 + tests/qttest/CMakeLists.txt | 15 ++ tests/qttest/tst_conductorapplychanges.cpp | 14 +- tests/qttest/tst_conductormultiedit.cpp | 189 ++++++++++++++++ 10 files changed, 533 insertions(+), 56 deletions(-) create mode 100644 sources/conductormultiedit.h create mode 100644 tests/qttest/tst_conductormultiedit.cpp diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 4593d5d56..9b1d9ab2f 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -188,6 +188,7 @@ set(QET_SRC_FILES ${QET_DIR}/sources/conductorprofile.h ${QET_DIR}/sources/conductorproperties.cpp ${QET_DIR}/sources/conductorproperties.h + ${QET_DIR}/sources/conductormultiedit.h ${QET_DIR}/sources/conductorsegment.cpp ${QET_DIR}/sources/conductorsegment.h ${QET_DIR}/sources/conductorsegmentprofile.h diff --git a/sources/conductormultiedit.h b/sources/conductormultiedit.h new file mode 100644 index 000000000..e234feae6 --- /dev/null +++ b/sources/conductormultiedit.h @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + 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 . +*/ +#ifndef CONDUCTORMULTIEDIT_H +#define CONDUCTORMULTIEDIT_H + +#include "QPropertyUndoCommand/qpropertyundocommand.h" +#include "conductorproperties.h" + +#include +#include +#include +#include + +#include + +/** + @brief ConductorMultiEdit + The rules for editing several conductors at once in the Selection + properties panel (#500). They are templates over the conductor type so + they can be tested without a diagram: T needs properties(), a + "properties" Q_PROPERTY, and relatedPotentialConductors() returning + QSet. +*/ +namespace ConductorMultiEdit +{ + /** + @brief sortByPosition + Sort top to bottom, then left to right, so the panel shows the same + "first" conductor whatever order the scene lists the selection in. + @param list : the conductors + @param pos_of : callable giving a conductor's position + */ + template + void sortByPosition(QList &list, PosOf pos_of) + { + std::stable_sort(list.begin(), list.end(), [&pos_of](T *a, T *b) { + const QPointF pa = pos_of(a); + const QPointF pb = pos_of(b); + if (pa.y() != pb.y()) + return pa.y() < pb.y(); + return pa.x() < pb.x(); + }); + } + + /** + @brief targets + @param selected : the edited conductors + @param apply_all : true to add every conductor of their potentials + @return each conductor an edit reaches, once + */ + template + QList targets(const QList &selected, bool apply_all) + { + QList list; + QSet seen; + for (T *conductor : selected) + { + //Already reached through an earlier conductor's potential: + //its own potential is the same one. + if (!conductor || seen.contains(conductor)) + continue; + seen.insert(conductor); + list << conductor; + if (!apply_all) + continue; + const QSet potential = conductor->relatedPotentialConductors(); + for (T *other : potential) + { + if (!seen.contains(other)) + { + seen.insert(other); + list << other; + } + } + } + return list; + } + + /** + @brief undo + @param targets : the conductors to change, see targets() + @param shown : the properties as the panel showed them + @param edited : the properties after the user's edit + @return one undo step giving each target the fields that differ + between shown and edited, or nullptr if no conductor changes + */ + template + QUndoCommand *undo(const QList &targets, + const ConductorProperties &shown, + const ConductorProperties &edited) + { + if (edited == shown) + return nullptr; + + auto *undo = new QUndoCommand(); + int changed = 0; + for (T *conductor : targets) + { + const ConductorProperties old_properties = conductor->properties(); + ConductorProperties properties = old_properties; + properties.applyChanges(shown, edited); + if (properties == old_properties) + continue; + + QVariant old_value, new_value; + old_value.setValue(old_properties); + new_value.setValue(properties); + new QPropertyUndoCommand(conductor, "properties", old_value, new_value, undo); + ++changed; + } + + if (!changed) + { + delete undo; + return nullptr; + } + undo->setText(changed == 1 + ? QCoreApplication::translate("ConductorPropertiesEditorWidget", + "Modifier les propriétés d'un conducteur", "undo caption") + : QCoreApplication::translate("ConductorPropertiesEditorWidget", + "Modifier les propriétés de plusieurs conducteurs", "undo caption")); + return undo; + } + + /// The fields a user types in. When the selected conductors do not + /// agree on one, the panel shows it blank. + enum TextField { + Text, Formula, Function, TensionProtocol, + WireColor, WireSection, Cable, Bus + }; + + inline QList textFields() + { + return {Text, Formula, Function, TensionProtocol, + WireColor, WireSection, Cable, Bus}; + } + + inline QString &textField(ConductorProperties &p, TextField field) + { + switch (field) + { + case Text: return p.text; + case Formula: return p.m_formula; + case Function: return p.m_function; + case TensionProtocol: return p.m_tension_protocol; + case WireColor: return p.m_wire_color; + case WireSection: return p.m_wire_section; + case Cable: return p.m_cable; + case Bus: break; + } + return p.m_bus; + } + + /** + @brief mixedTextFields + @return the text fields whose value is not the same in all of list + */ + inline QList mixedTextFields(QList list) + { + QList mixed; + if (list.size() < 2) + return mixed; + for (TextField field : textFields()) + { + const QString first = textField(list.first(), field); + for (ConductorProperties &p : list) + { + if (textField(p, field) != first) + { + mixed << field; + break; + } + } + } + return mixed; + } + + /** + @brief shown + @return what the panel shows for list: the first conductor's + properties, with the mixed text fields blank. Typing any value + in a blank field, even the first conductor's, then differs from + what was shown, so it is applied to every conductor. + */ + inline ConductorProperties shown(const QList &list, + const QList &mixed) + { + if (list.isEmpty()) + return ConductorProperties(); + ConductorProperties p = list.first(); + for (TextField field : mixed) + textField(p, field).clear(); + return p; + } +} + +#endif // CONDUCTORMULTIEDIT_H diff --git a/sources/factory/propertieseditorfactory.cpp b/sources/factory/propertieseditorfactory.cpp index 83b554f79..ba8b6acb3 100644 --- a/sources/factory/propertieseditorfactory.cpp +++ b/sources/factory/propertieseditorfactory.cpp @@ -38,6 +38,7 @@ #include "../ui/shapegraphicsitempropertieswidget.h" #include +#include #include /** @@ -87,13 +88,15 @@ PropertiesEditorWidget *PropertiesEditorFactory::propertiesEditor( //A rubber band over wires selects their labels too: each label //counts as its conductor, once. QList mapped; + QSet seen; for (QGraphicsItem *qgi : std::as_const(items)) { if (auto *cti = qgraphicsitem_cast(qgi)) { if (Conductor *parent_cond = cti->parentConductor()) { qgi = parent_cond; } } - if (!mapped.contains(qgi)) { + if (!seen.contains(qgi)) { + seen.insert(qgi); mapped << qgi; } } diff --git a/sources/ui/conductorpropertieseditorwidget.cpp b/sources/ui/conductorpropertieseditorwidget.cpp index af3e0be33..60dd4ba43 100644 --- a/sources/ui/conductorpropertieseditorwidget.cpp +++ b/sources/ui/conductorpropertieseditorwidget.cpp @@ -17,7 +17,7 @@ */ #include "conductorpropertieseditorwidget.h" -#include "../QPropertyUndoCommand/qpropertyundocommand.h" +#include "../conductormultiedit.h" #include "../diagram.h" #include "../qetgraphicsitem/conductor.h" #include "conductorpropertieswidget.h" @@ -143,18 +143,26 @@ void ConductorPropertiesEditorWidget::setConductors( for (const QPointer &c : std::as_const(m_conductors)) if (c) disconnect(c, &Conductor::propertiesChange, - this, &ConductorPropertiesEditorWidget::updateUi); + this, &ConductorPropertiesEditorWidget::scheduleUpdateUi); m_conductors.clear(); - for (Conductor *c : conductors) + //The scene gives no order for its selection: sort, so the same + //wires always show the same "first" one. + QList sorted = conductors; + ConductorMultiEdit::sortByPosition(sorted, [](Conductor *c) { + return c->sceneBoundingRect().topLeft(); + }); + + for (Conductor *c : std::as_const(sorted)) { m_conductors << c; //Keep the dock in sync when a conductor is edited elsewhere (e.g. //the modal "Edit conductor" dialog); otherwise a stale snapshot //would be written back on the next apply() and overwrite that - //change (issue #500). + //change (issue #500). One edit of N conductors emits N signals: + //they are gathered into one reload. connect(c, &Conductor::propertiesChange, - this, &ConductorPropertiesEditorWidget::updateUi, + this, &ConductorPropertiesEditorWidget::scheduleUpdateUi, Qt::UniqueConnection); } @@ -195,7 +203,21 @@ void ConductorPropertiesEditorWidget::apply() if (!first || !first->diagram()) return; if (QUndoCommand *undo = associatedUndo()) first->diagram()->undoStack().push(undo); - m_shown = m_cpw->properties(); + updateUi(); +} + +/** + @brief ConductorPropertiesEditorWidget::scheduleUpdateUi + Reload the widget once the current event is done, however many + conductors changed in it. +*/ +void ConductorPropertiesEditorWidget::scheduleUpdateUi() +{ + if (m_update_pending) return; + m_update_pending = true; + QMetaObject::invokeMethod(this, [this]() { + if (m_update_pending) updateUi(); + }, Qt::QueuedConnection); } /** @@ -289,15 +311,24 @@ void ConductorPropertiesEditorWidget::reset() /** @brief ConductorPropertiesEditorWidget::updateUi - Reload the widget from the first conductor (e.g. when the selection - changes). + Reload the widget from the conductors (e.g. when the selection + changes): the first one's values, with the text fields they do not + agree on left blank. */ void ConductorPropertiesEditorWidget::updateUi() { - Conductor *first = firstConductor(); - if (!first) return; + m_update_pending = false; + QList list; + for (const QPointer &c : std::as_const(m_conductors)) + if (c) + list << c->properties(); + if (list.isEmpty()) return; + + const auto mixed = ConductorMultiEdit::mixedTextFields(list); m_updating = true; - m_cpw->setProperties(first->properties()); + m_cpw->setProperties(ConductorMultiEdit::shown(list, mixed)); + m_cpw->setMixedTextFields(mixed); + m_cpw->setTextLocked(list.size() > 1); //Read back rather than keep the conductor's own values: a value the //widget cannot show exactly must not count as an edit. m_shown = m_cpw->properties(); @@ -319,47 +350,20 @@ void ConductorPropertiesEditorWidget::updateUi() */ QUndoCommand *ConductorPropertiesEditorWidget::associatedUndo() const { - const ConductorProperties new_properties = m_cpw->properties(); - if (new_properties == m_shown) return nullptr; + //Most calls are a field losing focus with nothing edited: answer + //before walking the potentials. + const ConductorProperties edited = m_cpw->properties(); + if (edited == m_shown) return nullptr; - const bool apply_all = m_apply_all_cb && m_apply_all_cb->isChecked(); - QList targets; + QList selected; for (const QPointer &c : m_conductors) - { - if (!c) continue; - if (!targets.contains(c.data())) - targets << c.data(); - if (apply_all) - for (Conductor *potential_conductor : c->relatedPotentialConductors()) - if (!targets.contains(potential_conductor)) - targets << potential_conductor; - } + if (c) + selected << c.data(); - auto *undo = new QUndoCommand(); - int changed = 0; - for (Conductor *conductor : std::as_const(targets)) - { - const ConductorProperties old_properties = conductor->properties(); - ConductorProperties properties = old_properties; - properties.applyChanges(m_shown, new_properties); - if (properties == old_properties) continue; - - QVariant old_value, new_value; - old_value.setValue(old_properties); - new_value.setValue(properties); - new QPropertyUndoCommand(conductor, "properties", old_value, new_value, undo); - ++changed; - } - - if (!changed) - { - delete undo; - return nullptr; - } - undo->setText(changed == 1 - ? tr("Modifier les propriétés d'un conducteur", "undo caption") - : tr("Modifier les propriétés de plusieurs conducteurs", "undo caption")); - return undo; + return ConductorMultiEdit::undo( + ConductorMultiEdit::targets( + selected, m_apply_all_cb && m_apply_all_cb->isChecked()), + m_shown, edited); } /** diff --git a/sources/ui/conductorpropertieseditorwidget.h b/sources/ui/conductorpropertieseditorwidget.h index f0b0d0d0c..ea915a706 100644 --- a/sources/ui/conductorpropertieseditorwidget.h +++ b/sources/ui/conductorpropertieseditorwidget.h @@ -65,6 +65,7 @@ class ConductorPropertiesEditorWidget : public PropertiesEditorWidget private: void connectChangeSignals(); void disconnectChangeSignals(); + void scheduleUpdateUi(); Conductor *firstConductor() const; private: @@ -77,6 +78,7 @@ class ConductorPropertiesEditorWidget : public PropertiesEditorWidget ConductorProperties m_shown; QList m_live_connections; bool m_updating = false; + bool m_update_pending = false; }; #endif // CONDUCTORPROPERTIESEDITORWIDGET_H diff --git a/sources/ui/conductorpropertieswidget.cpp b/sources/ui/conductorpropertieswidget.cpp index e184c6133..46729cfe7 100644 --- a/sources/ui/conductorpropertieswidget.cpp +++ b/sources/ui/conductorpropertieswidget.cpp @@ -191,6 +191,48 @@ void ConductorPropertiesWidget::setHiddenOneTextPerFolio(const bool &hide) { ui -> m_one_text_per_folio_cb -> setHidden(hide); } +/** + @brief ConductorPropertiesWidget::setMixedTextFields + Mark the text fields whose value differs between several edited + conductors: they are shown blank, with a hint saying why. + @param mixed +*/ +void ConductorPropertiesWidget::setMixedTextFields( + const QList &mixed) +{ + using namespace ConductorMultiEdit; + const QList> edits { + {Text, ui->m_text_le}, + {Formula, ui->m_formula_le}, + {Function, ui->m_function_le}, + {TensionProtocol, ui->m_tension_protocol_le}, + {WireColor, ui->m_wire_color_le}, + {WireSection, ui->m_wire_section_le}, + {Cable, ui->m_cable_le}, + {Bus, ui->m_bus_le}}; + + for (const auto &edit : edits) + edit.second->setPlaceholderText(mixed.contains(edit.first) + ? tr("Plusieurs valeurs", "several conductors, different values") + : QString()); +} + +/** + @brief ConductorPropertiesWidget::setTextLocked + Lock the conductor text while several conductors are edited at once: + the same number on all of them would make duplicates. + @param locked +*/ +void ConductorPropertiesWidget::setTextLocked(bool locked) +{ + m_text_locked = locked; + ui->m_text_le->setEnabled(!locked && ui->m_formula_le->text().isEmpty()); + ui->m_text_le->setToolTip(locked + ? tr("Plusieurs conducteurs sélectionnés : le texte se modifie " + "sur un seul conducteur à la fois.") + : tr("Texte")); +} + /** @brief ConductorPropertiesWidget::setDisabledShowText @param disable @@ -239,7 +281,7 @@ void ConductorPropertiesWidget::initWidget() connect(ui->m_multiwires_gb, &QGroupBox::toggled, [this](bool toggle) {this->ui->m_singlewire_gb->setChecked(!toggle);}); connect(ui->m_singlewire_gb, &QGroupBox::toggled, [this](bool toggle) {this->ui->m_multiwires_gb->setChecked(!toggle);}); - connect(ui->m_formula_le, &QLineEdit::textChanged, [this](QString text) {this->ui->m_text_le->setEnabled(text.isEmpty());}); + connect(ui->m_formula_le, &QLineEdit::textChanged, [this](QString text) {this->ui->m_text_le->setEnabled(!m_text_locked && text.isEmpty());}); ui->m_multiwires_gb->setChecked(true); ui->m_singlewire_gb->setChecked(true); #if TODO_LIST diff --git a/sources/ui/conductorpropertieswidget.h b/sources/ui/conductorpropertieswidget.h index a27ef868a..a52178269 100644 --- a/sources/ui/conductorpropertieswidget.h +++ b/sources/ui/conductorpropertieswidget.h @@ -18,6 +18,7 @@ #ifndef CONDUCTORPROPERTIESWIDGET_H #define CONDUCTORPROPERTIESWIDGET_H +#include "../conductormultiedit.h" #include "../conductorproperties.h" #include @@ -49,6 +50,8 @@ class ConductorPropertiesWidget : public QWidget void setHiddenOneTextPerFolio (const bool &hide); void setDisabledShowText (const bool &disable = true); void setHiddenAvailableAutonum (const bool &hide); + void setMixedTextFields(const QList &mixed); + void setTextLocked(bool locked); QComboBox *autonumComboBox() const; QPushButton *editAutonumPushButton() const; @@ -72,6 +75,7 @@ class ConductorPropertiesWidget : public QWidget Ui::ConductorPropertiesWidget *ui; ConductorProperties m_properties; QTextOrientationSpinBoxWidget *m_verti_select, *m_horiz_select; + bool m_text_locked = false; }; #endif // CONDUCTORPROPERTIESWIDGET_H diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index e339f3721..d8bb2b612 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -523,6 +523,21 @@ add_test(NAME tst_conductorapplychanges COMMAND tst_conductorapplychanges) target_include_directories(tst_conductorapplychanges PRIVATE ${QET_DIR}/sources) target_link_libraries(tst_conductorapplychanges PRIVATE Qt::Test Qt::Widgets Qt::Xml pugixml::pugixml) +# ConductorMultiEdit -- editing several wires at once in the Selection +# properties panel: order, which wires an edit reaches, one undo step for +# all of them, and fields shown blank because the wires disagree. +add_executable( + tst_conductormultiedit + tst_conductormultiedit.cpp + ${QET_DIR}/sources/conductorproperties.cpp + ${QET_DIR}/sources/QPropertyUndoCommand/qpropertyundocommand.cpp + ${QET_DIR}/sources/qet.cpp + ${QET_DIR}/sources/qeticons.cpp + ${QET_DIR}/sources/shortcutmanager.cpp) +add_test(NAME tst_conductormultiedit COMMAND tst_conductormultiedit) +target_include_directories(tst_conductormultiedit PRIVATE ${QET_DIR}/sources) +target_link_libraries(tst_conductormultiedit PRIVATE Qt::Test Qt::Widgets Qt::Xml pugixml::pugixml) + # DiagramContext::fromXml() -- the two readers (QDom for projects, pugixml # for element definitions in the collection) give the same values: stray # spaces trimmed, accents kept. diff --git a/tests/qttest/tst_conductorapplychanges.cpp b/tests/qttest/tst_conductorapplychanges.cpp index aa286b0a0..75be52264 100644 --- a/tests/qttest/tst_conductorapplychanges.cpp +++ b/tests/qttest/tst_conductorapplychanges.cpp @@ -102,9 +102,11 @@ class tst_conductorapplychanges : public QObject QTest::newRow(list.at(i).first) << i; } - // The shown wire is all variant 0, the user changes one field - // to variant 1, another selected wire is all variant 2: it - // takes that one field and keeps every other one. + // The shown wire is all variant 0 and the user changes one + // field to variant 1. The other wire holds variant 1 in every + // other field, so copying one of them by mistake (they are + // variant 0 in the edit) shows, booleans included; and variant + // 2 in the edited field, so not copying it shows too. void eachFieldAlone() { QFETCH(int, index); @@ -115,9 +117,11 @@ class tst_conductorapplychanges : public QObject set(after, 1); QVERIFY(after != before); - ConductorProperties other = all(2); + ConductorProperties other = all(1); + set(other, 2); ConductorProperties expected = other; set(expected, 1); + QVERIFY(expected != other); other.applyChanges(before, after); QVERIFY(other == expected); @@ -127,7 +131,7 @@ class tst_conductorapplychanges : public QObject void noEditChangesNothing() { const ConductorProperties shown = all(0); - ConductorProperties other = all(2); + ConductorProperties other = all(1); const ConductorProperties kept = other; other.applyChanges(shown, shown); QVERIFY(other == kept); diff --git a/tests/qttest/tst_conductormultiedit.cpp b/tests/qttest/tst_conductormultiedit.cpp new file mode 100644 index 000000000..9a6d36e92 --- /dev/null +++ b/tests/qttest/tst_conductormultiedit.cpp @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include + +#include + +#include "conductormultiedit.h" +#include "qetapp.h" + + // qet.cpp needs it; the application is not linked +QString QETApp::m_interface_language; + +// A wire as ConductorMultiEdit sees one: properties through the same +// Q_PROPERTY the undo command writes, a potential, and a position. +class FakeWire : public QObject +{ + Q_OBJECT + Q_PROPERTY(ConductorProperties properties READ properties WRITE setProperties) + + public: + FakeWire(const QString &num, const QString &function, + const QString &cable, QPointF pos) : + m_pos(pos) + { + m_properties.text = num; + m_properties.m_function = function; + m_properties.m_cable = cable; + } + + ConductorProperties properties() const { return m_properties; } + void setProperties(const ConductorProperties &p) { m_properties = p; } + QSet relatedPotentialConductors() const { return m_potential; } + QPointF pos() const { return m_pos; } + + // Joins wires into one potential; like the real one, a wire's + // potential lists the others, not itself. + static void join(const QList &wires) + { + for (FakeWire *w : wires) + for (FakeWire *o : wires) + if (o != w) + w->m_potential.insert(o); + } + + ConductorProperties m_properties; + + private: + QSet m_potential; + QPointF m_pos; +}; + +// The rules for editing several wires at once in the Selection properties +// panel (#500): which wire is shown first, which wires an edit reaches, +// what one undo step does to all of them, and the fields shown blank +// because the wires do not agree on them. +class tst_conductormultiedit : public QObject +{ + Q_OBJECT + + using Wires = QList; + + static ConductorProperties edited(ConductorProperties p, + QString ConductorProperties::*field, + const QString &value) + { + p.*field = value; + return p; + } + + private slots: + // Same wires, any selection order: same first wire + void firstWireIsStable() + { + FakeWire a("1", "", "", {50, 10}), b("2", "", "", {10, 10}), + c("3", "", "", {0, 90}); + const auto pos = [](FakeWire *w) { return w->pos(); }; + + Wires one {&c, &a, &b}, two {&a, &b, &c}; + ConductorMultiEdit::sortByPosition(one, pos); + ConductorMultiEdit::sortByPosition(two, pos); + QCOMPARE(one, (Wires {&b, &a, &c})); + QCOMPARE(two, one); + } + + void targetsWithoutPotential() + { + FakeWire a("1", "", "", {}), b("2", "", "", {}), c("3", "", "", {}); + FakeWire::join({&a, &c}); + QCOMPARE(ConductorMultiEdit::targets(Wires {&a, &b, &a}, false), + (Wires {&a, &b})); + } + + // Each wire once: a selected wire already reached through + // another one's potential is not added again + void targetsWithPotential() + { + FakeWire a("1", "", "", {}), b("1", "", "", {}), c("1", "", "", {}), + d("2", "", "", {}); + FakeWire::join({&a, &b, &c}); + const Wires t = ConductorMultiEdit::targets(Wires {&a, &d, &c}, true); + QCOMPARE(t.size(), 4); + QCOMPARE(QSet(t.begin(), t.end()), + (QSet {&a, &b, &c, &d})); + } + + // One edit of three wires: each takes the function, keeps its + // own number and cable; one undo restores all three exactly + void undoRedoThreeWires() + { + FakeWire a("101", "fnA", "W1", {}), b("102", "fnB", "W2", {}), + c("103", "fnC", "W3", {}); + const Wires wires {&a, &b, &c}; + const ConductorProperties before_a = a.properties(), + before_b = b.properties(), before_c = c.properties(); + + const ConductorProperties shown = a.properties(); + QUndoStack stack; + stack.push(ConductorMultiEdit::undo(wires, shown, + edited(shown, &ConductorProperties::m_function, "PWR"))); + QCOMPARE(stack.count(), 1); + + for (FakeWire *w : wires) + QCOMPARE(w->m_properties.m_function, QStringLiteral("PWR")); + QCOMPARE(b.m_properties.text, QStringLiteral("102")); + QCOMPARE(c.m_properties.m_cable, QStringLiteral("W3")); + + stack.undo(); + QVERIFY(a.properties() == before_a); + QVERIFY(b.properties() == before_b); + QVERIFY(c.properties() == before_c); + + stack.redo(); + QCOMPARE(c.m_properties.m_function, QStringLiteral("PWR")); + QCOMPARE(c.m_properties.text, QStringLiteral("103")); + } + + void noUndoWhenNothingChanges() + { + FakeWire a("1", "PWR", "", {}), b("2", "PWR", "", {}); + const ConductorProperties shown = a.properties(); + QCOMPARE(ConductorMultiEdit::undo(Wires {&a, &b}, shown, shown), + static_cast(nullptr)); + // An edit every wire already has + QCOMPARE(ConductorMultiEdit::undo(Wires {&a, &b}, edited(shown, + &ConductorProperties::m_function, "X"), shown), + static_cast(nullptr)); + } + + // Wires that disagree on a text field show it blank; typing the + // first wire's own value then reaches every wire (1.5 mm² on + // a 1.5 and a 2.5 wire) + void mixedFieldTakesFirstWiresValue() + { + FakeWire a("1", "fn", "", {}), b("2", "fn", "", {}); + a.m_properties.m_wire_section = "1.5"; + b.m_properties.m_wire_section = "2.5"; + const QList list {a.properties(), b.properties()}; + + const auto mixed = ConductorMultiEdit::mixedTextFields(list); + QCOMPARE(mixed, (QList { + ConductorMultiEdit::Text, ConductorMultiEdit::WireSection})); + + const ConductorProperties shown = ConductorMultiEdit::shown(list, mixed); + QVERIFY(shown.m_wire_section.isEmpty()); + QCOMPARE(shown.m_function, QStringLiteral("fn")); + + QUndoStack stack; + stack.push(ConductorMultiEdit::undo(Wires {&a, &b}, shown, + edited(shown, &ConductorProperties::m_wire_section, "1.5"))); + QCOMPARE(a.m_properties.m_wire_section, QStringLiteral("1.5")); + QCOMPARE(b.m_properties.m_wire_section, QStringLiteral("1.5")); + // Another field edited while the section is mixed leaves + // each wire's section alone + a.m_properties.m_wire_section = "1.5"; + b.m_properties.m_wire_section = "2.5"; + stack.push(ConductorMultiEdit::undo(Wires {&a, &b}, shown, + edited(shown, &ConductorProperties::m_function, "PWR"))); + QCOMPARE(a.m_properties.m_wire_section, QStringLiteral("1.5")); + QCOMPARE(b.m_properties.m_wire_section, QStringLiteral("2.5")); + } + + void oneWireIsNeverMixed() + { + FakeWire a("1", "fn", "", {}); + QVERIFY(ConductorMultiEdit::mixedTextFields({a.properties()}).isEmpty()); + } +}; + +QTEST_GUILESS_MAIN(tst_conductormultiedit) +#include "tst_conductormultiedit.moc" From 0bc27f00233c6f961e54f83fe1fc8b11d3ea4689 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 2 Oct 2026 06:20:17 +1300 Subject: [PATCH 005/225] Translations for the multi-wire panel strings (English, French) The three new strings only. A full update_translations run rewrites 33 files (+87,518/-52,666 lines, 184 new strings from other merged work), so that is left to the next translations refresh. Co-Authored-By: Claude Opus 5.5 --- lang/qet_en.ts | 20 ++++++++++++++++++++ lang/qet_fr.ts | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/lang/qet_en.ts b/lang/qet_en.ts index 4368b8d93..50d4860fa 100644 --- a/lang/qet_en.ts +++ b/lang/qet_en.ts @@ -1055,6 +1055,15 @@ Note: these options DO NOT allow or block auto numberings, only their update pol Conducteur Conductor + + + %n conducteurs sélectionnés : seuls les champs modifiés leur sont appliqués. + selection properties panel + + %n conductor selected: only the fields you change are applied to it. + %n conductors selected: only the fields you change are applied to them. + + ConductorPropertiesWidget @@ -1302,6 +1311,17 @@ Note: these options DO NOT allow or block auto numberings, only their update pol Style : Style: + + + Plusieurs valeurs + several conductors, different values + Several values + + + + Plusieurs conducteurs sélectionnés : le texte se modifie sur un seul conducteur à la fois. + Several conductors selected: the text is changed on one conductor at a time. + ConfigSaveLoaderWidget diff --git a/lang/qet_fr.ts b/lang/qet_fr.ts index 01e15a8c4..fccaeef64 100644 --- a/lang/qet_fr.ts +++ b/lang/qet_fr.ts @@ -1054,6 +1054,15 @@ Remarque: Ces options n'autorisent ou bloquent l'auto numérotation, s Conducteur + + + %n conducteurs sélectionnés : seuls les champs modifiés leur sont appliqués. + selection properties panel + + %n conducteur sélectionné : seuls les champs modifiés lui sont appliqués. + %n conducteurs sélectionnés : seuls les champs modifiés leur sont appliqués. + + ConductorPropertiesWidget @@ -1301,6 +1310,17 @@ Remarque: Ces options n'autorisent ou bloquent l'auto numérotation, s conductor style: dashed and dotted line + + + Plusieurs valeurs + several conductors, different values + + + + + Plusieurs conducteurs sélectionnés : le texte se modifie sur un seul conducteur à la fois. + + ConfigSaveLoaderWidget From 4b501521a5b150077991e104a2749bd41b20db97 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 2 Oct 2026 07:08:18 +1300 Subject: [PATCH 006/225] Add a sketch fillet: round the corner between two drawn lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Ajouter un congé" on the drawing toolbar, in Édition > Ajouter and in the command search. Click two lines one after the other, or once where they meet, and a small dialog asks for the radius (remembered for the next one); Enter makes the fillet. The tool stays on for the next corner; Escape or a right-click ends it. The two lines are trimmed -- or extended, when they did not reach each other -- to the points where the arc touches them, and an arc is added between them. For each line the part kept is the one on the side of the corner where it was clicked; a click on the corner keeps the longer part of each. Parallel or nearly aligned lines, and a radius too big for the lines, are refused with a status-bar message. The arc is an ordinary Ellipse shape with a start and end angle, as the Arc tool and the ellipse's arc handles save it, so nothing new is written to the file. It is added by its own small undo command rather than AddGraphicsObjectCommand, whose redo() calls setPos(), and QetShapeItem::setPos() snaps the outline's corner to the grid, which would pull the arc off the lines. One undo step undoes the fillet. New icon draw-fillet, light and dark, registered like the Align icons. Co-Authored-By: Claude Opus 5.5 --- cmake/qet_compilation_vars.cmake | 2 + ico/generated/scalable-22/draw-fillet.svg | 13 + ico/icon-themes.qrc | 28 ++ ico/scalable/16/draw-fillet.svg | 11 + ico/scalable/draw-fillet.svg | 13 + .../qet-dark/scalable-16/draw-fillet.svg | 11 + .../qet-dark/scalable-22/draw-fillet.svg | 13 + ico/themes/qet-dark/scalable/draw-fillet.svg | 13 + sources/diagramevent/diagrameventfillet.cpp | 343 ++++++++++++++++++ sources/diagramevent/diagrameventfillet.h | 62 ++++ sources/qetdiagrameditor.cpp | 7 + sources/qeticons.cpp | 2 + sources/qeticons.h | 1 + 13 files changed, 519 insertions(+) create mode 100644 ico/generated/scalable-22/draw-fillet.svg create mode 100644 ico/scalable/16/draw-fillet.svg create mode 100644 ico/scalable/draw-fillet.svg create mode 100644 ico/themes/qet-dark/scalable-16/draw-fillet.svg create mode 100644 ico/themes/qet-dark/scalable-22/draw-fillet.svg create mode 100644 ico/themes/qet-dark/scalable/draw-fillet.svg create mode 100644 sources/diagramevent/diagrameventfillet.cpp create mode 100644 sources/diagramevent/diagrameventfillet.h diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 4593d5d56..38e698ea2 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -352,6 +352,8 @@ set(QET_SRC_FILES ${QET_DIR}/sources/diagramevent/diagrameventaddimage.h ${QET_DIR}/sources/diagramevent/diagrameventaddshape.cpp ${QET_DIR}/sources/diagramevent/diagrameventaddshape.h + ${QET_DIR}/sources/diagramevent/diagrameventfillet.cpp + ${QET_DIR}/sources/diagramevent/diagrameventfillet.h ${QET_DIR}/sources/diagramevent/diagrameventaddpath.cpp ${QET_DIR}/sources/diagramevent/diagrameventaddpath.h ${QET_DIR}/sources/diagramevent/diagrameventaddtext.cpp diff --git a/ico/generated/scalable-22/draw-fillet.svg b/ico/generated/scalable-22/draw-fillet.svg new file mode 100644 index 000000000..d73a6778a --- /dev/null +++ b/ico/generated/scalable-22/draw-fillet.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/ico/icon-themes.qrc b/ico/icon-themes.qrc index 12b084713..45d6454ca 100644 --- a/ico/icon-themes.qrc +++ b/ico/icon-themes.qrc @@ -29,6 +29,7 @@ 128x128/settings.png scalable/align-horizontal-center.svg scalable/align-horizontal-left.svg + scalable/draw-fillet.svg scalable/align-horizontal-right.svg scalable/align-vertical-bottom.svg scalable/align-vertical-center.svg @@ -61,17 +62,29 @@ scalable/16/align-horizontal-center.svg scalable/16/align-horizontal-center.svg scalable/16/align-horizontal-left.svg + scalable/16/draw-fillet.svg generated/scalable-22/align-horizontal-left.svg + generated/scalable-22/draw-fillet.svg scalable/16/align-horizontal-left.svg + scalable/16/draw-fillet.svg scalable/align-horizontal-left.svg + scalable/draw-fillet.svg scalable/16/align-horizontal-left.svg + scalable/16/draw-fillet.svg scalable/16/align-horizontal-left.svg + scalable/16/draw-fillet.svg scalable/16/align-horizontal-left.svg + scalable/16/draw-fillet.svg generated/scalable-22/align-horizontal-left.svg + generated/scalable-22/draw-fillet.svg scalable/16/align-horizontal-left.svg + scalable/16/draw-fillet.svg scalable/align-horizontal-left.svg + scalable/draw-fillet.svg scalable/16/align-horizontal-left.svg + scalable/16/draw-fillet.svg scalable/16/align-horizontal-left.svg + scalable/16/draw-fillet.svg scalable/16/align-horizontal-right.svg generated/scalable-22/align-horizontal-right.svg scalable/16/align-horizontal-right.svg @@ -2372,6 +2385,7 @@ themes/qet-dark/128x128/settings.png themes/qet-dark/scalable/align-horizontal-center.svg themes/qet-dark/scalable/align-horizontal-left.svg + themes/qet-dark/scalable/draw-fillet.svg themes/qet-dark/scalable/align-horizontal-right.svg themes/qet-dark/scalable/align-vertical-bottom.svg themes/qet-dark/scalable/align-vertical-center.svg @@ -2394,7 +2408,9 @@ themes/qet-dark/scalable-16/align-horizontal-center.svg themes/qet-dark/scalable-22/align-horizontal-center.svg themes/qet-dark/scalable-16/align-horizontal-left.svg + themes/qet-dark/scalable-16/draw-fillet.svg themes/qet-dark/scalable-22/align-horizontal-left.svg + themes/qet-dark/scalable-22/draw-fillet.svg themes/qet-dark/scalable-16/align-horizontal-right.svg themes/qet-dark/scalable-22/align-horizontal-right.svg themes/qet-dark/scalable-16/align-vertical-bottom.svg @@ -2990,17 +3006,29 @@ themes/qet-dark/scalable-16/align-horizontal-center.svg themes/qet-dark/scalable-16/align-horizontal-center.svg themes/qet-dark/scalable-16/align-horizontal-left.svg + themes/qet-dark/scalable-16/draw-fillet.svg themes/qet-dark/scalable-22/align-horizontal-left.svg + themes/qet-dark/scalable-22/draw-fillet.svg themes/qet-dark/scalable-16/align-horizontal-left.svg + themes/qet-dark/scalable-16/draw-fillet.svg themes/qet-dark/scalable/align-horizontal-left.svg + themes/qet-dark/scalable/draw-fillet.svg themes/qet-dark/scalable-16/align-horizontal-left.svg + themes/qet-dark/scalable-16/draw-fillet.svg themes/qet-dark/scalable-16/align-horizontal-left.svg + themes/qet-dark/scalable-16/draw-fillet.svg themes/qet-dark/scalable-16/align-horizontal-left.svg + themes/qet-dark/scalable-16/draw-fillet.svg themes/qet-dark/scalable-22/align-horizontal-left.svg + themes/qet-dark/scalable-22/draw-fillet.svg themes/qet-dark/scalable-16/align-horizontal-left.svg + themes/qet-dark/scalable-16/draw-fillet.svg themes/qet-dark/scalable/align-horizontal-left.svg + themes/qet-dark/scalable/draw-fillet.svg themes/qet-dark/scalable-16/align-horizontal-left.svg + themes/qet-dark/scalable-16/draw-fillet.svg themes/qet-dark/scalable-16/align-horizontal-left.svg + themes/qet-dark/scalable-16/draw-fillet.svg themes/qet-dark/scalable-16/align-horizontal-right.svg themes/qet-dark/scalable-22/align-horizontal-right.svg themes/qet-dark/scalable-16/align-horizontal-right.svg diff --git a/ico/scalable/16/draw-fillet.svg b/ico/scalable/16/draw-fillet.svg new file mode 100644 index 000000000..d152e7f4e --- /dev/null +++ b/ico/scalable/16/draw-fillet.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/ico/scalable/draw-fillet.svg b/ico/scalable/draw-fillet.svg new file mode 100644 index 000000000..01c5a994a --- /dev/null +++ b/ico/scalable/draw-fillet.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/ico/themes/qet-dark/scalable-16/draw-fillet.svg b/ico/themes/qet-dark/scalable-16/draw-fillet.svg new file mode 100644 index 000000000..f1c06dbae --- /dev/null +++ b/ico/themes/qet-dark/scalable-16/draw-fillet.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/ico/themes/qet-dark/scalable-22/draw-fillet.svg b/ico/themes/qet-dark/scalable-22/draw-fillet.svg new file mode 100644 index 000000000..c0585e5e1 --- /dev/null +++ b/ico/themes/qet-dark/scalable-22/draw-fillet.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/ico/themes/qet-dark/scalable/draw-fillet.svg b/ico/themes/qet-dark/scalable/draw-fillet.svg new file mode 100644 index 000000000..8fea23abe --- /dev/null +++ b/ico/themes/qet-dark/scalable/draw-fillet.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/sources/diagramevent/diagrameventfillet.cpp b/sources/diagramevent/diagrameventfillet.cpp new file mode 100644 index 000000000..e5ffe8c82 --- /dev/null +++ b/sources/diagramevent/diagrameventfillet.cpp @@ -0,0 +1,343 @@ +/* + 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 . +*/ +#include "diagrameventfillet.h" + +#include "../QPropertyUndoCommand/qpropertyundocommand.h" +#include "../diagram.h" +#include "../qetapp.h" +#include "../qetdiagrameditor.h" +#include "../qetgraphicsitem/qetshapeitem.h" +#include "../qgimanager.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { + +const char *radiusSettingKey = "diagrameditor/fillet_radius"; + +/** + Adds the fillet's arc to the diagram exactly where it was computed. + AddGraphicsObjectCommand is not used: its redo() calls setPos(), + and QetShapeItem::setPos() snaps the outline's corner to the grid, + which would pull the arc off the two lines it has to touch. +*/ +class AddFilletArcCommand : public QUndoCommand +{ + public: + AddFilletArcCommand(QetShapeItem *arc, Diagram *diagram, QUndoCommand *parent) : + QUndoCommand(parent), m_arc(arc), m_diagram(diagram) + { m_diagram->qgiManager().manage(m_arc); } + ~AddFilletArcCommand() override + { m_diagram->qgiManager().release(m_arc); } + void redo() override { m_diagram->addItem(m_arc); } + void undo() override { m_diagram->removeItem(m_arc); } + + private: + QetShapeItem *m_arc; + Diagram *m_diagram; +}; + +qreal length(const QPointF &v) { return std::hypot(v.x(), v.y()); } +qreal dot(const QPointF &a, const QPointF &b) { return a.x() * b.x() + a.y() * b.y(); } + +qreal distanceToSegment(const QPointF &p, const QLineF &segment) +{ + const QPointF d = segment.p2() - segment.p1(); + const qreal len2 = dot(d, d); + if (len2 <= 0) + return length(p - segment.p1()); + const qreal t = qBound(qreal(0), dot(p - segment.p1(), d) / len2, qreal(1)); + return length(p - (segment.p1() + t * d)); +} + +QLineF sceneLine(const QetShapeItem *line) +{ + return QLineF(line->mapToScene(line->line().p1()), line->mapToScene(line->line().p2())); +} + + //Angle of a point on a circle, in the convention QPainterPath::arcTo() + //and QetShapeItem use: degrees, anticlockwise from three o'clock. +qreal arcAngle(const QPointF ¢re, const QPointF &p) +{ + return qRadiansToDegrees(std::atan2(-(p.y() - centre.y()), p.x() - centre.x())); +} + + //Same test as QetShapeItem::setStartAngle()/setEndAngle() use to snap + //an arc back to a full ellipse. +bool nearlyFullTurn(qreal span) +{ + qreal wrapped = std::fmod(span, 360.0); + if (wrapped < 0) wrapped += 360.0; + return wrapped < 5.0 || wrapped > 355.0; +} + +} // namespace + +DiagramEventFillet::DiagramEventFillet(Diagram *diagram) : + DiagramEventInterface(diagram) +{ + m_running = true; + //Deferred for the same reason as DiagramEventAddShape's hint: + //the previous tool's destructor clears the status bar after + //this constructor returns. + QTimer::singleShot(0, this, [this]() { showDefaultHint(); }); +} + +DiagramEventFillet::~DiagramEventFillet() +{ + clearFirstPick(); + if (m_diagram && !m_diagram->views().isEmpty()) + { + if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst())) + editor->statusBar()->clearMessage(); + } +} + +void DiagramEventFillet::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + event->setAccepted(true); + if (Q_UNLIKELY(m_diagram->isReadOnly())) + return; + + if (event->button() == Qt::RightButton) + { + if (m_first_line) { + clearFirstPick(); + showDefaultHint(); + } else { + m_running = false; + emit finish(); + } + return; + } + if (event->button() != Qt::LeftButton) + return; + + const QPointF pos = event->scenePos(); + const QList lines = linesNear(pos); + if (lines.isEmpty()) { + showHint(tr("Aucune ligne ici : cliquez sur une ligne dessinée sur le folio")); + return; + } + + if (!m_first_line) + { + //One click where two lines meet picks both of them + if (lines.size() >= 2) { + filletLines({lines.at(0), pos}, {lines.at(1), pos}); + return; + } + m_first = {lines.first(), pos}; + m_first_line = lines.first(); + m_diagram->clearSelection(); + m_first_line->setSelected(true); + showHint(tr("Cliquez sur la deuxième ligne ; clic droit : annuler")); + return; + } + + const auto other = std::find_if(lines.cbegin(), lines.cend(), + [this](QetShapeItem *line) { return line != m_first_line; }); + if (other == lines.cend()) { + showHint(tr("Cliquez sur une autre ligne ; clic droit : annuler")); + return; + } + const Pick first = m_first; + clearFirstPick(); + filletLines(first, {*other, pos}); +} + +/** + @brief DiagramEventFillet::linesNear + @return the Line shapes passing within a few screen pixels of + scenePos, nearest first. Only the items under the cursor are looked + at; the diagram is not walked. +*/ +QList DiagramEventFillet::linesNear(const QPointF &scenePos) const +{ + qreal scale = 1; + if (!m_diagram->views().isEmpty()) + scale = m_diagram->views().constFirst()->transform().m11(); + const qreal tolerance = 6.0 / qMax(scale, qreal(0.01)); + + QList> found; + const QRectF area(scenePos - QPointF(tolerance, tolerance), QSizeF(2 * tolerance, 2 * tolerance)); + for (QGraphicsItem *item : m_diagram->items(area)) + { + if (item->type() != QetShapeItem::Type) + continue; + auto *shape = static_cast(item); + if (shape->shapeType() != QetShapeItem::Line) + continue; + const qreal distance = distanceToSegment(scenePos, sceneLine(shape)); + if (distance <= tolerance) + found.append({distance, shape}); + } + std::sort(found.begin(), found.end(), + [](const auto &a, const auto &b) { return a.first < b.first; }); + + QList lines; + for (const auto &f : found) + lines.append(f.second); + return lines; +} + +/** + @brief DiagramEventFillet::filletLines + Rounds the corner between the two picked lines. For each line, the + part kept is the one on the side of the corner where it was clicked; + a click on the corner itself keeps the longer part. The other end of + each line moves to the point where the arc touches it -- shortening + the line, or lengthening it when the two lines did not reach each + other. One undo step undoes the whole fillet. +*/ +void DiagramEventFillet::filletLines(const Pick &a, const Pick &b) +{ + const QLineF s1 = sceneLine(a.line); + const QLineF s2 = sceneLine(b.line); + + QPointF corner; + if (s1.intersects(s2, &corner) == QLineF::NoIntersection) { + showHint(tr("Ces deux lignes sont parallèles : pas de congé possible")); + return; + } + + qreal scale = 1; + if (!m_diagram->views().isEmpty()) + scale = m_diagram->views().constFirst()->transform().m11(); + const qreal cornerTolerance = 6.0 / qMax(scale, qreal(0.01)); + + auto keptEnd = [&](const QLineF &segment, const QPointF &pick) -> int { + const QPointF ends[2] = {segment.p1(), segment.p2()}; + const QPointF towardPick = pick - corner; + const bool onCorner = length(towardPick) < cornerTolerance; + int best = -1; + for (int pass = 0; pass < 2 && best < 0; ++pass) { + qreal bestLength = -1; + for (int i = 0; i < 2; ++i) { + const QPointF v = ends[i] - corner; + if (pass == 0 && !onCorner && dot(v, towardPick) <= 0) + continue; + if (length(v) > bestLength) { best = i; bestLength = length(v); } + } + } + return best; + }; + const int keep1 = keptEnd(s1, a.pos); + const int keep2 = keptEnd(s2, b.pos); + const QPointF e1 = keep1 == 0 ? s1.p1() : s1.p2(); + const QPointF e2 = keep2 == 0 ? s2.p1() : s2.p2(); + const qreal len1 = length(e1 - corner); + const qreal len2 = length(e2 - corner); + if (len1 < 1e-6 || len2 < 1e-6) { + showHint(tr("Une des lignes s'arrête au coin : rien à arrondir de ce côté")); + return; + } + + const QPointF u1 = (e1 - corner) / len1; + const QPointF u2 = (e2 - corner) / len2; + const qreal theta = std::acos(qBound(qreal(-1), dot(u1, u2), qreal(1))); + if (qRadiansToDegrees(theta) > 174.0) { + showHint(tr("Ces deux lignes sont presque alignées : pas de congé possible")); + return; + } + + QSettings settings; + bool ok = false; + QWidget *parent = m_diagram->views().isEmpty() ? nullptr : m_diagram->views().constFirst(); + const qreal radius = QInputDialog::getDouble(parent, tr("Congé"), tr("Rayon :"), + settings.value(radiusSettingKey, 20.0).toDouble(), 0.1, 100000.0, 1, &ok); + if (!ok || !m_diagram) + return; + settings.setValue(radiusSettingKey, radius); + + const qreal tangentDistance = radius / std::tan(theta / 2); + if (tangentDistance > len1 + 1e-6 || tangentDistance > len2 + 1e-6) { + showHint(tr("Rayon trop grand pour ces lignes (%1 au plus)") + .arg(std::min(len1, len2) * std::tan(theta / 2), 0, 'f', 1)); + return; + } + + const QPointF t1 = corner + u1 * tangentDistance; + const QPointF t2 = corner + u2 * tangentDistance; + const QPointF bisector = (u1 + u2) / length(u1 + u2); + const QPointF centre = corner + bisector * (radius / std::sin(theta / 2)); + + //Each line keeps its picked end and moves its other end to the + //tangent point; p1/p2 keep their roles, mapped back into the + //line's own coordinates (it may be rotated or skewed). + auto newLine = [](QetShapeItem *line, int keep, const QPointF &kept, const QPointF &tangent) { + const QPointF p1 = keep == 0 ? kept : tangent; + const QPointF p2 = keep == 0 ? tangent : kept; + return QLineF(line->mapFromScene(p1), line->mapFromScene(p2)); + }; + + auto *arc = new QetShapeItem(centre - QPointF(radius, radius), + centre + QPointF(radius, radius), QetShapeItem::Ellipse); + arc->setPen(a.line->pen()); + qreal start = std::fmod(arcAngle(centre, t1), 360.0); + if (start < 0) start += 360.0; + const qreal span = std::fmod(arcAngle(centre, t2) - start + 540.0, 360.0) - 180.0; + //Set the two angles in an order whose intermediate state is not + //close to a full turn, which the setters would snap to an ellipse. + if (!nearlyFullTurn(360.0 - start)) { + arc->setStartAngle(start); + arc->setEndAngle(start + span); + } else { + arc->setEndAngle(start + span); + arc->setStartAngle(start); + } + + auto *undo = new QUndoCommand(tr("Ajouter un congé")); + new QPropertyUndoCommand(a.line, "line", a.line->line(), newLine(a.line, keep1, e1, t1), undo); + new QPropertyUndoCommand(b.line, "line", b.line->line(), newLine(b.line, keep2, e2, t2), undo); + new AddFilletArcCommand(arc, m_diagram, undo); + m_diagram->undoStack().push(undo); + showDefaultHint(); +} + +void DiagramEventFillet::clearFirstPick() +{ + if (m_first_line) + m_first_line->setSelected(false); + m_first_line.clear(); + m_first = Pick(); +} + +void DiagramEventFillet::showHint(const QString &text) const +{ + if (!m_diagram || m_diagram->views().isEmpty()) + return; + if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst())) + editor->statusBar()->showMessage(text); +} + +void DiagramEventFillet::showDefaultHint() const +{ + showHint(tr("Congé : cliquez sur deux lignes, ou une fois là où elles se rejoignent ; " + "Échap ou clic droit : terminer")); +} diff --git a/sources/diagramevent/diagrameventfillet.h b/sources/diagramevent/diagrameventfillet.h new file mode 100644 index 000000000..c12664f10 --- /dev/null +++ b/sources/diagramevent/diagrameventfillet.h @@ -0,0 +1,62 @@ +/* + 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 . +*/ +#ifndef DIAGRAMEVENTFILLET_H +#define DIAGRAMEVENTFILLET_H + +#include "diagrameventinterface.h" + +#include +#include + +class QetShapeItem; + +/** + @brief The DiagramEventFillet class + The "add a fillet" tool: rounds the corner between two drawn lines, + the way a sketch fillet does in a CAD program. Click the two lines + one after the other, or once where they meet; a dialog asks for the + radius. The lines are trimmed (or extended) to the points where the + arc touches them, and an arc is added between them -- an ordinary + Ellipse shape with a start and end angle, so nothing new is saved. +*/ +class DiagramEventFillet : public DiagramEventInterface +{ + Q_OBJECT + + public: + DiagramEventFillet(Diagram *diagram); + ~DiagramEventFillet() override; + void mousePressEvent (QGraphicsSceneMouseEvent *event) override; + + private: + struct Pick { + QetShapeItem *line = nullptr; + QPointF pos; // where the line was clicked, in scene coordinates + }; + + QList linesNear(const QPointF &scenePos) const; + void filletLines(const Pick &a, const Pick &b); + void clearFirstPick(); + void showHint(const QString &text) const; + void showDefaultHint() const; + + Pick m_first; + QPointer m_first_line; // m_first.line, guarded: the line can be deleted between the two clicks +}; + +#endif // DIAGRAMEVENTFILLET_H diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index 0218f7cf8..7add56cc7 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -38,6 +38,7 @@ #endif #include "diagramevent/diagrameventaddshape.h" #include "diagramevent/diagrameventaddpath.h" +#include "diagramevent/diagrameventfillet.h" #include "diagramevent/diagrameventaddtext.h" #include "diagramevent/diagrameventaddpaste.h" #include "diagramview.h" @@ -1063,6 +1064,7 @@ void QETDiagramEditor::setUpActions() QAction *add_arc = m_add_item_actions_group.addAction(QET::Icons::PartArc, tr("Ajouter un arc")); QAction *add_polyline = m_add_item_actions_group.addAction(QET::Icons::PartPolygon, tr("Ajouter une polyligne")); QAction *add_path = m_add_item_actions_group.addAction(QET::Icons::PartBezier, tr("Ajouter une courbe")); + QAction *add_fillet = m_add_item_actions_group.addAction(QET::Icons::DrawFillet, tr("Ajouter un congé")); QAction *add_terminal_strip = m_add_item_actions_group.addAction(QET::Icons::TerminalStrip, tr("Ajouter un plan de bornes")); add_text ->setStatusTip(tr("Ajoute un champ de texte sur le folio actuel")); @@ -1076,6 +1078,7 @@ void QETDiagramEditor::setUpActions() add_arc ->setStatusTip(tr("Ajoute un arc sur le folio actuel")); add_polyline ->setStatusTip(tr("Ajoute une polyligne sur le folio actuel")); add_path ->setStatusTip(tr("Ajoute une courbe de Bézier sur le folio actuel")); + add_fillet ->setStatusTip(tr("Arrondit le coin entre deux lignes du folio actuel")); add_terminal_strip->setStatusTip(tr("Ajoute un plan de bornier sur le folio actuel")); add_text ->setData(QStringLiteral("text")); @@ -1089,6 +1092,7 @@ void QETDiagramEditor::setUpActions() add_arc ->setData(QStringLiteral("arc")); add_polyline ->setData(QStringLiteral("polyline")); add_path ->setData(QStringLiteral("path")); + add_fillet ->setData(QStringLiteral("fillet")); add_terminal_strip->setData(QStringLiteral("terminal_strip")); add_text->setCheckable(true); @@ -1098,6 +1102,7 @@ void QETDiagramEditor::setUpActions() add_arc->setCheckable(true); add_polyline->setCheckable(true); add_path->setCheckable(true); + add_fillet->setCheckable(true); connect(&m_add_item_actions_group, &QActionGroup::triggered, this, &QETDiagramEditor::addItemGroupTriggered); //No default key, but an id: they can then be found by the command @@ -2049,6 +2054,8 @@ void QETDiagramEditor::addItemGroupTriggered(QAction *action) { diagram_event = new DiagramEventAddPath (d); } + else if (value == "fillet") + diagram_event = new DiagramEventFillet (d); else if (value == "image") { DiagramEventAddImage *deai = new DiagramEventAddImage(d); diff --git a/sources/qeticons.cpp b/sources/qeticons.cpp index 244de3a98..ae3c9ff4f 100644 --- a/sources/qeticons.cpp +++ b/sources/qeticons.cpp @@ -140,6 +140,7 @@ namespace QET { QIcon Orientations; QIcon PartArc; QIcon PartBezier; + QIcon DrawFillet; QIcon PartCircle; QIcon PartEllipse; QIcon PartLine; @@ -540,6 +541,7 @@ void QET::Icons::initIcons() Orientations = QIcon::fromTheme("orientations"); PartArc = QIcon::fromTheme("arc"); PartBezier = QIcon::fromTheme("draw-bezier-curves"); + DrawFillet = QIcon::fromTheme("draw-fillet"); PartCircle = QIcon::fromTheme("circle"); PartEllipse = QIcon::fromTheme("ellipse"); PartLine = QIcon::fromTheme("line"); diff --git a/sources/qeticons.h b/sources/qeticons.h index 31a4d0aa8..345e6ceda 100644 --- a/sources/qeticons.h +++ b/sources/qeticons.h @@ -148,6 +148,7 @@ namespace QET { extern QIcon Orientations; extern QIcon PartArc; extern QIcon PartBezier; + extern QIcon DrawFillet; extern QIcon PartCircle; extern QIcon PartEllipse; extern QIcon PartLine; From 7df9052a47e414a349f4c3b0068d406e1a899afe Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 2 Oct 2026 07:16:44 +1300 Subject: [PATCH 007/225] qet-mcp: set a folio's conductor defaults (set_conductor_default) The "one text per potential" switch (onetextperfolio) lives in a folio's conductor defaults, which no scripting call or qet_edit op could reach, so callers patched the saved .qet afterwards (#1178). qet.setConductorDefault(folio, property, value) sets onetextperfolio or any setConductorProperty name on a folio's defaults, or with folio -1 on the project's defaults that new folios copy. A change to onetextperfolio re-shows or hides the conductor texts at once, as the Folio properties dialog does. Not on the undo stack, like both dialogs. Co-Authored-By: Claude Opus 5.5 --- misc/qet-mcp/README.md | 7 +++ misc/qet-mcp/qet_mcp.py | 19 +++++++ misc/qet-mcp/test_qet_mcp.py | 26 +++++++++ sources/scripting/qetscriptapi.cpp | 84 ++++++++++++++++++++++++++++++ sources/scripting/qetscriptapi.h | 5 ++ 5 files changed, 141 insertions(+) diff --git a/misc/qet-mcp/README.md b/misc/qet-mcp/README.md index 37a3a2cb8..6c15ead1f 100644 --- a/misc/qet-mcp/README.md +++ b/misc/qet-mcp/README.md @@ -487,6 +487,13 @@ Python, plus the hang guard on `addConductor` and the database refresh in at names none of them and is refused, so address a potential from one of its leaves. Property names are the file's own, so `qet_conductors` reads back exactly what was set. +- **`set_conductor_default` sets a folio's conductor defaults**, the + Conductors tab of Folio properties: `onetextperfolio` (`"true"` shows one + wire number per potential on the folio), or any `set_conductor` property, + which conductors drawn later on that folio start from. `"folio": -1` sets + the project's defaults instead, which each folio added afterwards copies; + it does not change existing folios. Like the dialogs, it is not on the + undo stack. - **`link_elements` takes a folio for each end**, because a master and its slave are normally on different folios. Whether a pair may be linked is decided by QElectroTech's own `isLinkable()`, so a script cannot make a diff --git a/misc/qet-mcp/qet_mcp.py b/misc/qet-mcp/qet_mcp.py index 20485964d..fa7f35360 100755 --- a/misc/qet-mcp/qet_mcp.py +++ b/misc/qet-mcp/qet_mcp.py @@ -1425,6 +1425,10 @@ OPS = { "set_project_title": ("setProjectTitle", [("title", "str")]), "set_folio_border": ("setFolioBorder", [("folio", "folio"), ("property", "str"), ("value", "str")]), + # A folio's conductor defaults, or with "folio": -1 the project's ones + # that each new folio copies. + "set_conductor_default": ("setConductorDefault", [("folio", "folio"), ("property", "str"), + ("value", "str")]), "embed_title_block_template": ("embedTitleBlockTemplate", [("name", "str")]), "duplicate_elements": ("duplicateElements", [("folio", "folio"), ("elements", "elmts"), ("to_folio", "folio"), ("x", "num"), ("y", "num")]), @@ -1499,6 +1503,10 @@ CONDUCTOR_PROPERTIES = ["num", "formula", "function", "bus", "cable", "color2", "bicolor", "style", "dash-size", "condsize", "numsize", "displaytext"] +# A folio's conductor defaults take the same names, plus the folio-wide +# "one text per potential" switch, which no single conductor carries. +CONDUCTOR_DEFAULT_PROPERTIES = ["onetextperfolio"] + CONDUCTOR_PROPERTIES + # Methods this tool needs that only exist in a build carrying the drawing # verbs. Probed in the script rather than assumed, because the failure mode # otherwise is a TypeError on line N of a generated file the caller never @@ -1752,6 +1760,11 @@ def _build_script(operations: list, output: str) -> str: if op.get("kind") == "element_info" and not op.get("field"): raise ValueError(f"operation {i}: element_info needs a non-empty " f"\"field\" (information key)") + if (name == "set_conductor_default" + and op.get("property") not in CONDUCTOR_DEFAULT_PROPERTIES): + raise ValueError(f"operation {i}: unknown conductor default " + f"{op.get('property')!r}; expected one of " + f"{', '.join(CONDUCTOR_DEFAULT_PROPERTIES)}") if name == "set_folio_border" and op.get("property") not in FOLIO_BORDER_PROPERTIES: raise ValueError(f"operation {i}: unknown folio border property " f"{op.get('property')!r}; expected one of " @@ -2840,6 +2853,12 @@ TOOLS = [ "set_folio_border sets one " "of the folio frame's " + ", ".join(FOLIO_BORDER_PROPERTIES) + " (counts 1-99, sizes 1-1000, display-* true/false). " + "set_conductor_default sets one of a folio's conductor defaults " + "(Folio properties > Conductors): onetextperfolio (true/false, one " + "wire number per potential on the folio) or any set_conductor " + "property, which new conductors on that folio start from; \"folio\": " + "-1 sets the project's defaults that each folio added later copies. " + "Not on the undo stack. " "embed_title_block_template copies a template into the project from " "the common/company/custom collection that has it (only reachable if " "the binary's compiled-in template path resolves to something real -- " diff --git a/misc/qet-mcp/test_qet_mcp.py b/misc/qet-mcp/test_qet_mcp.py index 422c4a085..3bdff5052 100644 --- a/misc/qet-mcp/test_qet_mcp.py +++ b/misc/qet-mcp/test_qet_mcp.py @@ -401,6 +401,8 @@ class EditValidation(unittest.TestCase): "regex": False, "case_sensitive": True}], "set_project_title": [{"op": "set_project_title", "title": "T"}], "set_folio_border": [f, {"op": "set_folio_border", "folio": "$f", "property": "columns", "value": "10"}], + "set_conductor_default": [f, {"op": "set_conductor_default", "folio": "$f", + "property": "onetextperfolio", "value": "true"}], "embed_title_block_template": [{"op": "embed_title_block_template", "name": "default"}], "duplicate_elements": el + [{"op": "duplicate_elements", "id": "d", "folio": "$f", "elements": ["$e"], "to_folio": "$f", "x": 50, "y": 50}], @@ -525,6 +527,8 @@ class EditValidation(unittest.TestCase): ("unknown kind", {"op": "search_and_replace", "kind": "folio", "field": "x", "pattern": "a", "replacement": "b", "regex": False, "case_sensitive": False}), + ("unknown conductor default", {"op": "set_conductor_default", "folio": 0, + "property": "label", "value": "x"}), ("unknown conductor field", {"op": "search_and_replace", "kind": "conductor", "field": "label", "pattern": "a", "replacement": "b", "regex": False, "case_sensitive": False}), @@ -3685,6 +3689,28 @@ class Integration(unittest.TestCase): with self.subTest(prop=prop, value=good): self.ok(self.sb.edit(base, [{"op": "set_folio_border", "folio": 0, "property": prop, "value": good}])) + def test_conductor_defaults(self): + """The folio's "one text per potential" switch (#1178), and the + project's defaults that a folio added later copies, as saved.""" + base = self.sb.new(folios=1) + r = self.ok(self.sb.edit(base, [ + {"op": "set_conductor_default", "folio": 0, "property": "onetextperfolio", "value": "true"}, + {"op": "set_conductor_default", "folio": 0, "property": "conductor_section", "value": "1.5"}, + {"op": "set_conductor_default", "folio": -1, "property": "onetextperfolio", "value": "true"}, + {"op": "add_folio"}])) + root = ET.parse(r["output"]).getroot() + folios = [d.find("defaultconductor") for d in root.iter("diagram")] + self.assertEqual([d.get("onetextperfolio") for d in folios], ["1", "1"], + "set on folio 0; copied from the project's defaults by the new folio") + self.assertEqual(folios[0].get("conductor_section"), "1.5") + self.assertEqual(root.find("newdiagrams/conductors").get("onetextperfolio"), "1") + for prop, bad in (("onetextperfolio", "yes"), ("numsize", "0"), ("color", "notacolour")): + with self.subTest(prop=prop, value=bad): + self.assertFalse(self.sb.edit(base, [{"op": "set_conductor_default", "folio": 0, + "property": prop, "value": bad}])["ok"]) + self.assertFalse(self.sb.edit(base, [{"op": "set_conductor_default", "folio": 5, + "property": "onetextperfolio", "value": "true"}])["ok"]) + # ---- duplicating ---- def test_duplicate_returns_copies_in_the_order_asked(self): diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index 53f3a41aa..e312aaa39 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -40,6 +40,7 @@ #include "../qetproject.h" #include "../qetresult.h" #include "../qetgraphicsitem/conductor.h" +#include "../qetgraphicsitem/conductortextitem.h" #include "../conductorsegment.h" #include "../qetgraphicsitem/diagramimageitem.h" @@ -3701,6 +3702,89 @@ bool QetScriptApi::setFolioBorder(int folioIndex, const QString &property, const return true; } +/** + @brief QetScriptApi::conductorDefault + One property of the conductor defaults: a folio's (what its new + conductors start from, and where "one text per potential" lives), or + with folioIndex -1, the project's, which each new folio copies. + Empty if the folio or the property does not exist. +*/ +QString QetScriptApi::conductorDefault(int folioIndex, const QString &property) const +{ + if (!m_project) return QString(); + ConductorProperties p; + if (folioIndex == -1) { + p = m_project->defaultConductorProperties(); + } else { + const QList diagrams = m_project->diagrams(); + if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString(); + p = diagrams.at(folioIndex)->defaultConductorProperties; + } + if (property == QLatin1String("onetextperfolio")) + return p.m_one_text_per_folio ? QStringLiteral("true") : QStringLiteral("false"); + return conductorPropertyValue(p, property); +} + +/** + @brief QetScriptApi::setConductorDefault + Set one property of the conductor defaults, under the names + setConductorProperty() takes plus "onetextperfolio" (true/false: one + number per potential on each folio). Like the Folio properties and + Project properties dialogs, this is not on the undo stack: neither + dialog has an undo command for it. +*/ +bool QetScriptApi::setConductorDefault(int folioIndex, const QString &property, const QString &value) +{ + if (!m_project) return false; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.setConductorDefault: project is read-only")); + return false; + } + Diagram *diagram = nullptr; + if (folioIndex != -1) { + const QList diagrams = m_project->diagrams(); + if (folioIndex < 0 || folioIndex >= diagrams.count()) return false; + diagram = diagrams.at(folioIndex); + } + const ConductorProperties old_p = diagram ? diagram->defaultConductorProperties + : m_project->defaultConductorProperties(); + ConductorProperties new_p = old_p; + if (property == QLatin1String("onetextperfolio")) { + const QString v = value.toLower(); + if (v != QLatin1String("true") && v != QLatin1String("false")) { + log(QStringLiteral("qet.setConductorDefault: onetextperfolio is true or false, not '%1'").arg(value)); + return false; + } + new_p.m_one_text_per_folio = (v == QLatin1String("true")); + } else if (!setConductorPropertyValue(new_p, property, value)) { + log(QStringLiteral("qet.setConductorDefault: cannot set '%1' to '%2'; properties are onetextperfolio, %3") + .arg(property, value, conductorPropertyNames().join(QStringLiteral(", ")))); + return false; + } + if (new_p == old_p) return true; + + if (!diagram) { + m_project->setDefaultConductorProperties(new_p); + return true; + } + diagram->defaultConductorProperties = new_p; + // Show or hide the conductor texts now, as the Folio properties dialog + // does (DiagramPropertiesDialog), or an export later in this run would + // draw them as they were. + if (new_p.m_one_text_per_folio != old_p.m_one_text_per_folio) + { + const QList conductor_list = diagram->conductors(); + for (Conductor *c : conductor_list) + { + const ConductorProperties cp = c->properties(); + c->textItem()->setVisible(cp.type == ConductorProperties::Multi && cp.m_show_text); + } + for (Conductor *c : conductor_list) + c->calculateTextItemPosition(); + } + return true; +} + /** @brief QetScriptApi::elementGeometry Where an element is: x and y are its origin (what setElementPosition() diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h index dff3225e7..35ef9c5d5 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -538,6 +538,11 @@ class QetScriptApi : public QObject Q_INVOKABLE QString folioBorder(int folioIndex, const QString &property) const; Q_INVOKABLE bool setFolioBorder(int folioIndex, const QString &property, const QString &value); + // -- the conductor defaults of a folio (Folio properties > Conductors), + // or with folioIndex -1, the project's defaults for new folios -- + Q_INVOKABLE QString conductorDefault(int folioIndex, const QString &property) const; + Q_INVOKABLE bool setConductorDefault(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); From 0817c7dd87c8e234a965b4cebd13a89175f7cfb6 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 1 Oct 2026 23:29:23 +0500 Subject: [PATCH 008/225] Rassian translate --- lang/qet_ru.ts | 800 +++++++++++++++++++++++++------------------------ 1 file changed, 402 insertions(+), 398 deletions(-) diff --git a/lang/qet_ru.ts b/lang/qet_ru.ts index b4dffbe76..041bdfa01 100644 --- a/lang/qet_ru.ts +++ b/lang/qet_ru.ts @@ -46,7 +46,7 @@ Licenses - + Лицензионное соглашение @@ -204,37 +204,37 @@ Traduction en serbe - + Перевод на сербский Traduction en ukrainien - + Перевод на украинский Traduction en norvégien - + Перевод на норвежский Traduction en japonais - + Перевод на японский Traduction en coréen - + Перевод на корейский Traduction en mongol - + Перевод на монгольский Traduction en slovène - + Перевод на словенский @@ -304,7 +304,7 @@ Redimensionneur d'éléments Element scaler - Изменение размера элемента + Изменение размера элемента @@ -316,13 +316,13 @@ Convertisseur d'élément DXF Dxf2elmt - Конвертер элементов DXF + Конвертер элементов DXF Outil de traduction d'éléments Qet_translate - Инструмент перевода элементов + Инструмент перевода элементов @@ -741,7 +741,7 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Aperçu - Предпросмотр + Предпросмотр @@ -768,7 +768,7 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Impossible d'enregistrer la nomenclature dans %1. %2 - Не удалось сохранить спецификацию в %1. + Не удалось сохранить спецификацию в %1. %2 @@ -791,13 +791,13 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Requête refusée - Запрос отклонен + Запрос отклонен Erreur dans la requête : %1 - Ошибка в запросе: + Ошибка в запросе: %1 @@ -807,25 +807,25 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Créer une copie de sauvegarde ? window title - Создать резервную копию? + Создать резервную копию? Souhaitez-vous créer une copie de sauvegarde ? dialog message - Хотите создать резервную копию? + Хотите создать резервную копию? Oui yes button - Да + Да Non no button - Нет + Нет @@ -877,7 +877,7 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Cliquez pour choisir une couleur - Нажмите, чтобы выбрать цвет + Нажмите, чтобы выбрать цвет @@ -932,74 +932,74 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Noir - Чёрный + Чёрный Marron - Коричневый + Коричневый Gris - Серый + Серый Bleu - Синий + Синий Vert - Зелёный + Зелёный Rouge - Красный + Красный Orange - Оранжевый + Оранжевый Violet - Фиолетовый + Фиолетовый Blanc - Белый + Белый Couleur de conducteur - Цвет провода + Цвет провода Applique une couleur aux conducteurs sélectionnés, et l'utilise pour les prochains conducteurs tracés status bar tip - Применяет цвет к выбранным проводникам и использует его для последующих начерченных проводников + Применяет цвет к выбранным проводникам и использует его для последующих начерченных проводников Récemment utilisées - Недавно использованные + Недавно использованные Autre couleur… - Другой цвет… + Другой цвет… Modifier la couleur de %n conducteur(s) undo caption - + Изменить цвет %n проводника Изменить цвет %n проводников Изменить цвет %n проводников @@ -1008,7 +1008,7 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Choisir une couleur de conducteur - Выбрать цвет провода + Выбрать цвет провода @@ -1041,7 +1041,7 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Appliquer les propriétés à l'ensemble des conducteurs de ce potentiel - Применить свойства ко всем проводникам этого потенциала + Применить свойства ко всем проводникам этого потенциала @@ -1482,27 +1482,27 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Enregistrer... - Сохранить... + Сохранить... Enregistrer le rapport de diagnostic - Сохранить отчет о диагностике + Сохранить отчет о диагностике Fichiers texte (*.txt);;Tous les fichiers (*) - Текстовые файлы (*.txt);;Все файлы (*) + Текстовые файлы (*.txt);;Все файлы (*) Erreur - Ошибка + Ошибка Impossible d'écrire dans le fichier « %1 ». - Не удалось записать данные в файл «%1». + Не удалось записать данные в файл «%1». @@ -1518,63 +1518,63 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Blanc - Белый + Белый Blanc cassé - Слоновой кости + Слоновой кости Gris clair - Светло-серый + Светло-серый Gris - Серый + Серый Gris foncé - Тёмно-серый + Тёмно-серый Noir - Чёрный + Чёрный Couleur de fond du folio - Цвет фона листа + Цвет фона листа Choisir la couleur de fond du folio status bar tip - Выбрать цвет фона листа + Выбор цвета фона листа Couleur système - Цвет системы + Цвет системы Récemment utilisées - Недавно использованные + Недавно использованные Autre couleur… - Другой цвет… + Другой цвет… Choisir une couleur de fond - Выбрать цвет фона + Выберите цвет фона @@ -1642,7 +1642,7 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Clic : positionner à la taille d'origine. Cliquer-glisser : positionner et redimensionner. Clic droit : pivoter de 90°. Ctrl+molette : ajuster la taille. - Клик: разместить в исходном размере. Клик и перетаскивание: разместить и изменить размер. Клик правой кнопкой мыши: повернуть на 90°. Ctrl + колесико: настроить размер. + Клик: разместить в исходном размере. нажать и потянуть: разместить и изменить размер. Клик правой кнопкой мыши: повернуть на 90°. Ctrl + колесико: настроить размер. @@ -1659,7 +1659,7 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Clic: point anguleux. Cliquer-glisser: point courbe. Clic sur le premier point: fermer. Échap/Entrée: terminer. Clic droit: annuler le dernier point. - Клик: угловая точка. Клик и перетаскивание: изогнутая точка. Клик по первой точке: закрыть. Esc/Enter: завершить. Клик правой кнопкой мыши: отменить последнюю точку. + Клик: угловая точка. Нажать и потянуть: изогнутая точка. Клик по первой точке: завершить. Esc/Enter: завершить. Клик правой кнопкой мыши: отменить последнюю точку. @@ -1667,37 +1667,37 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Clic gauche : positionner le point de départ (Ctrl = position libre) - Клик левой кнопкой мыши: установить начальную точку (Ctrl = произвольное положение) + Клик левой кнопкой мыши: установить начальную точку (Ctrl = произвольное положение) Clic gauche : positionner le premier coin (Ctrl = point central, position libre) - Клик левой кнопкой мыши: задать положение первого угла (Ctrl — центральная точка, произвольное положение) + Клик левой кнопкой мыши: задать положение первого угла (Ctrl — центральная точка, произвольное положение) Clic gauche : positionner le premier point (Ctrl = position libre) - Клик левой кнопкой мыши: установить первую точку (Ctrl = свободное позиционирование) + Клик левой кнопкой мыши: установить первую точку (Ctrl = свободное позиционирование) Clic gauche : positionner le point final (Ctrl = position libre) ; clic droit : annuler - Клик левой кнопкой мыши: установить конечную точку (Ctrl = произвольное положение); клик правой кнопкой мыши: отменить + Клик левой кнопкой мыши: установить конечную точку (Ctrl = произвольное положение); клик правой кнопкой мыши: отменить Clic gauche : positionner le coin opposé (Maj = carré, Ctrl = depuis le centre + position libre, Ctrl+Maj = carré centré) ; clic droit : annuler - Клик левой кнопкой мыши: установить положение противоположного угла (Shift = квадрат, Ctrl = от центра + произвольное положение, Ctrl+Shift = центрированный квадрат); клик правой кнопкой мыши: отменить + Клик левой кнопкой мыши: установить положение противоположного угла (Shift = квадрат, Ctrl = от центра + произвольное положение, Ctrl+Shift = центрированный квадрат); клик правой кнопкой мыши: отменить Clic gauche : positionner le coin opposé (Maj = cercle, Ctrl = depuis le centre + position libre, Ctrl+Maj = cercle centré) ; clic droit : annuler - Клик левой кнопкой мыши: установить положение противоположного угла (Shift = круг, Ctrl = от центра + произвольное положение, Ctrl+Shift = центрированный круг); клик правой кнопкой мыши: отменить + Клик левой кнопкой мыши: установить положение противоположного угла (Shift = круг, Ctrl = от центра + произвольное положение, Ctrl+Shift = центрированный круг); клик правой кнопкой мыши: отменить Clic gauche : point suivant ; double-clic ou Entrée : terminer ; clic droit : annuler le dernier point - Клик левой кнопкой мыши: следующая точка; двойной клик или клавиша «Enter»: завершить; клик правой кнопкой мыши: отменить последнюю точку + Клик левой кнопкой мыши: следующая точка; двойной клик или клавиша «Enter»: завершить; клик правой кнопкой мыши: отменить последнюю точку @@ -1720,12 +1720,12 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Glisser un coin/bord : redimensionner (Ctrl = depuis le centre, Maj = conserver les proportions) - Перетаскивание угла/края: изменение размера (Ctrl — от центра, Shift — с сохранением пропорций) + Перетаскивание угла/края: изменение размера (Ctrl — от центра, Shift — с сохранением пропорций) Glisser un coin : pivoter (Maj = par pas de 15°) ; glisser un bord : incliner (Maj = par pas de 15°) ; point rouge : déplacer le centre de rotation - Перетащить угол: повернуть (Shift = с шагом 15°); перетащить грань: наклонить (Shift = с шагом 15°); красная точка: переместить центр вращения + Перетащить угол: повернуть (Shift = с шагом 15°); перетащить грань: наклонить (Shift = с шагом 15°); красная точка: переместить центр вращения @@ -1740,109 +1740,109 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Glisser : redimensionner (Maj = conserver les proportions, Ctrl = depuis le centre) - Перетаскивание: изменение размера (Shift — сохранить пропорции, Ctrl — от центра) + Перетаскивание: изменение размера (Shift — сохранить пропорции, Ctrl — от центра) Glisser : pivoter (Maj = par pas de 15°) - Перетащить: повернуть (Shift = с шагом 15°) + Перетащить: повернуть (Shift = с шагом 15°) Glisser : incliner (Maj = par pas de 15°) - Перетаскивание: наклон (Shift = с шагом 15°) + Перетаскивание: наклон (Shift = с шагом 15°) Glisser : déplacer le centre de rotation - Перетащить: переместить центр вращения + Перетащить: переместить центр вращения Redimensionner une image - Изменить размер изображения + Изменить размер изображения Faire pivoter une image - Повернуть изображение + Повернуть изображение Incliner une image - Наклонить изображение + Наклонить изображение Déplacer le centre de rotation d'une image - Смещение центра вращения изображения + Смещение центра вращения изображения Modifier une image - Изменить изображение + Изменить изображение Restaurer les proportions d'une image - Восстановить пропорции изображения + Восстановить пропорции изображения Enregistrer l'image sous... - Сохранить изображение как... + Сохранить изображение как... Enregistrer l'image d'origine sous... - Сохранить исходное изображение как... + Сохранить исходное изображение как... Image PNG (*.png) - Изображение PNG (*.png) + Изображение PNG (*.png) Image JPEG (*.jpg *.jpeg) - Изображение JPEG (*.jpg, *.jpeg) + Изображение JPEG (*.jpg, *.jpeg) Image BMP (*.bmp) - Изображение BMP (*.bmp) + Изображение BMP (*.bmp) Image SVG (*.svg) - Изображение SVG (*.svg) + Изображение SVG (*.svg) Tous les fichiers (*) - Все файлы (*) + Все файлы (*) Transparence non conservée - Прозрачность не сохраняется + Прозрачность не сохраняется Ce format ne prend pas en charge la transparence : l'image sera enregistrée telle qu'elle était avant l'application de la couleur transparente. Continuer ? - Этот формат не поддерживает прозрачность: изображение будет сохранено в том виде, в каком оно было до применения прозрачного цвета. Продолжить? + Этот формат не поддерживает прозрачность: изображение будет сохранено в том виде, в каком оно было до применения прозрачного цвета. Продолжить? Échec de l'enregistrement - Ошибка при регистрации + Ошибка при сохранении Impossible d'enregistrer l'image à cet emplacement. - Не удалось сохранить изображение в этом месте. + Не удалось сохранить изображение в этом месте. @@ -1852,67 +1852,67 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Remplacer l'image... - Заменить изображение... + Заменить изображение... Couleur transparente... - Прозрачный цвет... + Прозрачный цвет... Rogner... - Обрезать... + Обрезать... Miroir horizontal - Отразить по горизонтали + Отразить по горизонтали Miroir vertical - Отразить по вертикали + Отразить по вертикали Restaurer les proportions - Восстановить пропорции + Восстановить пропорции Selectionner une image... - Выберите изображение... + Выберите изображение... Image Files (*.png *.jpg *.jpeg *.bmp *.svg) - Графические файлы (*.png, *.jpg, *.jpeg, *.bmp, *.svg) + Графические файлы (*.png, *.jpg, *.jpeg, *.bmp, *.svg) Erreur - Ошибка + Ошибка Impossible de charger l'image. - Невозможно загрузить изображение. + Невозможно загрузить изображение. Remplacer une image - Заменить изображение + Заменить изображение Miroir horizontal d'une image - Отразить изображение по горизонтали + Отразить изображение по горизонтали Miroir vertical d'une image - Отразить изображение по вертикали + Отразить изображение по вертикали @@ -1922,7 +1922,7 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Rogner une image - Обрезать изображение + Обрезать изображение @@ -1931,7 +1931,7 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Propriétés du folio window title - Параметры листа + Свойства листа @@ -1945,7 +1945,7 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol <Shift> to move - + <Shift> — для перемещения @@ -1972,12 +1972,12 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Créer un template context menu action - + Создать шаблон Renvoi de folio - Ссылка на страницу + Ссылка на страницу @@ -1998,24 +1998,24 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Modèle enregistré - Зарегистрированная модель + Новый шаблон Le modèle a été enregistré avec succès sous : %1 - Модель была успешно сохранена под именем: + Шаблон был успешно сохранен под именем: %1 Erreur - Ошибка + Ошибка Le fichier n'a pas pu être écrit. - Не удалось записать файл. + Не удалось записать файл. @@ -2128,7 +2128,7 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol ... - + ... @@ -2427,12 +2427,12 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Tourner autour de son propre centre - Вращаться вокруг собственной оси + Вращаться вокруг собственной оси Y - Y + Y @@ -2448,7 +2448,7 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol X - X + X @@ -2505,7 +2505,7 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Modifier le point de rotation d'un champ texte - Изменение точки поворота текстового поля + Изменение точки поворота текстового поля @@ -2571,12 +2571,12 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol ex. 80.5 - пример, 80.5 + пример, 80.5 Nombre décimal avec un point comme séparateur (ex. 80.5) - Десятичное число с точкой в качестве разделителя (пример, 80.5) + Десятичное число с точкой в качестве разделителя (пример, 80.5) @@ -2584,32 +2584,32 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol File not found: %1 - Файл не найден: %1 + Файл не найден: %1 Cannot read %1 - Не удается прочитать %1 + Не удается прочитать %1 This .edz is a zip-format package, which is not yet supported (only 7-Zip .edz files can be imported). - Этот файл .edz представляет собой архив в формате ZIP, который пока не поддерживается (импортировать можно только файлы .edz в формате 7-Zip). + Этот файл .edz представляет собой архив в формате ZIP, который пока не поддерживается (импортировать можно только файлы .edz в формате 7-Zip). Not a valid .edz package (unrecognised archive format). - Недопустимый пакет .edz (неизвестный формат архива). + Недопустимый пакет .edz (неизвестный формат архива). Could not create a temporary directory: %1 - Не удалось создать временный каталог: %1 + Не удалось создать временный каталог: %1 No *.part.xml found inside the .edz - В папке .edz не найден файл *.part.xml + В папке .edz не найден файл *.part.xml @@ -2618,7 +2618,7 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Numéroter automatiquement un élément undo caption - Автоматическая нумерация элемента + Автоматическая нумерация элемента @@ -2670,13 +2670,13 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Enregistrer un template dialog title - Сохранить шаблон + Сохранить шаблон Choisissez l'emplacement dans lequel vous souhaitez enregistrer votre template. dialog content - Выберите папку, в которую вы хотите сохранить свой шаблон. + Выберите папку, в которую вы хотите сохранить свой шаблон. @@ -2696,19 +2696,19 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol Nom du nouveau template - Название нового шаблона + Название нового шаблона Écraser le template ? message box title - Перезаписать шаблон? + Перезаписать шаблон? Le template existe déjà. Voulez-vous l'écraser ? message box content - Шаблон уже существует. Хотите его перезаписать? + Шаблон уже существует. Хотите его перезаписать? @@ -2789,12 +2789,12 @@ Le nom affiché de l'élément se modifie séparément dans les propriété ex. 80.5 - пример 80,5 + пример 80.5 Nombre décimal avec un point comme séparateur (ex. 80.5) - Десятичное число с точкой в качестве разделителя (например, 80.5) + Десятичное число с точкой в качестве разделителя (например, 80.5) @@ -2808,7 +2808,7 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Exclure de la numérotation auto - Исключить из автоматической нумерации + Исключить из автоматической нумерации @@ -2818,17 +2818,17 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Ajouter une propriété personnalisée - Добавить настраиваемое свойство + Добавить настраиваемое свойство Séparation de potentiel - Разделение потенциалов + Разделение потенциалов Exclure de la nomenclature - Исключить из спецификации + Исключить из спецификации @@ -2837,18 +2837,18 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Personnaliser la barre… - Настроить панель… + Настроить панель… Glissez les commandes et les éléments dans la barre, hors de la barre, ou d'une place à l'autre. Un double-clic fait passer une commande ou un élément d'une liste à l'autre. - Перетаскивайте команды и элементы на панель, с панели или с одного места на другое. Двойной щелчок переносит команду или элемент из одного списка в другой. + Перетаскивайте команды и элементы на панель, с панели или с одного места на другое. Двойной щелчок переносит команду или элемент из одного списка в другой. Rechercher un élément… - Найти элемент… + Найти элемент… @@ -2858,39 +2858,39 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Valeurs par défaut - Значения по умолчанию + Значения по умолчанию Annuler - Отмена + Отмена Terminé - Завершено + Завершить Dans la barre : - В строке: + На панеле: Autres commandes : - Другие заказы: + Другие команды: Entrée pour insérer · Échap pour fermer - Клавиша «Enter» для ввода · Клавиша «Esc» для закрытия + Клавиша «Enter» для вставки · Клавиша «Esc» для закрытия Glisser pour changer la largeur de la barre - Проведите пальцем, чтобы изменить ширину полосы + Потяните, чтобы изменить ширину полосы @@ -2900,7 +2900,7 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Aucun résultat - Результатов нет + Результатов нет @@ -2939,12 +2939,12 @@ Le nom affiché de l'élément se modifie séparément dans les propriété margin: 5px; font-weight: bold; - + margin: 5px; font-weight: bold; Exclure de la nomenclature - Исключить из спецификации + Исключить из спецификации @@ -3190,7 +3190,7 @@ Le nom affiché de l'élément se modifie séparément dans les propriété T%1 - + T%1 @@ -3205,18 +3205,18 @@ Le nom affiché de l'élément se modifie séparément dans les propriété + - + + - - + - Adresse - Адрес + Адрес @@ -3233,12 +3233,12 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Nb. - + № T1 - + T1 @@ -3344,7 +3344,7 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Général - Простой + Общее @@ -3413,35 +3413,35 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Nombre maximum de contacts esclaves définis : non défini - Максимальное количество заданных подчиненных контактов: не определено + Максимальное количество заданных подчиненных контактов: не определено Nombre maximum de contacts esclaves définis : %1 - Максимальное количество заданных подчиненных контактов: %1 + Максимальное количество заданных подчиненных контактов: %1 Nombre de contacts esclaves utilisés : %1 - Количество используемых подчиненных контактов: %1 + Количество используемых подчиненных контактов: %1 Contacts : NO : %1/%2, NC : %3/%4, inverseurs : %5/%6, autres : %7/%8 - Контакты: NO: %1/%2, NC: %3/%4, переключатели: %5/%6, прочие: %7/%8 + Контакты: NO: %1/%2, NC: %3/%4, переключатели: %5/%6, прочие: %7/%8 Contacts : NO : %1, NC : %2, inverseurs : %3, autres : %4 - Контакты: NO: %1, NC: %2, переключатели: %3, прочие: %4 + Контакты: NO: %1, NC: %2, переключатели: %3, прочие: %4 @@ -3454,7 +3454,7 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Verrouiller la position - Закрепить положение + Закрепить положение @@ -3487,22 +3487,22 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Monter la sélection - Сдвинуть выделение вверх + Сдвинуть выделенный вверх Ajouter la sélection - Добавить выделение + Добавить выделенный Supprimer la sélection - Удалить выделение + Удалить выделенный Descendre la sélection - Сдвинуть выделение вниз + Сдвинуть выделенный вниз @@ -3587,7 +3587,7 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Automates (MAE/SPS) - Автоматы (MAE/SPS) + ПЛК (PLC) @@ -3607,22 +3607,22 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Importer des rapports depuis un fichier - Импорт отчетов из файла + Импорт отчетов из файла Importer... - Импортировать... + Импортировать... Exporter tous les rapports enregistrés vers un fichier - Экспортировать все сохраненные отчеты в файл + Экспортировать все сохраненные отчеты в файл Exporter... - Экспортировать... + Экспортировать... @@ -3658,24 +3658,24 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Exporter - Экспорт + Экспорт Aucun rapport enregistré à exporter. - Для экспорта не найдено ни одного отчета. + Для экспорта не найдено ни одного отчета. Exporter les rapports - Экспорт отчетов + Экспорт отчетов Fichiers JSON (*.json) - Файлы JSON (*.json) + Файлы JSON (*.json) @@ -3683,58 +3683,58 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Erreur - Ошибка + Ошибка Impossible d'écrire dans %1. - Не удалось записать данные в %1. + Не удалось записать данные в %1. Importer des rapports - Импорт отчетов + Импорт отчетов Impossible de lire %1. - Не удалось прочитать %1. + Не удалось прочитать %1. %1 ne contient pas des rapports QElectroTech valides. - %1 не содержит действительных отчетов QElectroTech. + %1 не содержит действительных отчетов QElectroTech. Importer - Импортировать + Импортировать Ce fichier ne contient aucun rapport. - В этом файле нет отчетов. + В этом файле нет отчетов. Rapport déjà existant - Уже существующий отчет + Отчет уже существуюет Un rapport nommé « %1 » existe déjà. Le remplacer ? - Отчет с именем «%1» уже существует. Заменить его? + Отчет с именем «%1» уже существует. Заменить его? Impossible d'écrire la configuration locale. - Не удалось сохранить локальные настройки. + Не удалось сохранить локальные настройки. %1 rapport(s) importé(s), %2 ignoré(s). - %1 отчет(ы) импортировано, %2 пропущено. + %1 отчет(ы) импортировано, %2 пропущено. @@ -3915,7 +3915,7 @@ Le nom affiché de l'élément se modifie séparément dans les propriété Importer une pièce EPLAN (.edz)… - Импорт чертежа EPLAN (.edz)… + Импорт чертежа EPLAN (.edz)… @@ -3987,7 +3987,7 @@ Tout les éléments et les dossier contenus dans ce dossier seront supprimés. Avertissement — Importation d'un fichier EPLAN (.edz) - Предупреждение — Импорт файла EPLAN (.edz) + Предупреждение — Импорт файла EPLAN (.edz) @@ -4003,7 +4003,7 @@ En important ce fichier, vous confirmez que : • vous connaissez son origine et êtes autorisé à l'utiliser dans ce contexte, au regard des conditions applicables à cette source ; • cette importation est effectuée à vos propres risques et responsabilité ; • ni QElectroTech, ni ses mainteneurs, ni ses contributeurs ne peuvent être tenus responsables d'une utilisation non conforme de ces données. - Формат .edz может быть получен из двух различных источников: + Формат .edz может быть получен из двух различных источников: • Портал EPLAN Data Portal (dataportal.eplan.com), на который распространяются условия использования среды EPLAN Cloud; • Сайт производителя компонентов (или дистрибьютора), который предоставляет свои файлы .edz напрямую в соответствии со своими собственными условиями. @@ -4019,33 +4019,33 @@ QElectroTech не может автоматически определить п J'ai lu et j'accepte ces conditions. - Я прочитал(а) и принимаю эти условия. + Я прочитал(а) и принимаю эти условия. Importer - Импортировать + Импортировать Importer une pièce EPLAN - Импорт детали из EPLAN + Импорт детали из EPLAN Pièces EPLAN (*.edz) - Детали EPLAN (*.edz) + Детали EPLAN (*.edz) Import EPLAN - Импорт из EPLAN + Импорт из EPLAN Impossible d'importer cette pièce : %1 - Не удалось импортировать эту деталь: + Не удалось импортировать эту деталь: %1 @@ -4084,17 +4084,17 @@ QElectroTech не может автоматически определить п Rechercher... - Поиск... + Поиск... Collections - Коллекции + Коллекции Modèles - Модели + Шаблоны @@ -4143,13 +4143,13 @@ QElectroTech не может автоматически определить п Double-cliquez pour réduire ou développer la collection company de cartouches Status tip - Дважды щелкните, чтобы свернуть или развернуть коллекцию штампов компании + Дважды щелкните, чтобы свернуть или развернуть коллекцию штампов компании Ceci est la collection company de cartouches -- utilisez-la pour créer, stocker et éditer vos propres cartouches. "What's this" tip - Это коллекция штампов компании — используйте её для создания, хранения и редактирования собственных шаблонов. + Это коллекция штампов компании — используйте её для создания, хранения и редактирования собственных шаблонов. @@ -4195,7 +4195,7 @@ QElectroTech не может автоматически определить п Propriétés du folio - Параметры листа + Свойства листа @@ -4205,17 +4205,17 @@ QElectroTech не может автоматически определить п Insérer un folio au-dessus - Вставить страницу выше + Вставить страницу выше Insérer un folio en dessous - Вставить страницу ниже + Вставить страницу ниже Copier et coller - + Вставить копию @@ -4240,7 +4240,7 @@ QElectroTech не может автоматически определить п Remonter ce folio x100 - Переместить лист вверх на 100 + Переместить лист вверх на 100 @@ -4255,7 +4255,7 @@ QElectroTech не может автоматически определить п Abaisser ce folio x100 - Переместить лист вниз на 100 + Переместить лист вниз на 100 @@ -4267,7 +4267,7 @@ QElectroTech не может автоматически определить п Panneau des éléments - Панель элементов + Панель элементов @@ -4408,13 +4408,13 @@ QElectroTech не может автоматически определить п Images non incluses dans l'export DXF message box title - Изображения, не включенные в экспорт в формат DXF + Изображения, не включенные в экспорт в формат DXF Le format DXF utilisé ici (AC1006) ne permet pas d'inclure d'image. Les images seront représentées uniquement par un rectangle de contour (position, taille, rotation et inclinaison conservées), sans le contenu de l'image. message box content - Используемый здесь формат DXF (AC1006) не позволяет включать изображения. Изображения будут отображаться исключительно в виде прямоугольника с контуром (с сохранением положения, размера, поворота и наклона), без самого содержимого изображения. + Используемый здесь формат DXF (AC1006) не позволяет включать изображения. Изображения будут отображаться исключительно в виде прямоугольника с контуром (с сохранением положения, размера, поворота и наклона), без самого содержимого изображения. @@ -4979,52 +4979,52 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments Couleur de l'application - Цвет приложения + Цвет приложения Afficher les propriétés d'un conducteur sélectionné dans le panneau Propriétés de la sélection - Отобразить свойства выбранного проводника на панели «Свойства выделенного объекта» + Показывать свойства выбранного проводника на панели «Свойства выбора» Afficher la grille par défaut (appliqué au prochain lancement) - Показать сетку по умолчанию (будет применено при следующем запуске) + Показать сетку по умолчанию (будет применено при следующем запуске) Par défaut, un double-clic insère l'élément sur le folio ; l'édition reste accessible par le menu contextuel. - По умолчанию двойной щелчок вставляет элемент на страницу; доступ к редактированию по-прежнему можно получить через контекстное меню. + По умолчанию двойной щелчок вставляет элемент на страницу; доступ к редактированию по-прежнему можно получить через контекстное меню. Double-cliquer dans la collection ouvre l'éditeur d'élément au lieu de l'insérer - Двойной щелчок в коллекции открывает редактор элемента, а не вставляет его + Двойной щелчок в коллекции открывает редактор элемента, а не вставляет его Après un clic qui sélectionne un élément ou un conducteur, les commandes de la barre de raccourcis apparaissent près du curseur et s'effacent quand la souris s'éloigne. - После щелчка, выделяющего элемент или проводник, команды панели быстрого доступа появляются рядом с курсором и исчезают, когда курсор удаляется. + После щелчка, выделяющего элемент или проводник, команды панели быстрого доступа появляются рядом с курсором и исчезают, когда курсор удаляется. Afficher les commandes près de la sélection - Показать команды рядом с выделенным элементом + Показать команды рядом с выделенным элементом Maintenir le bouton droit et glisser dans une direction lance une commande de la barre de raccourcis. Un simple clic droit ouvre toujours le menu contextuel, au relâchement du bouton. - Если удерживать правую кнопку мыши и перетащить курсор в каком-либо направлении, запускается команда из панели быстрого доступа. Обычный щелчок правой кнопкой мыши всегда открывает контекстное меню после отпускания кнопки. + Если удерживать правую кнопку мыши и перетащить курсор в каком-либо направлении, запускается команда из панели быстрого доступа. Обычный щелчок правой кнопкой мыши всегда открывает контекстное меню после отпускания кнопки. Gestes de la souris avec le bouton droit - Действия с правой кнопкой мыши + Действия правой кнопки мыши Afficher les guides par défaut (appliqué au prochain lancement) - Показать подсказки по умолчанию (будет применено при следующем запуске) + Показать подсказки по умолчанию (будет применено при следующем запуске) @@ -5044,27 +5044,27 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments Autoriser l'exécution de scripts JavaScript (Projet > Exécuter un script, et --run) - Разрешить выполнение скриптов JavaScript (Проект > Выполнить скрипт и --run) + Разрешить выполнение скриптов JavaScript (Проект > Запустить скрипт и --run) 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. - Скрипт запускается с вашими правами: он может читать и изменять открытый проект, а также записывать файлы. По умолчанию эта функция отключена; запускайте только те скрипты, источник которых вам известен. + Скрипт запускается с вашими правами: он может читать и изменять открытый проект, а также записывать файлы. По умолчанию эта функция отключена; запускайте только те скрипты, источник которых вам известен. Répertoire de la collection company - Каталог коллекции компании + Каталог коллекции компании Répertoire des cartouches company - Каталог штампов компании + Каталог штампов компании Répertoire des Macros utilisateur - Каталог пользовательских макросов + Каталог пользовательских шаблонов @@ -5123,12 +5123,12 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments Grille des textes déplacés à la souris - Сетка текстов, перемещаемых с помощью мыши + Сетка текстов, перемещаемых с помощью мыши Fraction de la grille des folios. Maintenir Ctrl pendant le déplacement pour placer librement. - Привязка к сетки листа. Удерживайте клавишу Ctrl во время перемещения, чтобы разместить элементы в произвольном порядке. + Привязка к сетки листа. Удерживайте клавишу Ctrl во время перемещения, чтобы разместить элементы в произвольном порядке. @@ -5163,39 +5163,39 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments Affichage Grille - Показывать в виде таблицы + Показывать в виде таблицы max: - + max: Taille des points de la grille de Diagram-Editor : 1 - 5 - Размер точек сетки в Diagram-Editor: 1–5 + Размер точек сетки в Diagram-Editor: 1–5 min: - + min: Taille des points de la grille de l'éditeur d'éléments : 1 - 5 - Размер точек сетки в редакторе элементов: 1–5 + Размер точек сетки в редакторе элементов: 1–5 Editor - Редактор + Редактор Max. parts in Element Editor List - Максимальное количество элементов в списке редактора элементов + Максимальное количество элементов в списке редактора элементов @@ -5235,12 +5235,12 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments Désactivée - Отключено + Отключено Activé par la variable d'environnement QET_ENABLE_SCRIPTING ; ce réglage est sans effet tant qu'elle est définie. - Включается с помощью переменной среды QET_ENABLE_SCRIPTING; этот параметр не действует, пока она определена. + Включается с помощью переменной среды QET_ENABLE_SCRIPTING; этот параметр не действует, пока она определена. @@ -5322,7 +5322,7 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments Coréen - Корейский + Корейский @@ -5383,42 +5383,42 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments Ukrainien - Украинский + Украинский Chinois - Китайский + Китайский Suédois - Шведский + Шведский Chemin de la collection company - Путь к коллекции компании + Путь к коллекции компании Chemin des cartouches company - Путь к штампам компании + Путь к штампам компании Chemin des macros utilisateur - Путь к пользовательским макросам + Путь к пользовательским шаблонам To high values might lead to crashes of the application. - Слишком высокие значения могут привести к сбоям в работе приложения. + Слишком высокие значения могут привести к сбоям в работе приложения. Fonctionnalité expérimental - Экспериментальная функция + Экспериментальная функция @@ -5428,7 +5428,7 @@ Toutes valeurs autre que ‘Pas d’arrondi’ peut causer des erreurs de rendu 1 - la valeur sélectionnée 2 - du dpi de l'écran 3 - Modifier le projet sur un autre ordinateur et/ou écran n'ayant pas les mêmes paramètres des points 1 et 2. - ПРЕДУПРЕЖДЕНИЕ: + ПРЕДУПРЕЖДЕНИЕ: Любое значение, отличное от «Без округления», может привести к ошибкам при визуализации проекта в зависимости от: 1 — выбранного значения @@ -5687,22 +5687,22 @@ Toutes valeurs autre que ‘Pas d’arrondi’ peut causer des erreurs de rendu Rogner l'image - Обрезать изображение + Обрезать изображение Faites glisser les poignées, ou l'intérieur du cadre, pour ajuster la zone à conserver. - Перетащите ручки или внутреннюю границу рамки, чтобы настроить область, которую нужно сохранить. + Перетащите ручки или внутреннюю границу рамки, чтобы настроить область, которую нужно сохранить. Réinitialiser - Сбросить + Сбросить Revenir à l'image complète, sans rognage - Вернуться к полному изображению без обрезки + Вернуться к полному изображению без обрезки @@ -5715,61 +5715,61 @@ Toutes valeurs autre que ‘Pas d’arrondi’ peut causer des erreurs de rendu Largeur - Ширина + Ширина % - + % Verrouillé : modifier la largeur ou la hauteur ajuste l'autre pour conserver les proportions. Cliquer pour déverrouiller. - Заблокировано: при изменении ширины или высоты другой параметр настраивается автоматически для сохранения пропорций. Щелкните, чтобы разблокировать. + Заблокировано: при изменении ширины или высоты другой параметр настраивается автоматически для сохранения пропорций. Щелкните, чтобы разблокировать. Hauteur - Высота + Высота Restaurer les proportions - Восстановить пропорции + Восстановить пропорции Angle - Угол + Угол ° - + ° Inclinaison X - Наклон по оси X + Наклон по оси X Inclinaison Y - Наклон по оси Y + Наклон по оси Y Libellé - Формулировка + Надпись Texte affiché sous l'image. Il suit l'image quand elle est déplacée, copiée ou tournée. - Текст, отображаемый под изображением. Он следует за изображением при его перемещении, копировании или повороте. + Текст, отображаемый под изображением. Он следует за изображением при его перемещении, копировании или повороте. @@ -5779,33 +5779,33 @@ Toutes valeurs autre que ‘Pas d’arrondi’ peut causer des erreurs de rendu Modifier la largeur d'une image - Изменить ширину изображения + Изменение ширины изображения Modifier la hauteur d'une image - Изменить высоту изображения + Изменение высоты изображения Modifier l'angle d'une image - Изменить угол наклона изображения + Измение угола наклона изображения Modifier l'inclinaison d'une image - Изменение наклона изображения + Изменение наклона изображения Modifier le libellé d'une image - Изменить подпись к изображению + Изменение надписи изображения Déverrouillé : largeur et hauteur peuvent être modifiées indépendamment. Cliquer pour verrouiller. - Разблокировано: ширину и высоту можно изменять независимо друг от друга. Нажмите, чтобы заблокировать. + Разблокировано: ширину и высоту можно изменять независимо друг от друга. Нажмите, чтобы заблокировать. @@ -5813,42 +5813,42 @@ Toutes valeurs autre que ‘Pas d’arrondi’ peut causer des erreurs de rendu Couleur transparente - Прозрачный цвет + Прозрачный цвет Image source - Источник изображения + Оригинальное изображение Aperçu - Предпросмотр + Предпросмотр rgb(%1, %2, %3) - + rgb(%1, %2, %3) Tolérance pour cette couleur - Допустимое отклонение для данного цвета + Допустимое отклонение для данного цвета Retirer cette couleur - Удалить этот цвет + Удалить этот цвет Cliquez sur l'image pour ajouter une couleur. Ajustez la tolérance de chaque couleur avec son curseur, ou cliquez sur × pour la retirer. - Щелкните по изображению, чтобы добавить цвет. Отрегулируйте порог чувствительности для каждого цвета с помощью соответствующего ползунка или нажмите ×, чтобы удалить его. + Щелкните по изображению, чтобы добавить цвет. Отрегулируйте порог чувствительности для каждого цвета с помощью соответствующего ползунка или нажмите ×, чтобы удалить его. Cliquez sur l'image pour choisir une couleur - Нажмите на изображение, чтобы выбрать цвет + Нажмите на изображение, чтобы выбрать цвет @@ -6065,7 +6065,7 @@ Veuillez utiliser l'éditeur avancé pour cela. Atteindre un élément window title - Перейти к элементу + Перейти к элементу @@ -6075,17 +6075,17 @@ Veuillez utiliser l'éditeur avancé pour cela. Case %1 - + Ячейка %1 Folio %1, case %2 - + Лист %1, ячейка %2 Folio %1 (%2), case %3 - + Лист %1 (%2), ячейка %3 @@ -6193,7 +6193,7 @@ Veuillez utiliser l'éditeur avancé pour cela. Masquer les éléments maîtres pleins - + Скрыть все главные элементы @@ -6494,7 +6494,7 @@ Voulez-vous tout de même lier ce contact esclave ? Collage multiple - Множественное копирование + Множественная вставка @@ -6609,32 +6609,32 @@ Voulez-vous tout de même lier ce contact esclave ? Guides - Руководство + Направляющие Définir les règles de numérotation automatique par défaut pour les nouveaux projets : - Задайте правила автоматической нумерации по умолчанию для новых проектов: + Задайте правила автоматической нумерации по умолчанию для новых проектов: Conducteurs - Проводники + Проводники Eléments - Элементы + Элементы Folios - Листы + Листы Numérotation auto - Автонумерация + Автонумерация @@ -6750,12 +6750,12 @@ Voulez-vous tout de même lier ce contact esclave ? Mettre à 0 pour un chiffre qui n'avance que par le report d'un chiffre cyclique suivant (ex: le "0" de "0.7") - Установить значение 0 для цифры, которая увеличивается только за счёт переноса младшего разряда (например, «0» для «0.7») + Установить значение 0 для цифры, которая увеличивается только за счёт переноса младшего разряда (например, «0» для «0.7») Valeur à laquelle ce chiffre revient à 0 en incrémentant le chiffre précédent (0 = pas de cycle) - Значение, при котором данная цифра становится равной 0 при увеличении предыдущей цифры (0 = цикл отсутствует) + Значение, при котором данная цифра становится равной 0 при увеличении предыдущей цифры (0 = цикл отсутствует) @@ -6765,12 +6765,12 @@ Voulez-vous tout de même lier ce contact esclave ? Format d'affichage : une suite de zéros donne le nombre minimum de chiffres (00 = 07, 000 = 007). Vide = largeur naturelle du type. - Формат отображения: последовательность нулей указывает минимальное количество цифр (00 = 07, 000 = 007). Пустое поле = естественная ширина типа. + Формат отображения: последовательность нулей указывает минимальное количество цифр (00 = 07, 000 = 007). Пустое поле = естественная ширина типа. 0 - + 0 @@ -6806,7 +6806,7 @@ Voulez-vous tout de même lier ce contact esclave ? Cyclique (modulo) - Циклический (по модулю) + Циклический (по модулю) @@ -6815,7 +6815,7 @@ Voulez-vous tout de même lier ce contact esclave ? Alphabétique - По алфавиту + По алфавиту @@ -7001,12 +7001,12 @@ Voulez-vous tout de même lier ce contact esclave ? Sélectionner une page - Выбрать страницу + Выбрать страницу Ce document PDF contient %n page(s) - + Этот документ PDF содержит %n страницу Этот документ PDF содержит %n страницы Этот документ PDF содержит %n страниц @@ -7015,32 +7015,32 @@ Voulez-vous tout de même lier ce contact esclave ? Page à importer : - Страница для импорта: + Страница для импорта: Résolution : - Разрешение : + Разрешение : 150 DPI (écran) - 150 DPI (экран) + 150 DPI (экран) 300 DPI (impression) - 300 DPI (печать) + 300 DPI (печать) 600 DPI (haute qualité) - 600 DPI (высокое качество) + 600 DPI (высокое качество) Aperçu : - Обзор: + Обзор: @@ -7118,7 +7118,7 @@ Voulez-vous tout de même lier ce contact esclave ? Connecter - + Разъем @@ -7131,12 +7131,12 @@ Voulez-vous tout de même lier ce contact esclave ? X - X + X Y - Y + Y @@ -7502,26 +7502,26 @@ Les variables suivantes sont incompatibles : Temps passé sur ce projet : label when configuring - Время, затраченное на этот проект: + Время, затраченное на этот проект: Suivre le temps passé sur ce projet (uniquement enregistré localement dans ce fichier) checkbox label - Отслеживать время, затраченное на этот проект (сохраняется только локально в этом файле) + Отслеживать время, затраченное на этот проект (сохраняется только локально в этом файле) Réinitialiser button label - Сбросить + Сбросить %1 h %2 min hours and minutes of time spent on a project - %1 ч %2 мин + %1 ч %2 мин @@ -7589,12 +7589,12 @@ Les variables suivantes sont incompatibles : Dessiner les noms des bornes - Нарисовать названия клемм + Рисовать названия клемм Inscrire les informations des composants - Ввести данные о компонентах + Вывести данные о компонентах @@ -7770,7 +7770,7 @@ Voulez-vous enregistrer les modifications ? Êtes-vous sûr de vouloir supprimer ce folio du projet ? message box content - Вы уверены, что хотите удалить этот лист из проекта? + Вы уверены, что хотите удалить этот лист из проекта? @@ -7823,12 +7823,12 @@ Voulez-vous enregistrer les modifications ? go one page left - перейти на одну страницу влево + перейти на одну страницу влево go one page right - перейти на одну страницу вправо + перейти на одну страницу вправо @@ -8557,57 +8557,57 @@ Maintenir Ctrl pendant le déplacement pour placer librement. Insérer le dernier élément - + Вставить последний элемент Place à nouveau le dernier élément inséré status bar tip - + Снова вставить последний добавленный элемент Rechercher une commande… - + Поиск команды… Tapez une partie du nom d'une commande et appuyez sur Entrée pour la lancer status bar tip - + Введите часть названия команды и нажмите Enter, чтобы запустить её Insérer un élément… - + Вставить элемент… Ouvre le sélecteur d'éléments à la position du curseur status bar tip - + Открыть окно выбора элементов в месте нахождения курсора Barre de raccourcis - + Панель быстрого доступа Ouvre à la position du curseur les commandes utiles pour la sélection, et le sélecteur d'éléments status bar tip - + Открывает в месте нахождения курсора команды, необходимые для выделения, а также средство выбора элементов Répéter la dernière commande - + Повторить последнюю команду Relance le dernier outil de dessin ou la dernière insertion d'élément (Entrée sur le folio) status bar tip - + Повторить последнее действие с инструментом рисования или последнюю вставку элемента (ввод на лист) @@ -8624,13 +8624,13 @@ Maintenir Ctrl pendant le déplacement pour placer librement. Aligner sur la grille - + Выровнять по сетке Remet les éléments, images et textes sélectionnés sur la grille status bar tip - + Вставляет выделенные элементы, изображения и текст по сетке @@ -8650,24 +8650,24 @@ Maintenir Ctrl pendant le déplacement pour placer librement. Sélectionner tous les conducteurs - + Выбрать все проводники Sélectionner tous les champs de texte - + Выделить все текстовые поля Sélectionne tous les conducteurs du folio, désélectionne le reste status bar tip - + Выделяет все проводники на листе, снимает выделение с остальных Sélectionne tous les champs de texte du folio, désélectionne le reste status bar tip - + Выделяет все текстовые поля на листе, снимает выделение с остальных @@ -8749,17 +8749,17 @@ Maintenir Ctrl pendant le déplacement pour placer librement. A&jouter - + &Добавить Aligner - + Выровнять Lignes et colonnes - + Строки и столбцы @@ -8808,9 +8808,9 @@ Déponter et/ou supprimer les niveaux des bornes concerné afin de pouvoir les s %n objet(s) remis sur la grille - - - + %n элемент возвращен на сетку + %n элемента возвращены на сетку + %n элементов возвращены на сетку @@ -8822,9 +8822,9 @@ Déponter et/ou supprimer les niveaux des bornes concerné afin de pouvoir les s (%n objet(s) verrouillé(s) laissé(s) en place) - - - + (%n заблокированный объект остался на месте) + (%n заблокированных объектов остались на месте) + (%n заблокированных объектов остались на месте) @@ -8837,55 +8837,55 @@ Déponter et/ou supprimer les niveaux des bornes concerné afin de pouvoir les s Répéter : insérer « %1 » - + Повторить: вставку «%1» Répéter : %1 - + Повторить: %1 folio %1 - + лист %1 %n élément(s) redessiné(s). - - - + %n перерисованный элемент. + %n перерисованных элементов. + %n перерисованных элементов. %n élément(s) dont la définition est introuvable ou illisible : leur dessin actuel a été conservé. - - - + %n элемент, определение которого не найдено или нечитаемо: их текущий рисунок был сохранен. + %n элемента, определение которых не найдено или нечитаемо: их текущий рисунок был сохранен. + %n элементов, определение которых не найдено или нечитаемо: их текущий рисунок был сохранен. %n élément(s) non redessiné(s) : leur taille, leur point de saisie ou leurs bornes ont changé (borne ajoutée, supprimée ou déplacée). - - - + %n элемент, не перерисованный: их размер, точка захвата или границы изменились (граница добавлена, удалена или перемещена). + %n элемента, не перерисованных: их размер, точка захвата или границы изменились (граница добавлена, удалена или перемещена). + %n элементов, не перерисованных: их размер, точка захвата или границы изменились (граница добавлена, удалена или перемещена). Pour les mettre à jour, il faut les supprimer puis les réinsérer. Attention : cette opération supprime les conducteurs déjà reliés à ces éléments, qu'il faudra retracer. - + Чтобы обновить их, необходимо удалить их, а затем вставить заново. Внимание: при выполнении этой операции будут удалены проводники, уже подключенные к этим элементам, и их придётся проложить заново. Exécuter un script - + Запустить скрипт @@ -8894,18 +8894,22 @@ Déponter et/ou supprimer les niveaux des bornes concerné afin de pouvoir les s 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. Activer les scripts ? Ce réglage est modifiable dans Configurer QElectroTech > Général > Projets. - + Скрипты отключены. + +Скрипт запускается с вашими правами: он может читать и изменять открытый проект, а также записывать файлы. Запускайте только те скрипты, источник которых вам известен. + +Включить скрипты? Этот параметр можно изменить в меню «Настройки QElectroTech» > «Общие» > «Проекты». Scripts JavaScript (*.js);;Tous les fichiers (*) - + Скрипты JavaScript (*.js);;Все файлы (*) Textes %1 - + Тексты %1 @@ -9381,24 +9385,24 @@ Activer les scripts ? Ce réglage est modifiable dans Configurer QElectroTech &g Dupli&quer - + &Дубликат Copie la sélection, décalée de l'espacement configuré status bar tip - + Скопировать выделенную область, сместив её на заданное расстояние Configurer la duplication... - + Настроить дублирование... Choisir l'espacement et la direction utilisés par Dupliquer status bar tip - + Выбрать интервал и направление, используемые функцией «Дублировать» @@ -10499,17 +10503,17 @@ Que désirez vous faire ? Impossible de vérifier la requête : la base de données ne peut pas être mise en lecture seule. - Не удалось проверить запрос: база данных не может быть переведена в режим «только для чтения». + Не удалось проверить запрос: база данных не может быть переведена в режим «только для чтения». Seules les requêtes en lecture seule sont autorisées : cette requête modifierait la base de données. - Допускаются только запросы в режиме «только для чтения»: данный запрос приведёт к изменению базы данных. + Допускаются только запросы в режиме «только для чтения»: данный запрос приведёт к изменению базы данных. Requête SQL invalide : %1 - Недопустимый SQL запрос : %1 + Недопустимый SQL запрос : %1 @@ -10697,7 +10701,7 @@ Que désirez vous faire ? Éditeur de cartouche - Редактор шаблонов + Редактор шаблонов @@ -10788,13 +10792,13 @@ Que désirez vous faire ? Erreur message box title - Ошибка + Ошибка Impossible d'enregistrer le modèle « %1 ». message box content - %1 is a title block template name - Не удалось сохранить шаблон «%1». + Не удалось сохранить шаблон «%1». @@ -10824,7 +10828,7 @@ Que désirez vous faire ? Le nom « %1 » n'est pas valide : il ne doit pas contenir les caractères suivants : \ / : * ? " < > | message box content - %1 is the rejected template name - Имя «%1» недействительно: оно не должно содержать следующие символы: \ / : * ? " < > | + Имя «%1» недействительно: оно не должно содержать следующие символы: \ / : * ? " < > | @@ -11017,13 +11021,13 @@ Que désirez vous faire ? Miroir de sélection undo caption - Отразить выделеное + Отразить выделеное Retourner la sélection undo caption - Снять выделение + Снять выделение @@ -11228,7 +11232,7 @@ Que désirez vous faire ? %n plan(s) de bornes Sentence fragment used in an automatically generated list of different objects, e.g. objects moved at the same time, which will be combined into a sentence. - %n схема клеммных колодок + %n схема клеммной колодки %n схемы клеммных колодок %n схем клеммных колодок @@ -11658,7 +11662,7 @@ Que désirez vous faire ? Macros - Макрос + Шаблоны @@ -11862,13 +11866,13 @@ Voulez-vous la remplacer ? Installation (=) - Установка (=) + Установка (=) Localisation (+) - Расположение (+) + Расположение (+) @@ -13008,7 +13012,7 @@ Pour l'activer : Configurer QElectroTech > Général > Projets, ou d Example Longer example string - Пример + Пример @@ -14409,29 +14413,29 @@ Les autres champs ne sont pas utilisés. Géométrie - Геометрия + Геометрия Largeur - Ширина + Ширина Hauteur - Высота + Высота Angle - Угол + Угол ° - + ° @@ -14511,7 +14515,7 @@ Les autres champs ne sont pas utilisés. Forme fermée - Закрытая форма + Закрытая форма @@ -14521,17 +14525,17 @@ Les autres champs ne sont pas utilisés. Modifier la longueur d'une ligne - Изменить длину линии + Изменить длину линии Modifier la taille d'une forme - Изменить размер фигуры + Изменить размер фигуры Modifier l'angle d'une forme - Изменить угол фигуры + Изменить угол фигуры @@ -14562,17 +14566,17 @@ Les autres champs ne sont pas utilisés. Longueur - Длина + Длина Rayon X - + Радиус X Rayon Y - + Радиус Y @@ -14644,7 +14648,7 @@ Les autres champs ne sont pas utilisés. Conducteurs sélectionnés - + Выбранные проводники @@ -14698,9 +14702,9 @@ Les autres champs ne sont pas utilisés. %n action(s) - - - + %n действие + %n действий + %n действий @@ -16745,7 +16749,7 @@ Les autres champs ne sont pas utilisés. T%1 - + T%1 @@ -17572,17 +17576,17 @@ Les autres champs ne sont pas utilisés. Alignement - Выравнивание + Выравнивание Point d'ancrage du texte et alignement des lignes entre elles - Точка привязки текста и выравнивание строк относительно друг друга + Точка привязки текста и выравнивание строк относительно друг друга Modifier l'alignement d'un champ texte - Изменить выравнивание текстового поля + Изменить выравнивание текстового поля @@ -18344,7 +18348,7 @@ associer le nom "volta" et la valeur "1745" remplacera %{vol Fil column title - + Провод @@ -18556,12 +18560,12 @@ associer le nom "volta" et la valeur "1745" remplacera %{vol Afficher dans le peigne de contacts tous les contacts esclaves définis par le maître, même ceux qui ne sont pas encore reliés, dans l'ordre défini par le maître - + Отобразить в списке контактов все подчиненные контакты, заданные ведущим устройством, включая те, которые ещё не подключены, в порядке, заданном ведущим устройством Afficher tous les esclaves définis par le maître - + Показать все ведомые устройства, определённые ведущим устройством @@ -18658,7 +18662,7 @@ associer le nom "volta" et la valeur "1745" remplacera %{vol Automate (PLC) - + ПЛК (PLC) @@ -18688,7 +18692,7 @@ associer le nom "volta" et la valeur "1745" remplacera %{vol Right - + Справа @@ -18698,7 +18702,7 @@ associer le nom "volta" et la valeur "1745" remplacera %{vol Champ de texte - + Текстовое поле @@ -18706,17 +18710,17 @@ associer le nom "volta" et la valeur "1745" remplacera %{vol La requête est vide. - Запрос пуст. + Запрос пуст. Une seule requête SELECT est autorisée (le caractère ';' ne peut apparaître qu'à la toute fin). - Допускается только один запрос SELECT (символ «;» может встречаться только в самом конце). + Допускается только один запрос SELECT (символ «;» может встречаться только в самом конце). Seules les requêtes en lecture seule (SELECT ou WITH ... SELECT) sont autorisées. - Допускаются только запросы, предназначенные только для чтения (SELECT или WITH ... SELECT). + Допускаются только запросы, предназначенные только для чтения (SELECT или WITH ... SELECT). @@ -18785,7 +18789,7 @@ associer le nom "volta" et la valeur "1745" remplacera %{vol Éditeur de texte - Текстовый редактор + Текстовый редактор From 5b4bba96ae8cb1d5a1edc977a2ecbafa1cda2f7f Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 2 Oct 2026 07:50:15 +1300 Subject: [PATCH 009/225] English: call a folio a sheet Issue #935. Changes the English translation only: "folio" becomes "sheet" in the 171 interface strings that use it, and the "Folio" label of the seven shipped title blocks becomes "Sheet". Variable names (%folio, %{folio-id}, %{previous-folio-num}, ...) are unchanged, so existing title blocks and labels keep working. French and the other languages are untouched. Co-Authored-By: Claude Opus 5.5 --- lang/qet_en.ts | 368 +++++++++++++-------------- titleblocks/A4_1.titleblock | 2 +- titleblocks/DIN_A4.titleblock | 4 +- titleblocks/ISO7200_A4_V1.titleblock | 2 +- titleblocks/default.titleblock | 2 +- titleblocks/double-logo.titleblock | 2 +- titleblocks/single-logo.titleblock | 2 +- 7 files changed, 191 insertions(+), 191 deletions(-) diff --git a/lang/qet_en.ts b/lang/qet_en.ts index 4368b8d93..fc3c35a1c 100644 --- a/lang/qet_en.ts +++ b/lang/qet_en.ts @@ -358,12 +358,12 @@ Ajuster la taille du tableau au folio - Adjust the size of the table to the folio + Adjust the size of the table to the sheet Ajouter de nouveau folio et tableau si nécessaire. - Add new folio and table if necessary. + Add new sheet and table if necessary. @@ -570,7 +570,7 @@ Folio - Folio + Sheet @@ -603,7 +603,7 @@ Apply to Selected Folios - Apply to Selected Folios + Apply to Selected Sheets @@ -670,7 +670,7 @@ Folio - Folio + Sheet @@ -710,7 +710,7 @@ Note: These options DO NOT allow or block Auto Numberings, only their Update Pol -Both: both new and existent element labels will be updated. This is the default option. -Update only new: only new created elements will be updated. Existent element labels will be frozen. -Update only existent: only existent elements will be updated. New elements will be assigned their formula but will not update once created. --Disable: both new and existent element labels will not be updated. This is valid for new folios as well. +-Disable: both new and existent element labels will not be updated. This is valid for new sheets as well. Note: these options DO NOT allow or block auto numberings, only their update policy. @@ -777,7 +777,7 @@ Note: these options DO NOT allow or block auto numberings, only their update pol Position du folio - Folio position + Sheet position @@ -865,7 +865,7 @@ Note: these options DO NOT allow or block auto numberings, only their update pol Dimensions du folio - Folio size + Sheet size @@ -1185,7 +1185,7 @@ Note: these options DO NOT allow or block auto numberings, only their update pol Afficher un texte de potentiel par folio. - Show one text per folio potential. + Show one text per sheet potential. @@ -1538,13 +1538,13 @@ Note: these options DO NOT allow or block auto numberings, only their update pol Couleur de fond du folio - Folio background color + Sheet background color Choisir la couleur de fond du folio status bar tip - Choose the folio background color + Choose the sheet background color @@ -1921,7 +1921,7 @@ Note: these options DO NOT allow or block auto numberings, only their update pol Propriétés du folio window title - Folio properties + Sheet properties @@ -1967,7 +1967,7 @@ Hold Shift and drag to move this text on its own Renvoi de folio - Folio reference + Sheet reference @@ -3045,12 +3045,12 @@ The element's display name is edited separately in the element properties.< Renvoi de folio suivant - Next folio reference + Next sheet reference Renvoi de folio précédent - Previous folio reference + Previous sheet reference @@ -3351,7 +3351,7 @@ The element's display name is edited separately in the element properties.< Folio : %1 - Folio : %1 + Sheet : %1 @@ -3629,17 +3629,17 @@ The element's display name is edited separately in the element properties.< Titre du folio - Folio title + Sheet title Position du folio - Folio position + Sheet position Numéro du folio - Folio number + Sheet number @@ -4100,7 +4100,7 @@ By importing this file, you confirm that: Glissez-déposez ce modèle de cartouche sur un folio pour l'y appliquer. Status tip displayed when selecting a title block template - Drag and drop this title block template onto a folio to apply it. + Drag and drop this title block template onto a sheet to apply it. @@ -4185,22 +4185,22 @@ By importing this file, you confirm that: Propriétés du folio - Folio properties + Sheet properties Ajouter un folio - Add a folio + Add a sheet Insérer un folio au-dessus - Insert a folio number above + Insert a sheet number above Insérer un folio en dessous - Insert a folio number below + Insert a sheet number below @@ -4210,42 +4210,42 @@ By importing this file, you confirm that: Supprimer ce folio - Delete this folio + Delete this sheet Remonter ce folio - Move this folio up + Move this sheet up Abaisser ce folio - Move down this folio + Move down this sheet Remonter ce folio x10 - Move this folio up x10 + Move this sheet up x10 Remonter ce folio x100 - Move this folio up x100 + Move this sheet up x100 Remonter ce folio au debut - Move this folio to the beginning + Move this sheet to the beginning Abaisser ce folio x10 - Move down this folio x10 + Move down this sheet x10 Abaisser ce folio x100 - Move down this folio x100 + Move down this sheet x100 @@ -4361,12 +4361,12 @@ By importing this file, you confirm that: Exporter les folios du projet window title - Export the project folios + Export the project sheets Choisissez les folios que vous désirez exporter ainsi que leurs dimensions : - Choose the folios you wish to export and specify their size : + Choose the sheets you wish to export and specify their size : @@ -4381,13 +4381,13 @@ By importing this file, you confirm that: Titre du folio - Folio title + Sheet title Vous devez entrer un nom de fichier non vide et unique pour chaque folio à exporter. message box content - You must enter a non-empty, unique filename for each folio to export. + You must enter a non-empty, unique filename for each sheet to export. @@ -4477,7 +4477,7 @@ By importing this file, you confirm that: Exporter entièrement le folio - Export the whole folio + Export the whole sheet @@ -4535,17 +4535,17 @@ By importing this file, you confirm that: C&réer de nouveaux folios - C&reate new folios + C&reate new sheets Numérotation automatique des folios sélectionnés - Automatic numbering of selected folios + Automatic numbering of selected sheets Nouveaux folios - New folios + New sheets @@ -4565,13 +4565,13 @@ By importing this file, you confirm that: Numérotation automatique de Folio : - Folio Auto Numbering : + Sheet Auto Numbering : Folio Autonumbering title window - Folio Autonumbering + Sheet Autonumbering @@ -4588,12 +4588,12 @@ Si le chiffre défini dans le champ Valeur possède moins de digits que le type Le champ "Incrémentation" n'est pas utilisé. help dialog about the folio autonumerotation - This is where you can define how the new folios will be numbered. + This is where you can define how the new sheets will be numbered. -A numbering is composed of a minimum variable. -You can add or delete a dialing variable through the - and + buttons. A numbering variable includes: a type, a value and an increment. --the "Digit 1", "Digit 01" and "Digit 001" types represent a numeric type defined in the "Value" field, which increments each new folio by the value of the "Incrementation" field. +-the "Digit 1", "Digit 01" and "Digit 001" types represent a numeric type defined in the "Value" field, which increments each new sheet by the value of the "Incrementation" field. - "Digit 01" and "Digit 001", are respectively represented on the diagram by two and three digits minimum. If the digit defined in the Value field has fewer digits than the chosen type, it will be preceded by one or two 0s in order to respect its type. @@ -4658,9 +4658,9 @@ that you create. Text and number inputs are -%prefix: Default Element Prefix -%l: Element Line -%c: Element Column - -%F: Folio Name - -%f or %id: Folio ID - -%total: Total of folios + -%F: Sheet Name + -%f or %id: Sheet ID + -%total: Total of sheets You can also assign any other titleblock variable that you create. Text and number inputs are also available. @@ -4853,7 +4853,7 @@ that you create. Text and number inputs are Utiliser les numéros de folio à la place de leur position dans le projet - Use folio numbers instead of their position in the project + Use sheet numbers instead of their position in the project @@ -4971,7 +4971,7 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments Par défaut, un double-clic insère l'élément sur le folio ; l'édition reste accessible par le menu contextuel. - By default, a double-click inserts the element on the folio; editing remains accessible from the context menu. + By default, a double-click inserts the element on the sheet; editing remains accessible from the context menu. @@ -5105,7 +5105,7 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments Fraction de la grille des folios. Maintenir Ctrl pendant le déplacement pour placer librement. - Fraction of the folio grid. Hold Ctrl while moving to place freely. + Fraction of the sheet grid. Hold Ctrl while moving to place freely. @@ -5182,7 +5182,7 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments Autoriser le dézoom au delà du folio - Allow unzoom beyond the folio + Allow unzoom beyond the sheet @@ -5423,13 +5423,13 @@ Any setting other than “No rounding” may cause rendering errors in the proje Ceci est un projet QElectroTech, c'est-à-dire un fichier d'extension .qet regroupant plusieurs folios. Il embarque également les éléments et modèles de cartouches utilisés dans ces folios. "What's this" tip - This is a QElectroTech project, ie. a .qet file containing several folios. It also embeds elements and title block templates used in the folios. + This is a QElectroTech project, ie. a .qet file containing several sheets. It also embeds elements and title block templates used in the sheets. Folio sans titre Fallback label when a diagram has no title - Untitled folio + Untitled sheet @@ -5453,7 +5453,7 @@ Any setting other than “No rounding” may cause rendering errors in the proje Ceci est un modèle de cartouche, qui peut être appliqué à un folio. "What's this" tip - This is a title block template, which can be applied to a folio. + This is a title block template, which can be applied to a sheet. @@ -5518,7 +5518,7 @@ Any setting other than “No rounding” may cause rendering errors in the proje Ajuster le tableau au folio - Fit table to folio + Fit table to sheet TextLabel @@ -6056,12 +6056,12 @@ Please use the advanced editor for this. Folio %1, case %2 - Folio %1, cell %2 + Sheet %1, cell %2 Folio %1 (%2), case %3 - Folio %1 (%2), cell %3 + Sheet %1 (%2), cell %3 Nom, label ou information de l'élément… @@ -6195,13 +6195,13 @@ Please use the advanced editor for this. N° de folio - Folio no. + Sheet no. Label de folio - Folio label + Sheet label @@ -6209,7 +6209,7 @@ Please use the advanced editor for this. Titre de folio - Folio title + Sheet title @@ -6297,7 +6297,7 @@ Please use the advanced editor for this. Report de folio - Folio reference + Sheet reference @@ -6364,18 +6364,18 @@ Please use the advanced editor for this. N° de folio - Folio no. + Sheet no. Label de folio - Folio label + Sheet label Titre de folio - Folio title + Sheet title @@ -6573,7 +6573,7 @@ Do you still want to link this slave contact? Folio - Folio + Sheet @@ -6583,7 +6583,7 @@ Do you still want to link this slave contact? Reports de folio - Folio references + Sheet references @@ -6613,7 +6613,7 @@ Do you still want to link this slave contact? Folios - Folios + Sheets @@ -6624,7 +6624,7 @@ Do you still want to link this slave contact? Nouveau folio configuration page title - New folio + New sheet @@ -6816,7 +6816,7 @@ Do you still want to link this slave contact? N° folio - Folio no. + Sheet no. @@ -6824,7 +6824,7 @@ Do you still want to link this slave contact? Folio - Folio + Sheet @@ -6832,7 +6832,7 @@ Do you still want to link this slave contact? Chiffre 1 - Folio - number format 1 - Folio + number format 1 - Sheet @@ -6840,7 +6840,7 @@ Do you still want to link this slave contact? Chiffre 01 - Folio - number format 01 - Folio + number format 01 - Sheet @@ -6848,7 +6848,7 @@ Do you still want to link this slave contact? Chiffre 001 - Folio - number format 001 - Folio + number format 001 - Sheet @@ -7265,7 +7265,7 @@ Conductor section : %1 Veuillez saisir une formule compatible pour ce potentiel. Les variables suivantes sont incompatibles : %sequf_ %seqtf_ %seqhf_ %id %F %M %LM - The new potential formula contains variables incompatible with folio references. + The new potential formula contains variables incompatible with sheet references. Please enter a compatible formula for this potential. The following variables are incompatible: %sequf_ %seqtf_ %seqhf_ %id %F %M %LM @@ -7308,12 +7308,12 @@ The following variables are incompatible: Folios - Folios + Sheets Numérotation auto des folios - Folio Auto Numbering + Sheet Auto Numbering @@ -7433,7 +7433,7 @@ The following variables are incompatible: Position du folio - Folio position + Sheet position @@ -7469,13 +7469,13 @@ The following variables are incompatible: Ce titre sera disponible pour tous les folios de ce projet en tant que %projecttitle. informative label - This title is made available to all child folios as %projecttitle. + This title is made available to all child sheets as %projecttitle. Vous pouvez définir ci-dessous des propriétés personnalisées qui seront disponibles pour tous les folios de ce projet (typiquement pour les cartouches). informative label - You may define below custom properties that will be made available to all folios of the project (typically to use within title blocks). + You may define below custom properties that will be made available to all sheets of the project (typically to use within title blocks). @@ -7513,7 +7513,7 @@ The following variables are incompatible: Folios à imprimer : - Folios to print : + Sheets to print : @@ -7583,7 +7583,7 @@ The following variables are incompatible: Adapter le folio à la page - Fit folio to page + Fit sheet to page @@ -7593,7 +7593,7 @@ The following variables are incompatible: Si cette option est cochée, le folio sera agrandi ou rétréci de façon à remplir toute la surface imprimable d'une et une seule page." - If this option is checked, the folio will be enlarged or shrunk to fill the entire printable surface of one and only one page. " + If this option is checked, the sheet will be enlarged or shrunk to fill the entire printable surface of one and only one page. " @@ -7705,7 +7705,7 @@ The following variables are incompatible: Folio sans titre - Untitled folio + Untitled sheet @@ -7752,13 +7752,13 @@ Do you want to save changes? Supprimer le folio ? message box title - Delete this folio ? + Delete this sheet ? Êtes-vous sûr de vouloir supprimer ce folio du projet ? message box content - Are you sure you want to delete this folio from the project? + Are you sure you want to delete this sheet from the project? @@ -7786,7 +7786,7 @@ Do you want to save changes? Ajouter un folio - Add a folio + Add a sheet @@ -7812,7 +7812,7 @@ Do you want to save changes? Ce projet ne contient aucun folio label displayed when a project contains no diagram - This project does not contain any folio + This project does not contain any sheet @@ -7829,7 +7829,7 @@ Do you want to save changes? <p align="center"><b>Ouverture du projet en cours...</b><br/>Création des onglets de folio :</p> - <p align="center"><b>Opening the project ...</b><br/>Creating folio tabs :</p> + <p align="center"><b>Opening the project ...</b><br/>Creating sheet tabs :</p> @@ -8073,7 +8073,7 @@ Available options: Chargement... Éditeur de schéma splash screen caption - Loading... Folios editor + Loading... Sheets editor @@ -8243,7 +8243,7 @@ Available options: Ce panneau liste les différentes actions effectuées sur le folio courant. Cliquer sur une action permet de revenir à l'état du schéma juste après son application. "What's this" tip - This panel lists the various changes applied to the current folio. Clicking a change gets the folio back to its state right after it was applied. + This panel lists the various changes applied to the current sheet. Clicking a change gets the sheet back to its state right after it was applied. Couleur de fond blanc/gris @@ -8253,7 +8253,7 @@ Available options: Affiche la couleur de fond du folio en blanc ou en gris Status tip of white/grey background button - Displays the background color of the folio in white or gray + Displays the background color of the sheet in white or gray @@ -8263,24 +8263,24 @@ Available options: Affiche ou masque la grille des folios - Display or hide the grid of folio + Display or hide the grid of sheet Propriétés du folio - Folio properties + Sheet properties Ajouter un folio - Add a folio + Add a sheet Supprimer le folio - Delete this folio + Delete this sheet @@ -8349,7 +8349,7 @@ Available options: Exporte un ou plusieurs folios du projet courant status bar tip - Export one or more folios of the current project + Export one or more sheets of the current project @@ -8423,7 +8423,7 @@ Available options: Enregistre le projet courant et tous ses folios status bar tip - Saves the current project and all its folios + Saves the current project and all its sheets @@ -8447,66 +8447,66 @@ Available options: Ajoute une colonne au folio status bar tip - Add a column to the folio + Add a column to the sheet Enlève une colonne au folio status bar tip - Remove a column from the folio + Remove a column from the sheet Agrandit le folio en hauteur status bar tip - Expands the folio height + Expands the sheet height Rétrécit le folio en hauteur status bar tip - Shrinks the folio height + Shrinks the sheet height Enlève les éléments sélectionnés du folio status bar tip - Removes selected elements from the folio + Removes selected elements from the sheet Sélectionne tous les éléments du folio status bar tip - Selects all elements on the folio + Selects all elements on the sheet Désélectionne tous les éléments du folio status bar tip - Deselect all elements on the folio + Deselect all elements on the sheet Agrandit le folio status bar tip - Expand the folio + Expand the sheet Rétrécit le folio status bar tip - Shrinks the folio + Shrinks the sheet Adapte le zoom de façon à afficher tout le contenu du folio indépendamment du cadre - Adjusts the zoom to display all the contents of folio regardless of context + Adjusts the zoom to display all the contents of sheet regardless of context Adapte le zoom exactement sur le cadre du folio status bar tip - Adjusts the zoom on exactly the part of the folio + Adjusts the zoom on exactly the part of the sheet @@ -8533,13 +8533,13 @@ Available options: Exporte le folio courant dans un autre format status bar tip - Exports the current folio to another format + Exports the current sheet to another format Imprime un ou plusieurs folios du projet courant status bar tip - Prints one or more folio of the current project + Prints one or more sheet of the current project @@ -8556,19 +8556,19 @@ Available options: Place les éléments du presse-papier sur le folio status bar tip - Pastes elements from the clipboard into the folio + Pastes elements from the clipboard into the sheet Édite les propriétés du folio (dimensions, informations du cartouche, propriétés des conducteurs...) status bar tip - Edits the properties of the folio (size, title block information, conductor properties...) + Edits the properties of the sheet (size, title block information, conductor properties...) Permet de visualiser le folio sans pouvoir le modifier status bar tip - Allows to view the folio without modifying it + Allows to view the sheet without modifying it @@ -8964,7 +8964,7 @@ Hold Ctrl while moving to place freely. Garde les numéros de colonne et les lettres de ligne du folio visibles au bord de la vue - Keeps the folio's column numbers and row letters visible at the edge of the view + Keeps the sheet's column numbers and row letters visible at the edge of the view @@ -8974,7 +8974,7 @@ Hold Ctrl while moving to place freely. Trace les limites des colonnes et des lignes du folio sur le schéma, à l'écran seulement - Draws the boundaries of the folio's columns and rows on the diagram, on screen only + Draws the boundaries of the sheet's columns and rows on the diagram, on screen only @@ -9078,7 +9078,7 @@ Hold Ctrl while moving to place freely. Relance le dernier outil de dessin ou la dernière insertion d'élément (Entrée sur le folio) status bar tip - Relaunches the last drawing tool or element insertion (Enter on the folio) + Relaunches the last drawing tool or element insertion (Enter on the sheet) @@ -9111,13 +9111,13 @@ Hold Ctrl while moving to place freely. Sélectionne tous les conducteurs du folio, désélectionne le reste status bar tip - Selects all conductors on the folio, deselects the rest + Selects all conductors on the sheet, deselects the rest Sélectionne tous les champs de texte du folio, désélectionne le reste status bar tip - Selects all text fields on the folio, deselects the rest + Selects all text fields on the sheet, deselects the rest @@ -9137,47 +9137,47 @@ Hold Ctrl while moving to place freely. Ajoute un champ de texte sur le folio actuel - Adds a text field to the current folio + Adds a text field to the current sheet Ajoute une image sur le folio actuel - Add an image to the current folio + Add an image to the current sheet Ajoute une page PDF sur le folio actuel - Adds a PDF page to the current folio + Adds a PDF page to the current sheet Ajoute une ligne sur le folio actuel - Add a line to the current folio + Add a line to the current sheet Ajoute un rectangle sur le folio actuel - Adds a rectangle on the current folio + Adds a rectangle on the current sheet Ajoute une ellipse sur le folio actuel - Add an ellipse to the current folio + Add an ellipse to the current sheet Ajoute une polyligne sur le folio actuel - Adds a polyline to the current folio + Adds a polyline to the current sheet Ajoute une courbe de Bézier sur le folio actuel - Adds a Bézier curve to the current folio + Adds a Bézier curve to the current sheet Ajoute un plan de bornier sur le folio actuel - Adds a terminal plan to the current folio + Adds a terminal plan to the current sheet @@ -9188,7 +9188,7 @@ Hold Ctrl while moving to place freely. Recherche et sélectionne rapidement un élément du folio status bar tip - Quickly search for and select an element in the folio + Quickly search for and select an element in the sheet @@ -9334,22 +9334,22 @@ Unbridge and/or remove the levels from the affected terminals so that they can b Êtes-vous sûr de vouloir supprimer ce folio ? - Are you sure you want to delete this folio? + Are you sure you want to delete this sheet? Supprimer les folios - Delete the folios + Delete the sheets Êtes-vous sûr de vouloir supprimer les %1 folios sélectionnés ? - Are you sure you want to delete the selected %1 folios ? + Are you sure you want to delete the selected %1 sheets ? Supprimer %1 folios - Delete %1 folios + Delete %1 sheets @@ -9360,7 +9360,7 @@ Unbridge and/or remove the levels from the affected terminals so that they can b Déplacer les folios - Move the folios + Move the sheets @@ -9381,7 +9381,7 @@ Unbridge and/or remove the levels from the affected terminals so that they can b folio %1 - folio %1 + sheet %1 @@ -9858,7 +9858,7 @@ Enable scripts? This setting can be changed in Configure QElectroTech > Gener <br><b>Erreur</b> :<br>Les reports de folio doivent posséder une seul borne.<br><b>Solution</b> :<br>Verifier que l'élément ne possède qu'une seul borne - <br><b>Error</b>:<br>Folio references must have a single terminal.<br><b>Solution</b>:<br>Check that the element has only one terminal + <br><b>Error</b>:<br>Sheet references must have a single terminal.<br><b>Solution</b>:<br>Check that the element has only one terminal @@ -10446,7 +10446,7 @@ What do you wish to do ? <p align="center"><b>Ouverture du projet en cours...</b><br/>Création des folios</p> - <p align="center"><b>Opening the project ...</b><br/>Creation of folios</p> + <p align="center"><b>Opening the project ...</b><br/>Creation of sheets</p> @@ -10906,7 +10906,7 @@ What do you wish to do ? modifier les dimensions du folio undo caption - change the folio size + change the sheet size @@ -11252,7 +11252,7 @@ What do you wish to do ? Folio sans titre - Untitled folio + Untitled sheet @@ -11588,12 +11588,12 @@ What do you wish to do ? Position du folio - Folio position + Sheet position Numéro de folio - Folio number + Sheet number @@ -11789,7 +11789,7 @@ Do you want to replace it ? Chercher/remplacer les propriétés de folio - Find / replace folio properties + Find / replace sheet properties @@ -11845,17 +11845,17 @@ Do you want to replace it ? Nombre de folio - Number of folios + Number of sheets Numéro du folio précédent - Number of the previous folio + Number of the previous sheet Numéro du folio suivant - Number of the following folio + Number of the following sheet @@ -12630,19 +12630,19 @@ Please download it by following the link and unzip it in the installation folder Ajouter un folio undo command text - Add a folio + Add a sheet Déplacer un folio undo command text - Moving a folio + Moving a sheet Supprimer un folio undo command text - Delete a folio + Delete a sheet @@ -13765,7 +13765,7 @@ Please add a new table or adjust the existing table to display all information.< Folio : - Folio: + Sheet: @@ -13801,9 +13801,9 @@ Les variables suivantes sont utilisables : - %autonum : Folio Auto Numeration Available as % folio for titleblock models The following variables can be used: --%id: current number of folios in the project --%total: total number of folios in the project --%autonum: folio auto numbering +-%id: current number of sheets in the project +-%total: total number of sheets in the project +-%autonum: sheet auto numbering @@ -13849,7 +13849,7 @@ E.g. associating the name "volta" with the value "1745" will Label de report de folio - Folio reference label + Sheet reference label @@ -13861,10 +13861,10 @@ Créer votre propre texte en vous aidant des variables suivantes : %LM : la localisation %l : le numéro de ligne %c : le numéro de colonne - You can define a custom label for folio references. + You can define a custom label for sheet references. Create your own text using the following variables: -%f: the folio position in the project -%F: the folio number +%f: the sheet position in the project +%F: the sheet number %M: the installation %LM: the location %l: the row number @@ -13906,7 +13906,7 @@ Create your own text using the following variables: Champ texte de folio - Folio text field + Sheet text field @@ -13916,12 +13916,12 @@ Create your own text using the following variables: <html><head/><body><p>Définir les propriétés à remplacer dans les folios</p></body></html> - <html><head/><body><p>Define properties to replace in folios</p></body></html> + <html><head/><body><p>Define properties to replace in sheets</p></body></html> Folio - Folio + Sheet @@ -14001,7 +14001,7 @@ Create your own text using the following variables: Folios - Folios + Sheets @@ -14031,7 +14031,7 @@ Create your own text using the following variables: Eléments report de folio - Folio reference elements + Sheet reference elements @@ -14041,17 +14041,17 @@ Create your own text using the following variables: Sélectionner les éléments de ce folio - Select the elements of this folio + Select the elements of this sheet Sélectionner les conducteurs de ce folio - Select the conductors of this folio + Select the conductors of this sheet Sélectionner les textes de ce folio - Select the texts of this folio + Select the texts of this sheet @@ -14158,7 +14158,7 @@ Create your own text using the following variables: Folio Autonumérotation title window - Folio Auto Numbering + Sheet Auto Numbering @@ -14175,12 +14175,12 @@ Si le chiffre défini dans le champ Valeur possède moins de digits que le type Le champ "Incrémentation" n'est pas utilisé. help dialog about the folio autonumerotation - This is where you can define how the new folios will be numbered. + This is where you can define how the new sheets will be numbered. -A numbering is composed of a minimum variable. -You can add or delete a dialing variable through the - and + buttons. A numbering variable includes: a type, a value and an increment. --the "Digit 1", "Digit 01" and "Digit 001" types represent a numeric type defined in the "Value" field, which increments each new folio by the value of the "Incrementation" field. +-the "Digit 1", "Digit 01" and "Digit 001" types represent a numeric type defined in the "Value" field, which increments each new sheet by the value of the "Incrementation" field. - "Digit 01" and "Digit 001", are respectively represented on the diagram by two and three digits minimum. If the digit defined in the Value field has fewer digits than the chosen type, it will be preceded by one or two 0s in order to respect its type. @@ -14220,10 +14220,10 @@ If the digit defined in the Value field has fewer digits than the chosen type, i -Type "Text", represents a fixed text. The "Incrementation" field is not used. --The "folio" type represents the number of the current folio. +-The "sheet" type represents the number of the current sheet. The other fields are not used. --The "Folio" type represents the name of the current folio. +-The "Sheet" type represents the name of the current sheet. The other fields are not used. @@ -14264,10 +14264,10 @@ If the digit defined in the Value field has fewer digits than the chosen type, i -Type "Text", represents a fixed text. The "Incrementation" field is not used. --The "folio" type represents the number of the current folio. +-The "sheet" type represents the number of the current sheet. The other fields are not used. --The "Folio" type represents the name of the current folio. +-The "Sheet" type represents the name of the current sheet. The other fields are not used. @@ -14578,7 +14578,7 @@ The other fields are not used. Folio, rien de sélectionné - Folio, nothing selected + Sheet, nothing selected @@ -17613,7 +17613,7 @@ The other fields are not used. Créer un Folio Numérotation Auto - Create an auto folio numbering + Create an auto sheet numbering @@ -17628,7 +17628,7 @@ The other fields are not used. Folio : - Folio: + Sheet: @@ -17699,9 +17699,9 @@ Les variables suivantes sont utilisables : - %autonum : Folio Auto Numeration Available as % folio for titleblock models The following variables can be used: --%id: current number of folios in the project --%total: total number of folios in the project --%autonum: folio auto numbering +-%id: current number of sheets in the project +-%total: total number of sheets in the project +-%autonum: sheet auto numbering @@ -17711,7 +17711,7 @@ The following variables can be used: <html><head/><body><p>Affiche le cartouche en bas (horizontalement) ou à droite (verticalement) du folio.</p></body></html> - <html> <head/><body><p>Sets the title block at the bottom (horizontal) or right (vertically) of the folio.</p></body></html> + <html> <head/><body><p>Sets the title block at the bottom (horizontal) or right (vertically) of the sheet.</p></body></html> @@ -17805,7 +17805,7 @@ associer le nom "volta" et la valeur "1745" remplacera %{vol Attention : les bordures des cellules vides n'apparaissent pas lors du rendu final sur le folio. - Warning: the edges of empty cells do not appear in the final render the folio. + Warning: the edges of empty cells do not appear in the final render the sheet. @@ -17906,12 +17906,12 @@ associer le nom "volta" et la valeur "1745" remplacera %{vol Par défaut, les variables suivantes sont disponibles :<ul><li>%{author} : auteur du folio</li><li>%{date} : date du folio</li><li>%{title} : titre du folio</li><li>%{filename} : nom de fichier du projet</li><li>%{plant} : nom de l'installation (=) dans laquelle se trouve le folio</li><li>%{locmach} : nom de la localisation (+) dans laquelle se trouve le folio</li><li>%{indexrev} : indice de révision du folio</li><li>%{version} : version du logiciel</li><li>%{folio} : numéro du folio</li><li>%{folio-id} : position du folio dans le projet</li><li>%{folio-total} : nombre total de folios dans le projet</li><li>%{previous-folio-num} : numéro du folio précédent</li><li>%{next-folio-num} : numéro du folio suivant</li><li>%{projecttitle} : titre du projet</li><li>%{projectpath} : chemin du projet</li><li>%{projectfilename} : nom du fichier</li><li>%{saveddate} : date d'enregistrement du fichier format local</li><li>%{saveddate-eu} : date d'enregistrement du fichier format dd-MM-yyyy</li><li>%{saveddate-us} : date d'enregistrement du fichier format yyyy-MM-dd</li><li>%{savedtime} : heure d'enregistrement du fichier</li><li>%{savedfilename} : nom du fichier enregistré</li><li>%{savedfilepath} : chemin du fichier enregistré</li></ul> - By default, the following variables are available: <ul><li>%{author}: author of the folio</li><li>%{date}: folio date</li><li>%{title}: folio title</li><li>%{filename}: filename of file</li> <li>%{plant}: name of the installation (=) in which the folio is located</li><li>%{locmach}: name of the location (+) where the folio is located</li><li>%{indexrev}: folio revision index</li><li>%{version}: version number</li><li>%{folio}: folio number</li><li>%{folio-id}: position of folio in the project</li><li>%{folio-total }: total number of folios in the project</li><li>%{previous-folio-num}: number of the previous folio</li><li>%{next-folio-num}: number of the next folio< / li><li>%{projecttitle}: title of the project</li><li>%{projectpath}: path of the project</li><li>%{projectfilename}: filename of the projectfile</li><li >%{saveddate}: date of saving the file in local format</li><li>%{saveddate-eu}: date of saving the file in format DD-MM-YYYY</li> <li>%{ saveddate-us}: date of saving the file in format YYYY-MM-DD</li><li>%{savedtime}: time of saving the file</li><li>%{savedfilename}: name of the file saved</li><li>%{savedfilepath}: path of the saved file</li></ul> + By default, the following variables are available: <ul><li>%{author}: author of the sheet</li><li>%{date}: sheet date</li><li>%{title}: sheet title</li><li>%{filename}: filename of file</li> <li>%{plant}: name of the installation (=) in which the sheet is located</li><li>%{locmach}: name of the location (+) where the sheet is located</li><li>%{indexrev}: sheet revision index</li><li>%{version}: version number</li><li>%{folio}: sheet number</li><li>%{folio-id}: position of sheet in the project</li><li>%{folio-total }: total number of sheets in the project</li><li>%{previous-folio-num}: number of the previous sheet</li><li>%{next-folio-num}: number of the next sheet< / li><li>%{projecttitle}: title of the project</li><li>%{projectpath}: path of the project</li><li>%{projectfilename}: filename of the projectfile</li><li >%{saveddate}: date of saving the file in local format</li><li>%{saveddate-eu}: date of saving the file in format DD-MM-YYYY</li> <li>%{ saveddate-us}: date of saving the file in format YYYY-MM-DD</li><li>%{savedtime}: time of saving the file</li><li>%{savedfilename}: name of the file saved</li><li>%{savedfilepath}: path of the saved file</li></ul> Chaque cellule d'un cartouche affiche une valeur, optionnellement précédée d'un label. Tous deux peuvent être traduits en plusieurs langues.<br/>Comme ce que vous éditez actuellement est un <em>modèle</em> de cartouche, ne saisissez pas directement des données brutes : insérez plutôt des variables sous la forme %{nom-de-variable}, qui seront ensuite remplacées par les valeurs adéquates sur le folio. - Each cell of a title block displays a value, optionally preceded by a label. Both can be translated to several languages.<br/>Since what you are currently editing is a title block <em>template</em>, avoid entering raw data directly: prefer inserting variables like %{variable-name}, which will be replaced afterwards with adequate values in the folio. + Each cell of a title block displays a value, optionally preceded by a label. Both can be translated to several languages.<br/>Since what you are currently editing is a title block <em>template</em>, avoid entering raw data directly: prefer inserting variables like %{variable-name}, which will be replaced afterwards with adequate values in the sheet. @@ -18316,7 +18316,7 @@ associer le nom "volta" et la valeur "1745" remplacera %{vol Folio column title - Folio + Sheet @@ -18548,8 +18548,8 @@ associer le nom "volta" et la valeur "1745" remplacera %{vol %M: Installation %LM: Localisation Create your own text using the following variables: -%f: the folio number -%F: the folio label +%f: the sheet number +%F: the sheet label %l: the row number %c: the column number %M: the installation @@ -18813,7 +18813,7 @@ associer le nom "volta" et la valeur "1745" remplacera %{vol Folio - Folio + Sheet diff --git a/titleblocks/A4_1.titleblock b/titleblocks/A4_1.titleblock index 9b4968380..2c14dabc6 100644 --- a/titleblocks/A4_1.titleblock +++ b/titleblocks/A4_1.titleblock @@ -284,7 +284,7 @@ Created by potrace 1.10, written by Peter Selinger 2001-2011 Folio Ark Seite - Folio + Sheet Folio Folio List diff --git a/titleblocks/DIN_A4.titleblock b/titleblocks/DIN_A4.titleblock index 92508662a..9b7ec1a4e 100644 --- a/titleblocks/DIN_A4.titleblock +++ b/titleblocks/DIN_A4.titleblock @@ -251,7 +251,7 @@ Created by potrace 1.10, written by Peter Selinger 2001-2011 Folio Ark Seite - Folio + Sheet Folio Folio List @@ -349,7 +349,7 @@ Created by potrace 1.10, written by Peter Selinger 2001-2011 Folio Ark Seite - Folio + Sheet Folio Folio List diff --git a/titleblocks/ISO7200_A4_V1.titleblock b/titleblocks/ISO7200_A4_V1.titleblock index 56ac4133e..7dc609347 100644 --- a/titleblocks/ISO7200_A4_V1.titleblock +++ b/titleblocks/ISO7200_A4_V1.titleblock @@ -648,7 +648,7 @@ Created by potrace 1.11, written by Peter Selinger 2001-2013 Ark Seite Σελίδα - Folio + Sheet Folio Folio List diff --git a/titleblocks/default.titleblock b/titleblocks/default.titleblock index c1883ba7c..26c14cb02 100644 --- a/titleblocks/default.titleblock +++ b/titleblocks/default.titleblock @@ -108,7 +108,7 @@ Ark Seite Σελίδα - Folio + Sheet Folio Folio List diff --git a/titleblocks/double-logo.titleblock b/titleblocks/double-logo.titleblock index c30dbec25..922f699fa 100644 --- a/titleblocks/double-logo.titleblock +++ b/titleblocks/double-logo.titleblock @@ -539,7 +539,7 @@ Ark Seite Σελίδα - Folio + Sheet Folio Folio List diff --git a/titleblocks/single-logo.titleblock b/titleblocks/single-logo.titleblock index e804c409e..5b8ddc3e6 100644 --- a/titleblocks/single-logo.titleblock +++ b/titleblocks/single-logo.titleblock @@ -538,7 +538,7 @@ Ark Seite Σελίδα - Folio + Sheet Folio Folio List From 1c1f9db3eb44d211e8cceb53d2c2f2892db96f22 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 2 Oct 2026 08:47:23 +1300 Subject: [PATCH 010/225] Scripting: qet.currentFolio(), and one undo step per script run in the editor A script started from the editor had no way to know which folio is on screen, and each qet.* call was its own undo step, so a script that adds twenty items needed twenty Ctrl+Z to take back. qet.currentFolio() returns the folio shown in the editor; through --run, which has no view, the first folio, or -1 when there is none. A run from the editor is now one undo macro, named after the script. A run that changed nothing leaves no empty entry behind. qet.undo() and qet.redo() inside a grouped run say why they cannot work (QUndoStack ignores them inside a macro) instead of failing silently. --run is unchanged: one step per call, so scripts that call qet.undo() keep working. Co-Authored-By: Claude Opus 5.5 --- sources/scripting/qetscriptapi.cpp | 37 ++++++++ sources/scripting/qetscriptapi.h | 4 + sources/scripting/qetscripting.cpp | 27 ++++++ sources/scripting/qetscripting.h | 5 +- tests/qttest/CMakeLists.txt | 12 +++ tests/qttest/tst_scriptcurrentfolio.cpp | 109 ++++++++++++++++++++++++ 6 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 tests/qttest/tst_scriptcurrentfolio.cpp diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index 53f3a41aa..3face8331 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -115,6 +115,23 @@ int QetScriptApi::folioCount() const return m_project ? m_project->diagrams().count() : 0; } +/** + @brief QetScriptApi::currentFolio + The index of the folio on screen, so a script started from the editor + acts where the user is looking. With no view (--run) there is no such + folio, and the first one stands in for it so a script written for the + editor can still be tried headless; -1 if the project has none. +*/ +int QetScriptApi::currentFolio() const +{ + if (!m_project) return -1; + const QList diagrams = m_project->diagrams(); + if (m_view && m_view->diagram()) { + return diagrams.indexOf(m_view->diagram()); + } + return diagrams.isEmpty() ? -1 : 0; +} + QString QetScriptApi::folioTitle(int index) const { if (!m_project) return QString(); @@ -3854,8 +3871,24 @@ bool QetScriptApi::setFolioTitle(int folioIndex, const QString &title) return true; } +/** + @brief QetScriptApi::setUndoGrouped + Set by QetScripting::runOnProject() while the whole run is one undo + macro. QUndoStack cannot undo or redo inside a macro: it prints a + warning and does nothing, so undo() and redo() say so instead. +*/ +void QetScriptApi::setUndoGrouped(bool grouped) +{ + m_undo_grouped = grouped; +} + bool QetScriptApi::undo() { + if (m_undo_grouped) { + log(QStringLiteral("qet.undo: not available here -- this run is one " + "undo step; press Ctrl+Z after it to undo it")); + return false; + } if (!m_project || !m_project->undoStack()->canUndo()) return false; m_project->undoStack()->undo(); return true; @@ -3863,6 +3896,10 @@ bool QetScriptApi::undo() bool QetScriptApi::redo() { + if (m_undo_grouped) { + log(QStringLiteral("qet.redo: not available while this run is one undo step")); + return false; + } if (!m_project || !m_project->undoStack()->canRedo()) return false; m_project->undoStack()->redo(); return true; diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h index dff3225e7..e79d44a84 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -354,6 +354,7 @@ class QetScriptApi : public QObject Q_INVOKABLE QString projectTitle() const; Q_INVOKABLE QString filePath() const; Q_INVOKABLE int folioCount() const; + Q_INVOKABLE int currentFolio() const; Q_INVOKABLE QString folioTitle(int index) const; Q_INVOKABLE QString folioUuid(int index) const; Q_INVOKABLE int folioIndex(const QString &uuid) const; @@ -578,6 +579,8 @@ class QetScriptApi : public QObject // -- logging: a script has no console of its own -- Q_INVOKABLE void log(const QString &message); + void setUndoGrouped(bool grouped); + private: bool runFlag(const QString &flag, const QStringList &args); Element *findElement(int folioIndex, const QString &elementUuid) const; @@ -597,6 +600,7 @@ class QetScriptApi : public QObject QETProject *m_project; DiagramView *m_view; + bool m_undo_grouped = false; QString m_query_error; }; diff --git a/sources/scripting/qetscripting.cpp b/sources/scripting/qetscripting.cpp index 789b2dee9..a85bc9782 100644 --- a/sources/scripting/qetscripting.cpp +++ b/sources/scripting/qetscripting.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #ifdef QET_HAS_SCRIPTING #include @@ -162,6 +163,20 @@ bool runOnProject(const QString &scriptPath, QETProject *project, DiagramView *v } }); + // From the editor, a run is one undo step: a script that places + // twenty items is undone with one Ctrl+Z, not twenty. Headless --run + // keeps one step per call, which is what a script calling qet.undo() + // itself relies on. QUndoStack keeps an empty macro as a blank entry + // (and marks the project modified), so a script that changed nothing + // has its macro taken off again: an obsolete command is deleted by + // QUndoStack::undo() instead of being undone onto the redo side. + QUndoStack *stack = (view && project) ? project->undoStack() : nullptr; + if (stack) { + stack->beginMacro(QObject::tr("Script : %1") + .arg(QFileInfo(scriptPath).completeBaseName())); + api->setUndoGrouped(true); + } + QJSValue result = engine.evaluate(source, scriptPath); { std::lock_guard lock(mtx); @@ -170,6 +185,18 @@ bool runOnProject(const QString &scriptPath, QETProject *project, DiagramView *v cv.notify_one(); watchdog.join(); + if (stack) { + api->setUndoGrouped(false); + stack->endMacro(); + // Always the top of the undo side once the macro is closed, even + // when beginMacro() dropped a redo tail and count() went down. + const QUndoCommand *macro = stack->command(stack->index() - 1); + if (macro && macro->childCount() == 0) { + const_cast(macro)->setObsolete(true); + stack->undo(); + } + } + if (result.isError()) { const QString message = QStringLiteral("Script error: %1:%2: %3") .arg(scriptPath) diff --git a/sources/scripting/qetscripting.h b/sources/scripting/qetscripting.h index 09c8d9e0f..6c9e746be 100644 --- a/sources/scripting/qetscripting.h +++ b/sources/scripting/qetscripting.h @@ -51,8 +51,9 @@ namespace QetScripting { /** @brief Run @p scriptPath against an already-open @p project (the - "Run Script..." GUI macro path). Errors go to stderr; there is no - modal reporting in this first version. + "Run Script..." GUI macro path). Errors go to stderr, and with a + @p view also to a message box. With a @p view the whole run is one + undo step; without one, each call is its own step, as on --run. @param view the active DiagramView, so the script's zoom methods have something to act on; nullptr from the headless entry point, where they become no-ops (see QetScriptApi). diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index e339f3721..f60cbbf95 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -454,6 +454,18 @@ if(QET_HAS_SCRIPTING) target_compile_definitions(tst_scriptterminaluuid PRIVATE "QET_TEST_BINARY_PATH=\"$\"" "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"") + + # qet.currentFolio() through --run: the first folio, or -1 with none; and + # --run keeps one undo step per call (only an editor run is grouped). + add_executable( + tst_scriptcurrentfolio + tst_scriptcurrentfolio.cpp) + add_test(NAME tst_scriptcurrentfolio COMMAND tst_scriptcurrentfolio) + add_dependencies(tst_scriptcurrentfolio qelectrotech) + target_link_libraries(tst_scriptcurrentfolio PRIVATE Qt::Test) + target_compile_definitions(tst_scriptcurrentfolio PRIVATE + "QET_TEST_BINARY_PATH=\"$\"" + "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"") endif() # A symbol saved without a uuid gets the same one on every load, and diff --git a/tests/qttest/tst_scriptcurrentfolio.cpp b/tests/qttest/tst_scriptcurrentfolio.cpp new file mode 100644 index 000000000..418e93e01 --- /dev/null +++ b/tests/qttest/tst_scriptcurrentfolio.cpp @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include + +#include +#include +#include +#include +#include +#include + +// qet.currentFolio() is the folio on screen in the editor. Through --run +// there is none, so it stands in with the first folio, or -1 when the +// project has no folio. --run also keeps one undo step per call: only a run +// started from the editor is grouped into one, and a headless script that +// calls qet.undo() itself must go on working. +class tst_scriptcurrentfolio : public QObject +{ + Q_OBJECT + + QTemporaryDir m_dir; + + // Run @p script on @p project in a sandbox of its own and return the + // JSON object it logged. + QJsonObject run(const QString &script, const QString &project) + { + const QString path = m_dir.filePath(QStringLiteral("probe.js")); + const QString home = m_dir.filePath(QStringLiteral("home")); + QDir().mkpath(home); + QFile f(path); + if (!f.open(QIODevice::WriteOnly)) return {}; + f.write(script.toUtf8()); + f.close(); + + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen")); + env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1")); + env.insert(QStringLiteral("HOME"), home); + env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config")); + env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data")); + env.insert(QStringLiteral("TMPDIR"), m_dir.path()); + QProcess proc; + proc.setProcessEnvironment(env); + proc.start(QStringLiteral(QET_TEST_BINARY_PATH), + {QStringLiteral("--run"), path, project}); + if (!proc.waitForFinished(120000)) return {}; + const QString out = QString::fromUtf8(proc.readAllStandardOutput() + + proc.readAllStandardError()); + const QString mark = QStringLiteral("PROBE "); + for (const QString &line : out.split(QLatin1Char('\n'))) { + const int i = line.indexOf(mark); + if (i >= 0) + return QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).object(); + } + return {}; + } + +private slots: + void initTestCase() + { + QVERIFY(m_dir.isValid()); + QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH))); + } + + void firstFolioWithoutAView() + { + const QJsonObject r = run(QStringLiteral( + "qet.log('PROBE ' + JSON.stringify({current: qet.currentFolio(),\n" + " folios: qet.folioCount()}));\n"), + QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet")); + QVERIFY2(!r.isEmpty(), "the script logged nothing"); + QVERIFY(r.value(QStringLiteral("folios")).toInt() > 1); + QCOMPARE(r.value(QStringLiteral("current")).toInt(), 0); + } + + void noFolioIsMinusOne() + { + const QString project = m_dir.filePath(QStringLiteral("empty.qet")); + QFile out(project); + QVERIFY(out.open(QIODevice::WriteOnly)); + out.write("\n\n"); + out.close(); + + const QJsonObject r = run(QStringLiteral( + "qet.log('PROBE ' + JSON.stringify({current: qet.currentFolio(),\n" + " folios: qet.folioCount()}));\n"), project); + QVERIFY2(!r.isEmpty(), "the script logged nothing"); + QCOMPARE(r.value(QStringLiteral("folios")).toInt(), 0); + QCOMPARE(r.value(QStringLiteral("current")).toInt(), -1); + } + + void headlessUndoStaysPerCall() + { + const QJsonObject r = run(QStringLiteral( + "var f = qet.currentFolio();\n" + "var before = qet.texts(f).length;\n" + "qet.addText(f, 'one', 40, 40);\n" + "qet.addText(f, 'two', 40, 80);\n" + "var undone = qet.undo();\n" + "qet.log('PROBE ' + JSON.stringify({undone: undone,\n" + " added: qet.texts(f).length - before}));\n"), + QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet")); + QVERIFY2(!r.isEmpty(), "the script logged nothing"); + QCOMPARE(r.value(QStringLiteral("undone")).toBool(), true); + QCOMPARE(r.value(QStringLiteral("added")).toInt(), 1); + } +}; + +QTEST_MAIN(tst_scriptcurrentfolio) +#include "tst_scriptcurrentfolio.moc" From e03e52306921ec6ec2d8eb1822a7c4f11ba67613 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 2 Oct 2026 09:01:02 +1300 Subject: [PATCH 011/225] Script buttons: stored scripts become commands with an icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every .js file in the "scripts" folder of the user's data folder that starts with a // ==QETScript== header becomes a command: in Projet > Scripts, as a button on a new Scripts toolbar, and, because it is registered with ShortcutManager as diagrameditor.script., in the shortcut settings, the shortcut bar (S) and command search. The header gives its name, icon (a file next to the script or a theme icon; a tile with its initials otherwise), tooltip, default shortcut and when it is enabled (always, with a selection, with a conductor selected). The folder is watched, so a script added, edited or deleted while QET is open appears, changes or goes without a restart. A file with a header that cannot be used gets no button; the Scripts menu lists it with the reason. The menu also opens the folder, and holds "Exécuter un script…". A click runs the script on the current project as one undo step named after it, and asks to switch scripting on first, like "Exécuter un script…" does: scripting stays off by default. ShortcutManager::unregisterAction() takes a command out of the lists when its script is deleted, and lets it come back under a new name. Co-Authored-By: Claude Opus 5.5 --- cmake/qet_compilation_vars.cmake | 3 + sources/qetdiagrameditor.cpp | 189 +++++++++++++++++++++++---- sources/qetdiagrameditor.h | 13 +- sources/scripting/qetscripting.cpp | 8 +- sources/scripting/qetscripting.h | 5 +- sources/scripting/scriptheader.h | 118 +++++++++++++++++ sources/scripting/scriptlibrary.cpp | 194 ++++++++++++++++++++++++++++ sources/scripting/scriptlibrary.h | 72 +++++++++++ sources/shortcutmanager.cpp | 27 ++++ sources/shortcutmanager.h | 2 + tests/qttest/CMakeLists.txt | 12 ++ tests/qttest/tst_scriptheader.cpp | 122 +++++++++++++++++ 12 files changed, 737 insertions(+), 28 deletions(-) create mode 100644 sources/scripting/scriptheader.h create mode 100644 sources/scripting/scriptlibrary.cpp create mode 100644 sources/scripting/scriptlibrary.h create mode 100644 tests/qttest/tst_scriptheader.cpp diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 4593d5d56..8dad6c023 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -923,6 +923,9 @@ list(APPEND QET_SRC_FILES ${QET_DIR}/sources/scripting/qetscriptapi.h ${QET_DIR}/sources/scripting/qetscripting.cpp ${QET_DIR}/sources/scripting/qetscripting.h + ${QET_DIR}/sources/scripting/scriptheader.h + ${QET_DIR}/sources/scripting/scriptlibrary.cpp + ${QET_DIR}/sources/scripting/scriptlibrary.h ) if(QET_SPACEMOUSE_ENABLED) diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index 0218f7cf8..0e0a77e19 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -18,6 +18,7 @@ #include "qetdiagrameditor.h" #ifdef QET_HAS_SCRIPTING #include "scripting/qetscripting.h" +#include "scripting/scriptlibrary.h" #endif #include #include @@ -81,7 +82,9 @@ #include #include #include +#include #include +#include #include #ifdef BUILD_WITHOUT_KF # include "ui/nokde/kautosavefile.h" @@ -162,6 +165,11 @@ QETDiagramEditor::QETDiagramEditor(const QStringList &files, QWidget *parent) : readSettings(); // restoreGeometry before show() show(); readSettingsState(); // restoreState() must be called after show() in Qt6 +#ifdef QET_HAS_SCRIPTING + //A toolbar saved as shown while there were scripts stays out of + //the way while there are none. + if (m_script_actions.isEmpty()) m_scripts_tool_bar->hide(); +#endif //If valid file path is given as arguments uint opened_projects = 0; @@ -701,6 +709,17 @@ void QETDiagramEditor::setUpActions() tr("Exécute un script JavaScript sur le projet courant (voir qet.*" " dans le script pour l'API disponible)")); connect(m_run_script, &QAction::triggered, this, &QETDiagramEditor::slot_runScript); + + //Stored scripts are files in a folder, written by hand, by the + //script manager or by an assistant: open it to add one. + m_open_scripts_folder = new QAction(QET::Icons::FolderOpen, tr("Ouvrir le dossier des scripts"), this); + m_open_scripts_folder->setStatusTip( + tr("Chaque fichier .js de ce dossier qui commence par un en-tête" + " // ==QETScript== devient un bouton")); + connect(m_open_scripts_folder, &QAction::triggered, this, []() { + QDir().mkpath(ScriptLibrary::folder()); + QDesktopServices::openUrl(QUrl::fromLocalFile(ScriptLibrary::folder())); + }); #endif #ifdef QET_EXPORT_PROJECT_DB @@ -1206,6 +1225,14 @@ void QETDiagramEditor::setUpToolBar() addToolBar(Qt::TopToolBarArea, diagram_tool_bar); addToolBar(Qt::TopToolBarArea, m_add_item_tool_bar); addToolBar(Qt::TopToolBarArea, m_depth_tool_bar); + + m_scripts_tool_bar = new QToolBar(tr("Scripts", "toolbar title"), this); + m_scripts_tool_bar->setObjectName("scripts"); + addToolBar(Qt::TopToolBarArea, m_scripts_tool_bar); +#ifndef QET_HAS_SCRIPTING + m_scripts_tool_bar->toggleViewAction()->setVisible(false); + m_scripts_tool_bar->hide(); +#endif } /** @@ -1327,7 +1354,10 @@ void QETDiagramEditor::setUpMenu() menu_project -> addAction(m_terminal_numbering); menu_project -> addAction(m_reload_element_drawings); #ifdef QET_HAS_SCRIPTING - menu_project -> addAction(m_run_script); + m_scripts_menu = menu_project -> addMenu(tr("Scripts")); + rebuildScriptActions(); + connect(&ScriptLibrary::instance(), &ScriptLibrary::changed, + this, &QETDiagramEditor::rebuildScriptActions); #endif #ifdef QET_EXPORT_PROJECT_DB menu_project -> addSeparator(); @@ -2380,6 +2410,9 @@ void QETDiagramEditor::slot_updateUndoStack() */ void QETDiagramEditor::slot_updateComplexActions() { +#ifdef QET_HAS_SCRIPTING + updateScriptActions(); +#endif DiagramView *dv = currentDiagramView(); if(!dv) { @@ -3723,28 +3756,7 @@ 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); - } + if (!ensureScriptingEnabled(tr("Exécuter un script"))) return; const QString script_path = QFileDialog::getOpenFileName( this, @@ -3756,6 +3768,137 @@ void QETDiagramEditor::slot_runScript() { QetScripting::runOnProject(script_path, project, currentDiagramView()); } + +/** + @brief QETDiagramEditor::ensureScriptingEnabled + @return true if scripts may run, asking to switch them on if they are + off. Shared by "Run a script..." and the stored script buttons, so + there is one prompt to keep right. +*/ +bool QETDiagramEditor::ensureScriptingEnabled(const QString &title) +{ + // 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, + title, + 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 false; + } + QetSettings::setScriptingEnabled(true); + } + return true; +} + +/** + @brief QETDiagramEditor::runStoredScript + Run the stored script at @a path on the current project, as one undo + step named after the script. The file is read again on every run, so + an edit to it takes effect on the next click. +*/ +void QETDiagramEditor::runStoredScript(const QString &path, const QString &name) +{ + QETProject *project = currentProject(); + if (!project) return; + if (!ensureScriptingEnabled(name)) return; + QetScripting::runOnProject(path, project, currentDiagramView(), name); +} + +/** + @brief QETDiagramEditor::rebuildScriptActions + One action per stored script, in the Projet > Scripts menu and on the + Scripts toolbar, registered with ShortcutManager under + diagrameditor.script.: that one registration is what lists + it in the shortcut settings, the shortcut bar (S) and command search. + Rebuilt from scratch whenever the scripts folder changes. +*/ +void QETDiagramEditor::rebuildScriptActions() +{ + for (QAction *action : std::as_const(m_script_actions)) { + ShortcutManager::instance().unregisterAction( + action, action->property("qet_script_action_id").toString()); + delete action; + } + m_script_actions.clear(); + m_scripts_menu->clear(); + m_scripts_tool_bar->clear(); + + const ScriptLibrary &library = ScriptLibrary::instance(); + for (const ScriptLibrary::Script &script : library.scripts()) { + const ScriptHeader &h = script.header; + auto *action = new QAction(ScriptLibrary::icon(script), h.name, this); + action->setStatusTip(h.tooltip); + action->setToolTip(h.tooltip.isEmpty() ? h.name : h.name + QLatin1Char('\n') + h.tooltip); + action->setProperty("qet_script_action_id", ScriptLibrary::actionId(h.id)); + action->setProperty("qet_script_context", h.context); + connect(action, &QAction::triggered, this, [this, path = script.path, name = h.name]() { + runStoredScript(path, name); + }); + ShortcutManager::instance().registerAction(action, ScriptLibrary::actionId(h.id), + tr("Scripts"), + QKeySequence::fromString(h.shortcut)); + m_scripts_menu->addAction(action); + m_scripts_tool_bar->addAction(action); + m_script_actions << action; + } + + //A script with a header that cannot be used gets no button: say + //which file and why, where its author will look for the button. + const QStringList errors = library.errors(); + if (!errors.isEmpty()) { + m_scripts_menu->addSeparator(); + for (const QString &error : errors) { + QAction *ignored = m_scripts_menu->addAction(QET::Icons::DialogInformation, + tr("Ignoré : %1").arg(error)); + ignored->setEnabled(false); + } + } + + m_scripts_menu->addSeparator(); + m_scripts_menu->addAction(m_run_script); + m_scripts_menu->addAction(m_open_scripts_folder); + + //Show the toolbar when the first script arrives, hide it when the + //last one goes; in between it is the user's to show or hide. + const bool has_scripts = !m_script_actions.isEmpty(); + if (has_scripts != m_had_scripts) m_scripts_tool_bar->setVisible(has_scripts); + m_had_scripts = has_scripts; + + updateScriptActions(); +} + +/** + @brief QETDiagramEditor::updateScriptActions + Enable each script for what its header's @context asks for: always + (canvas), with something selected (selection), or with a conductor + selected (conductor). None without an open project. +*/ +void QETDiagramEditor::updateScriptActions() +{ + DiagramView *dv = currentDiagramView(); + Diagram *diagram = dv ? dv->diagram() : nullptr; + const bool selection = diagram && !diagram->selectedItems().isEmpty(); + const bool conductor = diagram && !diagram->selectedConductors().isEmpty(); + for (QAction *action : std::as_const(m_script_actions)) { + const QString context = action->property("qet_script_context").toString(); + bool enabled = currentProject() != nullptr; + if (context == QLatin1String("selection")) enabled = enabled && selection; + else if (context == QLatin1String("conductor")) enabled = enabled && conductor; + action->setEnabled(enabled); + } +} #endif /** diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h index 0345255d4..8455c4476 100644 --- a/sources/qetdiagrameditor.h +++ b/sources/qetdiagrameditor.h @@ -154,6 +154,10 @@ class QETDiagramEditor : public QETMainWindow void slot_reloadElementDrawings(); #ifdef QET_HAS_SCRIPTING void slot_runScript(); + void rebuildScriptActions(); + void updateScriptActions(); + bool ensureScriptingEnabled(const QString &title); + void runStoredScript(const QString &path, const QString &name); #endif void editDiagramProperties(DiagramView *); void editDiagramProperties(Diagram *); @@ -245,6 +249,7 @@ class QETDiagramEditor : public QETMainWindow *m_reload_element_drawings, ///< Action to redraw every placed element from its current definition #ifdef QET_HAS_SCRIPTING *m_run_script, ///< Action to run a JavaScript macro against the current project + *m_open_scripts_folder, ///< Action to open the folder stored scripts are read from #endif *m_export_project_db, ///Export to file the internal database of the current project *m_tile_window, ///< Show MDI subwindows as tile @@ -310,7 +315,13 @@ class QETDiagramEditor : public QETMainWindow *view_tool_bar = nullptr, *diagram_tool_bar = nullptr, *m_add_item_tool_bar = nullptr, - *m_depth_tool_bar = nullptr; + *m_depth_tool_bar = nullptr, + *m_scripts_tool_bar = nullptr; ///< One button per stored script +#ifdef QET_HAS_SCRIPTING + QMenu *m_scripts_menu = nullptr; + QList m_script_actions; ///< One per stored script, rebuilt when the folder changes + bool m_had_scripts = false; +#endif QUndoGroup undo_group; AutoNumberingDockWidget *m_autonumbering_dock; diff --git a/sources/scripting/qetscripting.cpp b/sources/scripting/qetscripting.cpp index a85bc9782..ab9943a51 100644 --- a/sources/scripting/qetscripting.cpp +++ b/sources/scripting/qetscripting.cpp @@ -113,7 +113,8 @@ namespace { constexpr int kScriptTimeoutMs = 30000; } -bool runOnProject(const QString &scriptPath, QETProject *project, DiagramView *view) +bool runOnProject(const QString &scriptPath, QETProject *project, DiagramView *view, + const QString &title) { // Checked here as well as at each caller, deliberately: this is the // one function that actually evaluates JavaScript, so it is the one @@ -173,7 +174,8 @@ bool runOnProject(const QString &scriptPath, QETProject *project, DiagramView *v QUndoStack *stack = (view && project) ? project->undoStack() : nullptr; if (stack) { stack->beginMacro(QObject::tr("Script : %1") - .arg(QFileInfo(scriptPath).completeBaseName())); + .arg(title.isEmpty() ? QFileInfo(scriptPath).completeBaseName() + : title)); api->setUndoGrouped(true); } @@ -229,7 +231,7 @@ int run(const QStringList &) return 1; } -bool runOnProject(const QString &, QETProject *, DiagramView *) +bool runOnProject(const QString &, QETProject *, DiagramView *, const QString &) { err << "This build of QElectroTech was compiled without the Qt Qml " "module, so JavaScript scripting is not available.\n"; diff --git a/sources/scripting/qetscripting.h b/sources/scripting/qetscripting.h index 6c9e746be..a1d8deafd 100644 --- a/sources/scripting/qetscripting.h +++ b/sources/scripting/qetscripting.h @@ -57,9 +57,12 @@ namespace QetScripting { @param view the active DiagramView, so the script's zoom methods have something to act on; nullptr from the headless entry point, where they become no-ops (see QetScriptApi). + @param title what the undo step is called; the script's file name + if empty. @return true if the script ran without throwing. */ - bool runOnProject(const QString &scriptPath, QETProject *project, DiagramView *view = nullptr); + bool runOnProject(const QString &scriptPath, QETProject *project, DiagramView *view = nullptr, + const QString &title = QString()); } diff --git a/sources/scripting/scriptheader.h b/sources/scripting/scriptheader.h new file mode 100644 index 000000000..ab2fd494d --- /dev/null +++ b/sources/scripting/scriptheader.h @@ -0,0 +1,118 @@ +/* + 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 . +*/ +#ifndef SCRIPTHEADER_H +#define SCRIPTHEADER_H + +#include +#include +#include + +/** + @brief The ScriptHeader struct + How a stored script's button looks, read from a comment block at the top + of the .js file itself, so one file is all there is to write by hand, + to share, or for an assistant to create: + + @code + // ==QETScript== + // @name Add revision note + // @icon note.svg (a file next to the script, or builtin:) + // @tooltip Puts a "Rev A" note on the folio on screen + // @shortcut Ctrl+Alt+R + // @context canvas (canvas, selection or conductor) + // @api 1 + // ==/QETScript== + @endcode + + Only @name is required. An unknown key is an error rather than ignored: + a misspelt "@shortcut" that silently did nothing would be harder to find + than a script refused with the line that is wrong. + + Header-only, with no QElectroTech dependency, so it is tested on its own. +*/ +struct ScriptHeader +{ + QString id; ///< file name without .js; the action id is diagrameditor.script. + QString name; + QString icon; + QString tooltip; + QString shortcut; ///< as QKeySequence::fromString() reads it + QString context = QStringLiteral("canvas"); + int api = 1; + QString error; ///< empty if the header is usable + + bool isValid() const { return error.isEmpty(); } + + static QStringList contexts() + { + return {QStringLiteral("canvas"), QStringLiteral("selection"), + QStringLiteral("conductor")}; + } + + /** + @brief parse + @param text : the whole script + @param id : the script's file name without its extension + */ + static ScriptHeader parse(const QString &text, const QString &id) + { + ScriptHeader h; + h.id = id; + static const QRegularExpression block( + QStringLiteral("//\\s*==QETScript==\\s*\\n(.*?)//\\s*==/QETScript=="), + QRegularExpression::DotMatchesEverythingOption); + const QRegularExpressionMatch m = block.match(text); + if (!m.hasMatch()) { + h.error = QStringLiteral("no // ==QETScript== header"); + return h; + } + static const QRegularExpression line_re( + QStringLiteral("^\\s*//\\s*@(\\w+)\\s+(.*?)\\s*$")); + const QStringList lines = m.captured(1).split(QLatin1Char('\n')); + for (const QString &line : lines) { + const QRegularExpressionMatch lm = line_re.match(line); + if (!lm.hasMatch()) { + continue; + } + const QString key = lm.captured(1); + const QString value = lm.captured(2); + if (key == QLatin1String("name")) h.name = value; + else if (key == QLatin1String("icon")) h.icon = value; + else if (key == QLatin1String("tooltip")) h.tooltip = value; + else if (key == QLatin1String("shortcut")) h.shortcut = value; + else if (key == QLatin1String("context")) h.context = value; + else if (key == QLatin1String("api")) h.api = value.toInt(); + else { + h.error = QStringLiteral("unknown header key @%1").arg(key); + return h; + } + } + if (h.name.isEmpty()) { + h.error = QStringLiteral("@name is required"); + } else if (!contexts().contains(h.context)) { + h.error = QStringLiteral("@context must be one of: %1") + .arg(contexts().join(QStringLiteral(", "))); + } else if (h.api != 1) { + h.error = QStringLiteral("@api %1 is not supported by this " + "version (1 is)").arg(h.api); + } + return h; + } +}; + +#endif // SCRIPTHEADER_H diff --git a/sources/scripting/scriptlibrary.cpp b/sources/scripting/scriptlibrary.cpp new file mode 100644 index 000000000..0107ab0af --- /dev/null +++ b/sources/scripting/scriptlibrary.cpp @@ -0,0 +1,194 @@ +/* + 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 . +*/ +#include "scriptlibrary.h" + +#include "../qetapp.h" + +#include +#include +#include +#include +#include +#include +#include + +ScriptLibrary &ScriptLibrary::instance() +{ + static ScriptLibrary library; + return library; +} + +/** + @brief ScriptLibrary::folder + Where stored scripts live: "scripts" in the user's data folder, next to + the user's own element and title block collections. +*/ +QString ScriptLibrary::folder() +{ + return QETApp::dataDir() + QStringLiteral("/scripts"); +} + +/** + @brief ScriptLibrary::actionId + The ShortcutManager id of a script's action. The "diagrameditor." + prefix is what puts it among the commands the shortcut bar can show. +*/ +QString ScriptLibrary::actionId(const QString &script_id) +{ + return QStringLiteral("diagrameditor.script.") + script_id; +} + +ScriptLibrary::ScriptLibrary() +{ + //An editor saving a file often writes it more than once, or + //replaces it through a rename: wait for it to settle. + m_rescan_timer.setSingleShot(true); + m_rescan_timer.setInterval(250); + connect(&m_rescan_timer, &QTimer::timeout, this, &ScriptLibrary::rescan); + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, + &m_rescan_timer, qOverload<>(&QTimer::start)); + connect(&m_watcher, &QFileSystemWatcher::fileChanged, + &m_rescan_timer, qOverload<>(&QTimer::start)); + rescan(); +} + +/** + @brief ScriptLibrary::watch + Watch the folder and each script in it, or, while the folder does not + exist yet, the data folder it will be created in. +*/ +void ScriptLibrary::watch() +{ + if (!m_watcher.files().isEmpty()) m_watcher.removePaths(m_watcher.files()); + if (!m_watcher.directories().isEmpty()) m_watcher.removePaths(m_watcher.directories()); + + const QDir dir(folder()); + if (!dir.exists()) { + if (QFileInfo::exists(QETApp::dataDir())) { + m_watcher.addPath(QETApp::dataDir()); + } + return; + } + m_watcher.addPath(dir.path()); + QStringList files; + for (const Script &s : std::as_const(m_scripts)) files << s.path; + for (const QString &e : std::as_const(m_errors)) files << dir.filePath(e.section(QLatin1Char(':'), 0, 0)); + if (!files.isEmpty()) m_watcher.addPaths(files); +} + +void ScriptLibrary::rescan() +{ + QList