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>
This commit is contained in:
ispyisail
2026-09-16 20:20:58 +12:00
parent eba258f6cd
commit 0920e82188
5 changed files with 323 additions and 34 deletions
+10 -6
View File
@@ -21,15 +21,16 @@
#include <QStringList>
class QETProject;
class DiagramView;
/**
@brief JavaScript scripting entry points (bugtracker #162).
A script sees a single global, `qet` (see QetScriptApi), exposing a
deliberately small, mostly read-only surface: folio/element/conductor
counts and the same export operations the `--export-*` CLI flags
provide. See QetScriptApi's class comment for what is and is not in
scope for this first version.
A script sees a single global, `qet` (see QetScriptApi): reading the
model, exporting, editing geometry through the real undo commands, and
a narrow set of navigation/messaging calls. See QetScriptApi's class
comment for the exact scope and why each group of capability stops
where it does.
*/
namespace QetScripting {
@@ -52,9 +53,12 @@ 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.
@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).
@return true if the script ran without throwing.
*/
bool runOnProject(const QString &scriptPath, QETProject *project);
bool runOnProject(const QString &scriptPath, QETProject *project, DiagramView *view = nullptr);
}