diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index b13969404..f9bfec17f 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -29,8 +29,14 @@ #include "../qetmessagebox.h" #include "../qetproject.h" #include "../qetresult.h" +#include "../qetgraphicsitem/terminal.h" +#include "../qetinformation.h" +#include "../titleblockproperties.h" #include "../undocommand/addgraphicsobjectcommand.h" +#include "../undocommand/changeelementinformationcommand.h" +#include "../undocommand/changetitleblockcommand.h" #include "../undocommand/deleteqgraphicsitemcommand.h" +#include "../utils/conductorcreator.h" #include #include @@ -268,6 +274,48 @@ Element *QetScriptApi::findElement(int folioIndex, const QString &elementUuid) c return nullptr; } +Terminal *QetScriptApi::findTerminal(int folioIndex, const QString &elementUuid, + int terminalIndex, const QString &caller) +{ + Element *element = findElement(folioIndex, elementUuid); + if (!element) { + log(QStringLiteral("qet.%1: no element %2 on folio %3").arg(caller, elementUuid).arg(folioIndex)); + return nullptr; + } + const QList terminals = element->terminals(); + if (terminalIndex < 0 || terminalIndex >= terminals.count()) { + log(QStringLiteral("qet.%1: %2 has %3 terminal(s), no index %4") + .arg(caller, element->name()).arg(terminals.count()).arg(terminalIndex)); + return nullptr; + } + return terminals.at(terminalIndex); +} + +bool QetScriptApi::setInfoKey(int folioIndex, const QString &elementUuid, + const QString &key, const QString &value, const QString &caller) +{ + if (!m_project) return false; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.%1: project is read-only").arg(caller)); + return false; + } + if (key.isEmpty()) { + log(QStringLiteral("qet.%1: empty information key").arg(caller)); + return false; + } + Element *element = findElement(folioIndex, elementUuid); + if (!element) return false; + + const DiagramContext old_info = element->elementInformations(); + if (old_info.value(key).toString() == value) return true; // nothing to push + DiagramContext new_info = old_info; + new_info.addValue(key, value); + + auto *cmd = new ChangeElementInformationCommand(element, old_info, new_info); + m_project->undoStack()->push(cmd); + return true; +} + /** @brief QetScriptApi::addElement Place a new element on a folio, through the same AddGraphicsObjectCommand @@ -417,6 +465,207 @@ bool QetScriptApi::deleteElement(int folioIndex, const QString &elementUuid) return true; } +bool QetScriptApi::rotateElement(int folioIndex, const QString &elementUuid, double angle) +{ + if (m_project && m_project->isReadOnly()) { + log(QStringLiteral("qet.rotateElement: project is read-only")); + return false; + } + Element *element = findElement(folioIndex, elementUuid); + if (!element) return false; + + // The same property command RotateSelectionCommand pushes for an + // Element -- deliberately not RotateSelectionCommand itself, which + // works on diagram->selectedItems() and would mean quietly rewriting + // the user's selection to rotate one element by uuid. For a single + // element the two are mechanically identical: that class special-cases + // Element::Type to exactly this one command, and only adds a second, + // positional one when rotating a multi-item selection as a group. + auto *cmd = new QPropertyUndoCommand(element, "rotation", + QVariant(element->rotation()), + QVariant(element->rotation() + angle)); + cmd->setText(QObject::tr("Pivoter %1").arg(element->name())); + m_project->undoStack()->push(cmd); + return true; +} + +QStringList QetScriptApi::elementUuids(int folioIndex) const +{ + QStringList uuids; + if (!m_project) return uuids; + const QList diagrams = m_project->diagrams(); + if (folioIndex < 0 || folioIndex >= diagrams.count()) return uuids; + DiagramContent content(diagrams.at(folioIndex), false); + for (Element *elmt : std::as_const(content.m_elements)) { + uuids << elmt->uuid().toString(); + } + return uuids; +} + +QString QetScriptApi::elementName(int folioIndex, const QString &elementUuid) const +{ + Element *element = findElement(folioIndex, elementUuid); + return element ? element->name() : QString(); +} + +/** + @brief QetScriptApi::elementTerminals + The element's terminals, in the order addConductor() indexes them: one + entry per terminal, ": ( conductor(s))". Descriptive + rather than structured because its only job is to let a script -- or a + human reading a script's output -- see which index is which before + wiring anything to it. + + Indexes, not uuids, because a terminal uuid does not address a terminal + on a folio. Terminal::uuid() comes from the catalog .elmt definition + (see Terminal::stableUuid()), so it is empty for most of the installed + base, and where it is not, every instance of that same element carries + the same one -- two coils of one type placed side by side have + byte-identical terminal uuids, which is plainly visible in the saved + file of any project written through this API. The order of + Element::terminals() also comes from the definition, but it is at least + unambiguous within the element the caller has already named by uuid. +*/ +QStringList QetScriptApi::elementTerminals(int folioIndex, const QString &elementUuid) const +{ + QStringList list; + Element *element = findElement(folioIndex, elementUuid); + if (!element) return list; + const QList terminals = element->terminals(); + for (int i = 0 ; i < terminals.count() ; ++i) + { + Terminal *t = terminals.at(i); + list << QStringLiteral("%1: %2 (%3 conductor(s))") + .arg(i) + .arg(t->name().isEmpty() ? QStringLiteral("-") : t->name()) + .arg(t->conductorsCount()); + } + return list; +} + +QString QetScriptApi::elementInfo(int folioIndex, const QString &elementUuid, const QString &key) const +{ + Element *element = findElement(folioIndex, elementUuid); + if (!element) return QString(); + return element->elementInformations().value(key).toString(); +} + +bool QetScriptApi::setElementInfo(int folioIndex, const QString &elementUuid, + const QString &key, const QString &value) +{ + return setInfoKey(folioIndex, elementUuid, key, value, QStringLiteral("setElementInfo")); +} + +QString QetScriptApi::elementLabel(int folioIndex, const QString &elementUuid) const +{ + return elementInfo(folioIndex, elementUuid, QETInformation::ELMT_LABEL); +} + +bool QetScriptApi::setElementLabel(int folioIndex, const QString &elementUuid, const QString &label) +{ + return setInfoKey(folioIndex, elementUuid, QETInformation::ELMT_LABEL, label, + QStringLiteral("setElementLabel")); +} + +/** + @brief QetScriptApi::addConductor + Wire terminal terminalIndexA of one element to terminalIndexB of + another, on the same folio, through ConductorCreator -- the same class + the "draw a selection rectangle over terminals" GUI path uses. Going + through it rather than constructing a Conductor directly is what makes + the new conductor inherit an existing potential's properties and take + part in conductor auto-numbering; a hand-built one would be silently + outside both. + + Refuses, rather than creating anything, when the two terminals sit on + two different existing potentials: ConductorCreator then has to ask + which one's properties the new conductor should inherit, and it asks + with a plain modal QDialog that QET::QetMessageBox's non-interactive + mode does not cover -- so under headless --run there would be nobody to + answer it and the script would hang forever. Same reasoning, and the + same choice, as addElement() makes about the import-conflict dialog. + @return true if a conductor was created +*/ +bool QetScriptApi::addConductor(int folioIndex, + const QString &elementUuidA, int terminalIndexA, + const QString &elementUuidB, int terminalIndexB) +{ + if (!m_project) return false; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.addConductor: project is read-only")); + return false; + } + const QString caller = QStringLiteral("addConductor"); + Terminal *t1 = findTerminal(folioIndex, elementUuidA, terminalIndexA, caller); + Terminal *t2 = findTerminal(folioIndex, elementUuidB, terminalIndexB, caller); + if (!t1 || !t2) return false; + + if (t1 == t2) { + log(QStringLiteral("qet.addConductor: both ends are the same terminal")); + return false; + } + if (t1->isLinkedTo(t2)) { + log(QStringLiteral("qet.addConductor: those two terminals are already wired together")); + return false; + } + if (!t1->canBeLinkedTo(t2)) { + log(QStringLiteral("qet.addConductor: those two terminals cannot be linked")); + return false; + } + + const QList terminals {t1, t2}; + if (ConductorCreator::needsPotentialChoice(terminals)) { + log(QStringLiteral("qet.addConductor: those terminals are on two different existing " + "potentials, so creating a conductor would ask which one to inherit " + "-- refusing rather than open a dialog no script can answer")); + return false; + } + + Diagram *diagram = m_project->diagrams().at(folioIndex); + ConductorCreator creator(diagram, terminals); + Q_UNUSED(creator) + + // ConductorCreator has no return value and several ways to decline + // quietly, so report what actually happened rather than that it ran. + return t1->isLinkedTo(t2); +} + +int QetScriptApi::addFolio() +{ + if (!m_project) return -1; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.addFolio: project is read-only")); + return -1; + } + Diagram *diagram = m_project->addNewDiagram(); + if (!diagram) return -1; + return m_project->diagrams().indexOf(diagram); +} + +bool QetScriptApi::setFolioTitle(int folioIndex, const QString &title) +{ + if (!m_project) return false; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.setFolioTitle: project is read-only")); + return false; + } + const QList diagrams = m_project->diagrams(); + if (folioIndex < 0 || folioIndex >= diagrams.count()) return false; + Diagram *diagram = diagrams.at(folioIndex); + + // The folio title is one field of the title block properties, so it + // changes the way the title block dialog changes it: read the whole + // struct, set one member, push the command with both versions. + const TitleBlockProperties old_properties = diagram->border_and_titleblock.exportTitleBlock(); + if (old_properties.title == title) return true; + TitleBlockProperties new_properties = old_properties; + new_properties.title = title; + + auto *cmd = new ChangeTitleBlockCommand(diagram, old_properties, new_properties); + m_project->undoStack()->push(cmd); + return true; +} + bool QetScriptApi::undo() { if (!m_project || !m_project->undoStack()->canUndo()) return false; diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h index c0c5416f5..2544f483f 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -25,6 +25,7 @@ class QETProject; class DiagramView; class Element; +class Terminal; /** @brief The QetScriptApi class @@ -65,12 +66,34 @@ class Element; QPropertyUndoCommand merges consecutive commands on the same object+property when their text() also matches (QPropertyUndoCommand::mergeWith(), pre-existing), and - setElementPosition()/moveElement() always use the same text for a - given element -- so several position changes to the same element in a - row collapse into one undo step, the same way dragging an element - does, not one step per call. Verified against exactly that: two + setElementPosition()/moveElement()/rotateElement() always use the + same text for a given element -- so several position changes, or + several rotations, of the same element in a row collapse into one + undo step, the same way dragging or repeatedly rotating an element + does, not one step per call. setElementInfo()/setElementLabel() + behave the same way for the same reason, through + ChangeElementInformationCommand::mergeWith(). Verified against exactly that: two consecutive calls on one element, then undo/undo/redo/redo, land where a merge predicts, not where two independent steps would. + - @b Wiring, @b labelling and @b folios: create a conductor between two + terminals (ConductorCreator, the same class the GUI's + drag-a-rectangle-over-terminals path uses, so the result inherits an + existing potential's properties and joins conductor auto-numbering), + change an element's label or any other information key + (ChangeElementInformationCommand, which also tells the project + database what changed), add a folio (QETProject::addNewDiagram(), + already undoable) and set its title (ChangeTitleBlockCommand). With + addElement() these are what make a script able to draw rather than + only rearrange: before them a script could place two symbols and had + no way to connect them. + + Terminals are addressed by their @b index in Element::terminals(), + not by uuid, and elementTerminals() prints that indexing so a script + can see what it is about to wire. Terminal uuids look like the + obvious key and are not one: Terminal::uuid() is a property of the + catalog .elmt definition, empty for most of the installed base and, + where present, identical across every instance of that element -- so + it does not distinguish one placed coil's A1 from another's. - @b Navigating and @b messaging: select an element, zoom the active view, and show the user a message. Deliberately narrow: selection and messaging work with no view at all (headless `--run`); zoom is a no-op @@ -87,7 +110,11 @@ class Element; import-collision case that would otherwise reach QETProject::importElement()'s own ImportElementDialog::exec() and refuses instead, rather than let a plain QDialog (not routed through - QetMessageBox) block a script the same way. + QetMessageBox) block a script the same way. addConductor() declines the + same way, for the same reason, when the two terminals belong to two + different existing potentials and ConductorCreator would therefore ask + which one's properties to inherit -- measured: with that check removed, + exactly that call never returns. */ class QetScriptApi : public QObject { @@ -128,7 +155,29 @@ class QetScriptApi : public QObject Q_INVOKABLE QString addElement(int folioIndex, const QString &locationPath, double x, double y); Q_INVOKABLE bool setElementPosition(int folioIndex, const QString &elementUuid, double x, double y); Q_INVOKABLE bool moveElement(int folioIndex, const QString &elementUuid, double dx, double dy); + Q_INVOKABLE bool rotateElement(int folioIndex, const QString &elementUuid, double angle); Q_INVOKABLE bool deleteElement(int folioIndex, const QString &elementUuid); + + // -- address what is already there -- + Q_INVOKABLE QStringList elementUuids(int folioIndex) const; + Q_INVOKABLE QString elementName(int folioIndex, const QString &elementUuid) const; + Q_INVOKABLE QStringList elementTerminals(int folioIndex, const QString &elementUuid) const; + + // -- element information, through ChangeElementInformationCommand -- + Q_INVOKABLE QString elementInfo(int folioIndex, const QString &elementUuid, const QString &key) const; + Q_INVOKABLE bool setElementInfo(int folioIndex, const QString &elementUuid, const QString &key, const QString &value); + Q_INVOKABLE QString elementLabel(int folioIndex, const QString &elementUuid) const; + Q_INVOKABLE bool setElementLabel(int folioIndex, const QString &elementUuid, const QString &label); + + // -- wire two terminals together -- + Q_INVOKABLE bool addConductor(int folioIndex, + const QString &elementUuidA, int terminalIndexA, + const QString &elementUuidB, int terminalIndexB); + + // -- folios -- + Q_INVOKABLE int addFolio(); + Q_INVOKABLE bool setFolioTitle(int folioIndex, const QString &title); + Q_INVOKABLE bool undo(); Q_INVOKABLE bool redo(); Q_INVOKABLE bool canUndo() const; @@ -148,6 +197,10 @@ class QetScriptApi : public QObject private: bool runFlag(const QString &flag, const QStringList &args); Element *findElement(int folioIndex, const QString &elementUuid) const; + Terminal *findTerminal(int folioIndex, const QString &elementUuid, int terminalIndex, + const QString &caller); + bool setInfoKey(int folioIndex, const QString &elementUuid, + const QString &key, const QString &value, const QString &caller); QETProject *m_project; DiagramView *m_view; diff --git a/sources/utils/conductorcreator.cpp b/sources/utils/conductorcreator.cpp index 167a2a37d..4a0ce5a37 100644 --- a/sources/utils/conductorcreator.cpp +++ b/sources/utils/conductorcreator.cpp @@ -95,6 +95,29 @@ void ConductorCreator::create(Diagram *d, const QPolygonF &polygon) } } +/** + @brief ConductorCreator::needsPotentialChoice + Whether creating a potential between these terminals would ask the user + to choose which of several existing potentials to inherit from -- that + is, whether the constructor would reach PotentialSelectorDialog. + + This exists for callers with nobody there to answer: the dialog is a + plain QDialog::exec(), not routed through QET::QetMessageBox, so its + non-interactive mode does not cover it and a headless caller would hang + on it indefinitely. Such a caller can check this first and decline. + Exposed here, rather than reimplemented by the caller, so the condition + cannot drift away from the one setUpPropertieToUse() actually applies. + @param terminals_list the terminals a potential would be created between + @return true if the constructor would open the dialog +*/ +bool ConductorCreator::needsPotentialChoice(const QList &terminals_list) +{ + if (terminals_list.size() <= 1) { + return false; + } + return existingPotential(terminals_list).size() >= 2; +} + /** @brief ConductorCreator::propertieToUse @return true if the caller should proceed with conductor creation, @@ -104,7 +127,7 @@ void ConductorCreator::create(Diagram *d, const QPolygonF &polygon) */ bool ConductorCreator::setUpPropertieToUse() { - QList potentials = existingPotential(); + QList potentials = existingPotential(m_terminals_list); //There is an existing potential //we get one of them @@ -145,14 +168,15 @@ bool ConductorCreator::setUpPropertieToUse() @brief ConductorCreator::existingPotential Return the list of existing potential of the terminal list + @param terminals_list the terminals to inspect @return c_list QList */ -QList ConductorCreator::existingPotential() +QList ConductorCreator::existingPotential(const QList &terminals_list) { QList c_list; QList t_exclude; - for (Terminal *t : m_terminals_list) + for (Terminal *t : terminals_list) { if (t_exclude.contains(t)) { continue; @@ -166,9 +190,9 @@ QList ConductorCreator::existingPotential() //in the same potential of c, and if true, exclude this terminal from the search. for (Conductor *c : t->conductors().first()->relatedPotentialConductors(false)) { - if (m_terminals_list.contains(c->terminal1)) { + if (terminals_list.contains(c->terminal1)) { t_exclude.append(c->terminal1); - } else if (m_terminals_list.contains(c->terminal2)) { + } else if (terminals_list.contains(c->terminal2)) { t_exclude.append(c->terminal2); } } diff --git a/sources/utils/conductorcreator.h b/sources/utils/conductorcreator.h index 515081b1f..ec287bddd 100644 --- a/sources/utils/conductorcreator.h +++ b/sources/utils/conductorcreator.h @@ -38,10 +38,11 @@ class ConductorCreator public: ConductorCreator(Diagram *d, QList terminals_list); static void create(Diagram *d, const QPolygonF &polygon); - + static bool needsPotentialChoice(const QList &terminals_list); + private: + static QList existingPotential(const QList &terminals_list); bool setUpPropertieToUse(); - QList existingPotential(); Terminal *hubTerminal();