Files
qelectrotech-source-mirror/sources/scripting/qetscriptapi.h
T
ispyisail 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>
2026-09-16 20:20:58 +12:00

146 lines
6.6 KiB
C++

/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef QET_SCRIPT_API_H
#define QET_SCRIPT_API_H
#include <QObject>
#include <QString>
#include <QStringList>
class QETProject;
class DiagramView;
class Element;
/**
@brief The QetScriptApi class
The object a script sees as `qet` (bugtracker #162): batch/CI work and
human-written macros against an open project.
@b Scope. Three groups of capability, each added at a different point in
the discussion and each drawing its own line:
- @b Reading the model and @b exporting: folio/element/conductor counts,
and the same export operations the `--export-*` CLI flags provide.
Every export method is a thin wrapper around CLIExport::run() -- the
same, already-tested code path those flags use -- built from the
project's own file path rather than the live instance. That keeps this
file free of any dependency on cli_export.cpp's internals, at the cost
of re-opening the project from disk per call, which means @b none of
them ever see edits this script made with the methods below: only
save() writes the live instance. A script that edits then exports
@b must call save() first, or the export reflects the file as it was
before the script ran. Verified the hard way: an early version of this
file had save() go through the same reopen-from-disk path as the
exports, so it silently wrote back the unmodified original -- an
addElement() call counted correctly in memory and then vanished from
the saved file.
- @b Editing geometry, through the same undo commands the GUI itself
uses (AddGraphicsObjectCommand for placement, QPropertyUndoCommand on
the standard `pos` property for moves, DeleteQGraphicsItemCommand for
removal) -- Ctrl+Z undoes a script's edits exactly as it would the
equivalent manual ones, because they are, mechanically, the same
commands on the same stack. One consequence worth knowing, not a bug:
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
consecutive calls on one element, then undo/undo/redo/redo, land
where a merge predicts, not where two independent steps would.
- @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
returning false without one, since there is nothing to zoom.
Explicitly @b not in scope: driving arbitrary GUI actions or dialogs. A
script that could invoke any QAction by name could just as easily
trigger one that opens a modal QDialog::exec() with nobody there to
dismiss it -- exactly the hang class investigated for bugtracker #882.
Every method here is either non-blocking by construction or, for
messages, safe under QET::QetMessageBox's existing non-interactive mode
(already active for headless runs). Nothing here opens a dialog the
caller has to wait on.
*/
class QetScriptApi : public QObject
{
Q_OBJECT
public:
/**
@param project the project this API acts on
@param view the active DiagramView, when run interactively via
"Run Script..."; nullptr for the headless --run entry point.
Only the zoom methods use it -- everything else works either way.
*/
explicit QetScriptApi(QETProject *project, DiagramView *view = nullptr, QObject *parent = nullptr);
// -- read the model --
Q_INVOKABLE QString projectTitle() const;
Q_INVOKABLE QString filePath() const;
Q_INVOKABLE int folioCount() const;
Q_INVOKABLE QString folioTitle(int index) const;
Q_INVOKABLE int elementCount(int folioIndex) const;
Q_INVOKABLE int conductorCount(int folioIndex) const;
// -- export / save: thin wrappers around the --export-* CLI paths --
Q_INVOKABLE bool exportPdf(const QString &output, bool showTerminals = false);
Q_INVOKABLE bool exportPng(const QString &outDir, bool showTerminals = false);
Q_INVOKABLE bool exportSvg(const QString &outDir, bool showTerminals = false);
Q_INVOKABLE bool exportCables(const QString &output);
Q_INVOKABLE bool exportWires(const QString &output);
Q_INVOKABLE bool exportBom(const QString &output);
Q_INVOKABLE bool exportWiring(const QString &output);
Q_INVOKABLE bool exportNets(const QString &output);
Q_INVOKABLE bool exportLinks(const QString &output);
Q_INVOKABLE bool exportInfo(const QString &output);
Q_INVOKABLE bool setTitleBlock(const QString &output, const QStringList &assignments);
Q_INVOKABLE bool save(const QString &output);
// -- edit geometry, through the real undo commands --
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 deleteElement(int folioIndex, const QString &elementUuid);
Q_INVOKABLE bool undo();
Q_INVOKABLE bool redo();
Q_INVOKABLE bool canUndo() const;
Q_INVOKABLE bool canRedo() const;
// -- navigate and message --
Q_INVOKABLE bool selectElement(const QString &elementUuid);
Q_INVOKABLE void deselectAll(int folioIndex);
Q_INVOKABLE bool zoomFit();
Q_INVOKABLE bool zoomToContent();
Q_INVOKABLE bool zoomReset();
Q_INVOKABLE void showMessage(const QString &text);
// -- logging: a script has no console of its own --
Q_INVOKABLE void log(const QString &message);
private:
bool runFlag(const QString &flag, const QStringList &args);
Element *findElement(int folioIndex, const QString &elementUuid) const;
QETProject *m_project;
DiagramView *m_view;
};
#endif // QET_SCRIPT_API_H