mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-27 20:44:13 +02:00
279b001a164beaa0c6f41c10c8b9123e26462bb4
17 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
279b001a16 |
Let a script apply element auto-numbering
useElementAutoNum(name) select the current element numbering context numberElement(folio, el) give one element its label from it numberElement calls Element::setUpFormula(), the call the "add element" tool makes right after placing an element, so a script gets the same numbering: three coils numbered in turn are K1, K2, K3, and the counter persists in the project (after a reload the next element is K3). It is a separate call rather than a change to addElement(), which is merged code: numbering what it places would change what an existing script produces the moment its project happens to have a context selected. A slave or a report is refused, since it takes its label from its master, and so is a project with no context selected, instead of reporting a success that did nothing. setUpFormula() has a hazard for an element that is already placed. It writes the label straight into the element's information and pushes only the counter's advance onto the undo stack. Placing a new element hides that, because undoing the placement removes the element; for an existing one, a single undo rolled the counter back and left the label, so c3 stayed "K3" while the counter went back to expecting K3 and the next numbering would repeat a label it had forgotten. So the label it computed is taken, the information put back, and the change pushed as a command inside the same macro as the counter: one undo now reverts both, and renumbering c3 afterwards yields K3 again. Redo and the database agree. Folio auto-numbering is deliberately not offered: in the application it spawns whole new folios from a context, which is a different operation from labelling. "Renumber existing conductors" has no equivalent to bind -- conductor numbering is applied when a conductor is created or moved, and QElectroTech has no renumber-all action. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
59cc758b7f |
Let a script control the text fields drawn on a symbol
elementTexts() addElementText() setElementTextProperty() elementTextProperty() deleteElementText() A symbol arrives with the fields its definition gives it -- coil bobine_ka_a_remanence has A2, A1 and a label -- and until now a script could fill the value the label shows (setElementLabel) but not control the fields themselves: where each sits, its size, whether it is framed, what it is bound to, or add and delete one. A field's source is "text" (a fixed string), "info" (follows one of the element's information keys, so it follows setElementInfo and setElementLabel) or "composite" (a formula). Changes go through QPropertyUndoCommand on the item's own properties, as the element-texts editor does, and adding through AddElementTextCommand. An unknown source, a key that is not an element information key, a non-positive size and a bad index are refused with a reason. Two things called text differ for an information-bound field. The "text" property is the stored string, an unused placeholder (empty, or "Texte" for a newly added field); "shows" is what is drawn. I first believed the displayed text was stale in-session and wrote a helper to read it from the element's information instead. That was wrong: comparing toPlainText() against elementInfo() at seven points across relabel, rebinding, setting and undo found no difference. It was the stored string that looked stale, and the helper and the comment claiming the item "refreshes lazily" are removed. The header now says which is which. Fields are addressed by index in the element's own list, which follows the definition and shifts on delete; undoing a deletion puts the field back at the end. Consecutive info/label changes on one element merge into one undo step (ChangeElementInformationCommand::mergeWith), so one undo can revert several -- noted, not a bug. Verified on a coil: the label field moved, enlarged to 14 pt and framed, a new field bound to "comment" showing "24VDC coil", all through the project, exported to PNG and looked at. The saved file carries the moved position, size, frame and bound value, and a reload reads them back. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
0c958d3b66 |
Drop "version" from the folio properties: it was a silent no-op
setFolioProperty(f, "version", "V9-USER") returned true and changed nothing. TitleBlockProperties::version is the file-format stamp QElectroTech writes on every save, so a value set through the API reads back as "0.200.1-dev" and is what ends up in the file. A property that reports success and does nothing is the failure this API is careful to avoid everywhere else, so it is removed rather than documented. It went in untested: the earlier verification set author and plant and never tried the other six names. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
528d33792d |
Say what order a terminal index actually follows
The comment on elementTerminals() said the order of Element::terminals() "also comes from the definition", which reads as file order and is wrong. Element::parseTerminal() re-sorts the list on every insertion, top to bottom then left to right on each terminal's local position, so index 0 is the topmost terminal whatever order the .elmt lists them in. bobine_ka_a_remanence.elmt writes A2 (y=20) before A1 (y=-20) and index 0 is A1. Measured by placing 400 shipped elements and reading the real order back: a top-to-bottom, left-to-right prediction matched all 400, while file order matched only the 100 where the two happen to coincide. Of the 837 shipped elements with distinct named terminals, 619 list them in a different order than QElectroTech indexes them. Comment only. Two terminals at one point tie and the sort is not stable; the comment says so. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
fc41cf9f5d |
Let a script place an image
images() addImage() setImageScale() setImageRotation() deleteImage() addImage reads a file the way the "add image" tool does after its file dialog and pushes the same AddGraphicsObjectCommand. The pixels are copied into the project, which DiagramImageItem::toXml writes inline, so the saved file does not refer to the original path: verified by moving the source file away and reloading, where the image and its scale came back. The price is that the project grows by about the size of the image, so files over 10 MB are refused; so are unreadable files, non-images and missing paths, each with its own reason. Scaling sets both axes as one undo step. Images are addressed by index in a position-sorted listing like texts and shapes, by the on-screen bounding box -- so because an image turns and scales about its centre, scaling or rotating one can change where it sorts, and the header says to re-list after either. Observed: a 2x scale moved a listed top-left from (100,100) to (68,84), which is that, not a displacement. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
7f04bc1572 |
Let a script style a shape
shapeProperty()/setShapeProperty() color, fill, width, line-style, rotation Through QPropertyUndoCommand on the pen, brush and rotation properties the shape's own style editor changes, so it undoes like a hand edit. fill takes a colour or "none" for no fill; line-style is solid, dashed, dotted or dashdot. A colour that does not parse, a non-positive width, an unknown style or property and a bad index are refused with a reason. Verified on a rectangle: all five set and read back, fill "none" reads back as none, undo restores the previous fill, and the whole look survives save and reload. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
27bbf51024 |
Let a script style a conductor
setConductorProperty()/conductorProperty() now also take the properties that control how a conductor looks, under the names the .qet file uses: style normal, dashed or dashdotted bicolor true/false, with color2 as the second colour dash-size positive integer condsize positive number (line width) numsize positive integer (text size) displaytext true/false Values are validated rather than stored: a boolean other than true/false, a non-positive size, an unparseable colour or a line style the file format cannot express is refused, because ConductorProperties would write the latter back as a solid line and silently lose it. The change is still applied to the whole potential. Verified: all eight set, read back, written to the file in its own form (style as "line-style: dashed;", displaytext as 0) and read back again after a reload. Four invalid values decline. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
65299e1178 |
Let a script define and select auto-numbering
autoNums(kind) list contexts and their formulas addAutoNum(kind, name, parts) define one; parts are "type[:value[:increase]]" removeAutoNum(kind, name) useConductorAutoNum(folio, n) new conductors on a folio take it Kinds are conductor, element and folio. Part types are the ones the auto-numbering dialog offers; anything NumerotationContext rejects, an unknown kind, a non-numeric increase or a missing context is refused with a reason rather than dropped. Defining and removing a context is not undoable, because the application does it through direct project calls; only the counter advance is on the undo stack. useConductorAutoNum sets both the folio's name and the project's current name, because the numbering code reads the context by one and writes the advanced counter back under the other. Verified: a "W" + unit context selected on a folio, two conductors wired, W1 and W2 in the saved file, and the context survives reload. Depends on the preceding ConductorCreator fix for the database to agree with the drawing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
5b739c3338 |
Let a script build a terminal strip
terminalStrips() the project's strips, in its own order addTerminalStrip() AddTerminalStripCommand, as the creation dialog does removeTerminalStrip() RemoveTerminalStripCommand addTerminalToStrip() AddTerminalToStripCommand Only terminal-type elements can be added, the restriction the editor enforces by construction; anything else, or a terminal already on a strip, or a bad index, declines with a reason. Verified: two terminal elements placed and added, listing shows 2 terminals, removal and undo, and the strip with its terminals survives save and reload. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
a1e4e153ae |
Let a script delete a conductor or a folio, and set title block fields
deleteConductor() the conductor on a terminal, addressed as elsewhere removeFolio() RemoveDiagramCommand, the GUI's delete-folio command setFolioProperty() title, author, filename, plant, locmach, indexrev, folioProperty() version, folio -- via ChangeTitleBlockCommand deleteConductor removes only the named conductor; unlike a property change it is not potential-wide, and DeleteQGraphicsItemCommand rebuilds the rest of the potential so it stays connected, as when a user selects one conductor and presses Delete. Verified: deleting one leaf of a three-terminal potential leaves the other conductor, with its number. removeFolio skips the GUI's confirmation box, which nobody could answer headlessly. Undo and redo both work; later folio indexes shift down. Undoing a removal prints a UNIQUE-constraint warning from projectDataBase::addDiagram. That is inside RemoveDiagramCommand::undo(), which the GUI runs too, so it predates this change. The date and template are not offered as folio properties: the date has a use-current-date mode a plain string cannot express honestly. Verified headlessly, including save/reload of the title block fields. Qt 6.10.2, build clean, ctest matches master. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
46c35d2297 |
Let a script query the project database
Every structural question this API could answer, it answered by walking live objects. The project builds a SQLite database that already knows most of them, and nothing outside the application could reach it. tables() what is queryable, tables and views query(sql) rows, one object per row queryError() why the last one returned nothing This is not a new door. QElectroTech already ships a "Requête SQL personnalisée" box in the element-query dialog where a user types arbitrary SQL, guarded by projectDataBase::isReadOnlySelect(); query() goes through projectDataBase::newQuery(), which applies that same rule and returns the same rejection message. A script gets what a user already has, and neither can write: DELETE, UPDATE and a chained "SELECT 1; DROP TABLE" are all refused before reaching SQLite. An empty result and a failure are told apart. query() returns no rows for both, so queryError() carries the reason -- a refusal, or SQLite's own message for a bad column -- and is empty when the query simply matched nothing. Conflating those is how a silent typo in a column name becomes "there are no such elements". No updateDB() before querying, and that is a measured decision rather than an omission. A script that has just edited something is the expected caller, so a stale cache was the obvious hazard; but projectDataBase maintains itself incrementally through addElement(), elementInfoChanged(), addConductor() and the rest, which the undo commands behind every edit already call. Tested both ways on the cases most likely to go stale -- an element added and labelled, a conductor property changed -- each queried immediately afterwards through both the table and the view. Identical counts with the rebuild and without it, and updateDB() repopulates every table, so calling it per query would have been real cost for no benefit. The comment says so, so it is not added back on the assumption it must be needed. The views are the surface to depend on: element_nomenclature_view, project_summary_view and wiring_list_view exist to be queried. The tables are how the cache is arranged today and a column may move -- which is why tables() lists both and the header says which is which. Verified against examples/industrial.qet, the largest shipped project: 618 elements counted, the busiest wire numbers ranked (0VDC 93 times, 24V2 64), and duplicate element labels found by GROUP BY ... HAVING -- V6 seven times, V5 six -- which is a design-rule question no tool here could previously ask. Qt 6.10.2, ctest matches master. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2adc58c1c0 |
Let a script add the text and shapes a folio carries
The drawing furniture beside the circuit: a free-standing note, a line,
a rectangle, an ellipse, a polygon.
texts() addText() setTextContent() setTextColor()
setTextRotation() deleteText()
shapes() addShape() deleteShape()
Added with the same AddGraphicsObjectCommand the corresponding GUI tools
use, and changed through the plainText/color/rotation properties those
items already publish, so a script's note undoes like a hand-placed one.
These are addressed by index into a listing sorted by position, reading
order, because they have no better identity: unlike an element they carry
no uuid, and unlike a conductor they have no terminal to be named by.
Position is what they have and it persists, so the ordering survives a
save and reload -- verified by listing before and after, including a
rotated text whose bounding box moves. It does not survive adding or
deleting one: indexes after that point shift the way a list's do, which
is why texts() and shapes() exist rather than a caller keeping a handle.
The sort is on sceneBoundingRect(), not pos(). A QetShapeItem keeps its
geometry in its line/rect/polygon and leaves pos() at the origin, so
sorting on pos() put three shapes drawn in three different places all at
(0, 0) and made every shape index refer to whichever the set yielded
first -- which is what the first version of this did, and the test that
caught it was asking for three shapes and getting index 0 three times.
Path is deliberately not offered: it is built by successive clicks and
has no two-point form to give here.
Verified headlessly: three texts added bottom-up and listed in reading
order, edited, recoloured, rotated, one deleted; three shapes added,
listed with their real geometry, the middle one deleted and the right one
gone; unknown shape name, invalid colour and out-of-range index all
decline with a reason. Saved, reloaded, both listings identical.
Qt 6.10.2, build clean, ctest matches master, qet-lint clean on the
generated project, qet-coherence-check clean on the example corpus.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
82262c5980 |
Let a script number a conductor and link a cross-reference
Two gaps left over from the drawing verbs. A script could create a
conductor but not say what it was -- no number, colour, section or
formula -- and could not link a master to its slave, although
LinkElementCommand has been there all along and nothing bound it.
conductors() what is on this folio, and how to address it
conductorProperty()
setConductorProperty() num, formula, function, bus, cable,
tension_protocol, conductor_color,
conductor_section, color, text_color
elementLinkType() simple / master / slave / next_report / ...
linkedElements()
linkElements() two folio indices: a master and its slave are
normally on different folios
unlinkElement()
The property names are the ones the .qet file uses for the same fields,
so what a script sets is what a reader of the file sees rather than a
third spelling invented here.
A property is applied to every conductor of the same electrical
potential, not to the one conductor named. That is the rule the
application already follows -- SearchAndReplaceWorker pushes one
QPropertyUndoCommand per conductor of relatedPotentialConductors()
inside a macro -- because a wire number describes a potential, not one
drawn segment; setting it on one and leaving the rest of the potential
disagreeing would produce a file no GUI action could have produced.
Linking asks LinkElementCommand::isLinkable() rather than re-deriving
its rules, so a script cannot make a link the GUI would refuse: master
to master, a PLC master to a non-PLC slave, a next-report to another
next-report, or anything to an already-taken target.
A conductor is addressed as "the conductor on terminal i of element U".
It has no identity of its own to use instead: conductors carry no
persisted uuid, and the terminal1/terminal2 ids in the file are
folio-scoped integers QElectroTech renumbers on every save. Since the
change is potential-wide, any terminal of the potential names it equally
well, so in practice a potential is addressed from one of its leaves; a
terminal carrying several conductors names none of them and is refused
rather than guessed at.
Verified headlessly. Conductor: num, section and colour set from one end
of a potential and read back from the other, saved and reloaded, present
in the XML. Propagation shown to discriminate, which took two tries --
the first attempt wired A.0-B.0 and B.1-C.0 and saw no propagation,
correctly, because a coil's two terminals are opposite ends of the coil
and not one potential. Wiring a real hub at A.0 instead, a number set
via the B leaf appears on the C conductor too, in memory and in the
saved file. Cross-reference: a master on one folio linked to a slave on
another, linkedElements() agreeing from both ends, surviving save and
reload with link_uuid written on both folios; master-to-master,
self-link, unlink and relink all behave. Unknown property, invalid
colour, bare terminal and ambiguous terminal all decline with a reason.
Qt 6.10.2, build clean, ctest matches master, qet-coherence-check clean
on the example corpus, qet-lint clean on the generated projects.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
c740cdf1ac |
Let a script wire, label and rotate, not only place
The scripting API (bugtracker #162) could place an element and move it, and could count conductors but not make one. So a script could put a coil and a motor on a folio and had no way to connect them, which is most of what drawing is. This adds the missing verbs: addConductor() wire terminal i of one element to terminal j of another rotateElement() setElementInfo() any information key setElementLabel() the label key, by name, since it is the one people want addFolio() setFolioTitle() elementUuids() what is on this folio elementName() elementTerminals() which terminal index is which, before wiring it Each goes through the command the GUI already uses, so a script's edits undo like manual ones and reach the project database the same way: ConductorCreator (the drag-a-rectangle-over-terminals path, which is what makes a new conductor inherit an existing potential's properties and join auto-numbering), ChangeElementInformationCommand, QETProject::addNewDiagram(), ChangeTitleBlockCommand. rotateElement() pushes the same QPropertyUndoCommand on "rotation" that RotateSelectionCommand pushes for an Element, rather than RotateSelectionCommand itself, which works on the diagram's selection and would mean rewriting the user's selection to rotate one element. Terminals are addressed by index, not uuid. 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 -- two coils of the same type placed side by side have byte-identical terminal uuids, so a uuid cannot say which coil's A1 is meant. elementTerminals() exists so a script can see the indexing instead of guessing it. The one real hazard is that ConductorCreator asks the user which potential to inherit from when the two terminals sit on two different existing ones, and it asks with a plain modal QDialog that QET::QetMessageBox's non-interactive mode does not cover -- so under headless --run there is nobody to answer and the call never returns. Measured: with the check removed, that one call hangs until killed; with it, it declines in 0.4 s. addConductor() therefore refuses that case, the same way and for the same reason addElement() already refuses the import-conflict dialog. To make that check without duplicating the condition, existingPotential() becomes static over an explicit terminal list and ConductorCreator gains a public needsPotentialChoice() predicate. Behaviour of the GUI path is unchanged; setUpPropertieToUse() passes m_terminals_list to the same code it called before. Verified headlessly against a copy of examples/ArduinoLCD.qet: new folio titled, two coils placed, wired, labelled, an info key set and the element rotated; saved, reloaded, and the conductor, label, title and rotation (persisted as orientation="1") all read back. Re-saving the result is byte-identical. qet-lint clean on the generated project; qet-coherence-check clean on it and on the 24-project example corpus, and shown to report 9 findings on a deliberately broken copy of the same file, so the clean result discriminates. Qt 6.10.2, ctest identical to master (the 61 failures are the vendored KDE ECM suite, present on both). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4a83521306 |
Fix scripting review findings: import, save, backups, timeout
Addresses scorpio810's review of PR #891: - addElement() now imports the element into the project's own embedded collection first (QETProject::importElement()), same as the drag-from-collection-panel path -- otherwise the saved .qet referenced a definition outside the project, missing on another machine. Also found and fixed while wiring this up: ElementsLocation::setPath() forces any path to embed:// once a non-null project is passed, so the unconditional ElementsLocation(locationPath, m_project) this method used before silently broke every common://custom:// call. Refuses on an import-collision case that would otherwise reach QETProject::importElement()'s own modal ImportElementDialog, with nobody there to answer it in a script. - addElement()/setElementPosition()/moveElement()/deleteElement() refuse on a read-only project, matching their GUI equivalents. - save() goes through QETProject::write() when no path is given (read -only handling, saveddate/savedtime, QSaveFile via writeXmlFile()) and QET::writeXmlFile() directly for an explicit output path, instead of a plain QFile that skipped all of that. - Interactive "Run Script...": exports briefly disable project backups around the temporary QETProject CLIExport::run() opens on the same file, so it doesn't race the real open project's own KAutoSaveFile. Restored right after -- the headless --run path is untouched, since it depends on backups staying off for the whole run. - A watchdog thread now calls QJSEngine::setInterrupted() after 30s, so a runaway script (`while(true){}`) can't freeze the GUI or hang a CI job forever. First version used sleep_for() and blocked every run, fast ones included, for the full 30s on join() -- caught by testing a one-line script, fixed with wait_for() on a condition variable so a script that finishes early wakes the watchdog immediately. - Interactive script errors now also show a QetMessageBox, not just stderr (invisible on Windows). - Ran update_translations (lupdate) to pick up the 37 strings this feature had not yet added to the .ts files. Not applied: wrapping the whole script run in one undo macro. It would make the script's own qet.undo()/qet.redo() calls silent no-ops for the run's duration -- QUndoStack ignores undo()/redo() while a macro is open -- which would break that already-shipped, explicitly requested capability to get one convenience Ctrl+Z instead. Verified: Qt 6.10.2, builds clean, ctest 6/6. Ran each fix against a real project: addElement() on a common:// path now succeeds and the saved file embeds the definition (embed://import/...); read-only project refuses addElement(); save("") and save(otherpath) both produce a correctly embedded file; `while(true){}` under --run is interrupted at 30s where it previously hung forever, and a normal script now exits in ~0.3s instead of blocking for the full timeout budget. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
0920e82188 |
Add geometry editing, undo integration, and navigation to scripting
Extends the scripting surface from the previous commit with exactly the three things explicitly scoped out there, per follow-up direction: editing geometry, undo integration, and driving the GUI -- the last one narrowed to select/zoom/message after discussion, since "invoke any menu action by name" would let a script trigger a modal QDialog::exec() with nobody there to dismiss it, the same hang class investigated for bugtracker #882. ## New capabilities - addElement/setElementPosition/moveElement/deleteElement, through the same undo commands the GUI itself uses: AddGraphicsObjectCommand (the same one drag-from-collection-panel placement uses), QPropertyUndoCommand on the standard `pos` property, and DeleteQGraphicsItemCommand (refuses a non-deletable terminal, same as the Delete key). - undo/redo/canUndo/canRedo against the project's real QUndoStack -- the same one QETDiagramEditor's Ctrl+Z is wired to via undo_group.activeStack(), not a parallel mechanism. - selectElement/deselectAll (scene state, no view required -- works headless), zoomFit/zoomToContent/zoomReset (need the active DiagramView, so false headless where there is nothing to zoom), and showMessage (a modal QET::QetMessageBox::information -- safe headless because non-interactive mode is already on for the whole process before any script runs). ## Two real bugs caught by testing this, not assumed away 1. save() was still going through the same reopen-from-disk path as every export method: it opened a *second*, unmodified copy of the project from its file on disk and rewrote that. addElement() and friends operate on the live in-memory project, so nothing they did ever reached the saved file -- an element counted correctly in memory and then silently vanished from the output. Fixed by having save() write m_project->toXml() directly, the only method that touches the live instance rather than a fresh copy of the file. 2. A script calling setElementPosition() then moveElement() on the same element produced a saved position that didn't match either call, and undo/redo didn't step through them independently. Traced to QPropertyUndoCommand::mergeWith() (pre-existing, not new): consecutive commands on the same object+property merge when their text() also matches, and both calls build the identical "Déplacer %1" text for a given element -- exactly the same collapsing dragging an item repeatedly gets. Not a bug in the new code; a wrong assumption in the first test. Re-verified against the correct, merge-aware expectation: add -> merged move -> undo (back to first position) -> undo (element removed) -> redo (element back) -> redo (merged move reapplied) landed at the exact predicted final position, read back from the saved XML. ## Verified Qt6, build clean from a fresh reconfigure, ctest 6/6. - Headless: addElement returns a real uuid and the count updates; select/set-position/move all report correctly; the merge-aware undo/redo/save round trip above, confirmed against the saved file's actual XML, not just in-memory counters. - Corpus: the existing read-model smoke script re-run against all 24 shipped example projects on the fixed binary, 0 failures. - zoomFit correctly returns false headless (no view to act on), confirming the "narrowed GUI-driving" scope holds in code, not just in the doc comment. - GUI: running the add-only script via "Exécuter un script..." marked the project [modifié] in the title bar, the same change-tracking path a manual edit goes through -- consistent with the undo command actually being pushed onto the project's real stack rather than some side channel invisible to the rest of the application. Refs #162. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
eba258f6cd |
Add JavaScript scripting: --run and "Run Script..." (bugtracker #162)
Following up on my own comments there: a deliberately small, mostly read-only scripting surface, exposed to scripts as a single global `qet` object (QetScriptApi) built on QJSEngine rather than an embedded Python interpreter -- no new toolchain to package (QJSEngine ships in every Qt SDK QET already targets, via the Qml module), no GIL, no version pinning, automatic reflection of the QObject-derived core classes' own methods with no hand-written binding layer. ## What a script can do - Read the model: project title, file path, folio count/titles, element/conductor counts per folio. - Trigger the same operations the --export-* CLI flags already do (pdf/png/svg/cables/wires/bom/wiring/nets/links/info), plus set-titleblock and save -- thin wrappers around CLIExport::run(), reusing its already-tested logic rather than duplicating it. Deliberately NOT in this version: creating or editing diagram geometry, undo integration, driving the GUI. All explicitly out of scope per the discussion on #162. ## Two entry points, both built and tested - `qelectrotech --run script.js project.qet` -- headless/CI. - Projet > "Exécuter un script..." -- an interactive macro against the currently open project. Export/save calls act on the project's file on disk (see QetScriptApi's class comment for why), so unsaved GUI edits aren't visible to the script; save first if that matters. ## Optional dependency, not a hard requirement Qt::Qml is probed the same way QtPdf already is in this codebase: QUIET, non-fatal, behind a QET_HAS_SCRIPTING compile definition. A build without it compiles and links identically; the CLI flag and menu action are simply absent (main.cpp) or compile to a clear "not available" stderr message rather than silently disappearing (qetscripting.cpp), matching the existing QtPdf pattern rather than introducing a new one. One real bug caught building this, not assumed away: my first pass conditionally excluded the new source files from QET_SRC_FILES behind `if(QET_HAS_SCRIPTING)` inside qet_compilation_vars.cmake -- but that file is included before QET_HAS_SCRIPTING is set in the top-level CMakeLists.txt, so the variable didn't exist yet at that point and the files were silently never compiled, only caught by an undefined-symbol link error. Fixed by following the QtPdf file's own precedent: compile the files unconditionally, guard their Qt::Qml-dependent content internally instead. ## Verified Qt6, build clean, ctest 6/6. - Headless: a script reading project/folio/element/conductor counts, calling exportInfo() and exportPdf() against a real project -- correct JSON, a real single-page PDF confirmed with `file`. Error paths: a thrown script exception reports file:line:message and exit 1; missing script/project arguments exit 2 (matching CLIExport's own usage-error convention); a missing project file is reported and does not hang. - Corpus: the same read-model script run against all 24 shipped example projects, 0 failures. - GUI: "Exécuter un script..." opens a real file dialog filtered to *.js, running the picked script against the live open project produced the exact expected JSON export file, and the application was still fully responsive afterward. Refs #162. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |